/*
* Copyright © 2015 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package co.cask.cdap.internal.app.runtime.batch.distributed;
import co.cask.cdap.internal.asm.Methods;
import com.google.common.base.Preconditions;
import com.google.common.io.OutputSupplier;
import com.google.common.io.Resources;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.URL;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
/**
* Helper class to generate main classes for MapReduce processes using ASM. Those classes need to be generated
* instead of being part of source code is to avoid infinite recursion since we need classes to be of the same
* class names as the classes in MapReduce (e.g. MRAppMaster) in order to intercept the main() method call.
*/
public final class ContainerLauncherGenerator {
/**
* Generates a JAR file for launching MapReduce containers. The generated jar contains three classes inside
*
* <ul>
* <li>{@link org.apache.hadoop.mapreduce.v2.app.MRAppMaster}</li>
* <li>{@link org.apache.hadoop.mapred.YarnChild YarnChild}</li>
* <li>{@link MapReduceContainerLauncher}</li>
* </ul>
*
* @see MapReduceContainerLauncher
*/
public static void generateLauncherJar(String launcherClassPath, String classLoaderName,
OutputSupplier<? extends OutputStream> outputSupplier) throws IOException {
try (JarOutputStream output = new JarOutputStream(outputSupplier.getOutput())) {
generateLauncherClass(launcherClassPath, classLoaderName,
"org.apache.hadoop.mapreduce.v2.app.MRAppMaster", output);
generateLauncherClass(launcherClassPath, classLoaderName,
"org.apache.hadoop.mapred.YarnChild", output);
// Includes the launcher class in the JAR as well. No need to trace dependency as the launcher
// class must be dependency free.
String containerLauncherName = Type.getInternalName(MapReduceContainerLauncher.class) + ".class";
output.putNextEntry(new JarEntry(containerLauncherName));
URL launcherURL = ContainerLauncherGenerator.class.getClassLoader().getResource(containerLauncherName);
// Can never be null
Preconditions.checkState(launcherURL != null);
Resources.copy(launcherURL, output);
}
}
/**
* Generates a JAR file that contains a class with a static main method.
*
* @param mainClassName Name of the generated class
* @param mainDelegatorClass the actual class that the main method will delegate to
* @param outputSupplier the {@link OutputSupplier} for the jar file
*/
public static void generateLauncherJar(String mainClassName, Class<?> mainDelegatorClass,
OutputSupplier<? extends OutputStream> outputSupplier) throws IOException {
try (JarOutputStream output = new JarOutputStream(outputSupplier.getOutput())) {
generateMainClass(mainClassName, Type.getType(mainDelegatorClass), output);
}
}
/**
* Generates the bytecode for a main class and writes to the given {@link JarOutputStream}.
* The generated class looks like this:
*
* <pre>{@code
* class className {
* public static void main(String[] args) {
* MapReduceContainerLauncher.launch(launcherClassPath, classLoaderName, className, args);
* }
* }
* }
* </pre>
*
* The {@code launcherClassPath}, {@code classLoaderName} and {@code className} are represented as
* string literals in the generated class.
*/
private static void generateLauncherClass(String launcherClassPath, String classLoaderName,
String className, JarOutputStream output) throws IOException {
String internalName = className.replace('.', '/');
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER,
internalName, null, Type.getInternalName(Object.class), null);
Method constructor = Methods.getMethod(void.class, "<init>");
// Constructor
// MRAppMaster()
GeneratorAdapter mg = new GeneratorAdapter(Opcodes.ACC_PUBLIC, constructor, null, null, classWriter);
mg.loadThis();
mg.invokeConstructor(Type.getType(Object.class), constructor);
mg.returnValue();
mg.endMethod();
// Main method.
// public static void main(String[] args) {
// MapReduceContainerLauncher.launch(launcherClassPath, classLoaderName, className, args);
// }
Method mainMethod = Methods.getMethod(void.class, "main", String[].class);
mg = new GeneratorAdapter(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, mainMethod, null,
new Type[] { Type.getType(Exception.class) }, classWriter);
mg.getStatic(Type.getType(System.class), "out", Type.getType(PrintStream.class));
mg.visitLdcInsn("Launch class " + className);
mg.invokeVirtual(Type.getType(PrintStream.class), Methods.getMethod(void.class, "println", String.class));
// The Launcher classpath, classloader name and main classname are stored as string literal in the generated class
mg.visitLdcInsn(launcherClassPath);
mg.visitLdcInsn(classLoaderName);
mg.visitLdcInsn(className);
mg.loadArg(0);
mg.invokeStatic(Type.getType(MapReduceContainerLauncher.class),
Methods.getMethod(void.class, "launch", String.class, String.class, String.class, String[].class));
mg.returnValue();
mg.endMethod();
classWriter.visitEnd();
output.putNextEntry(new JarEntry(internalName + ".class"));
output.write(classWriter.toByteArray());
}
/**
* Generates a class that has a static main method which delegates the call to a static method in the given delegator
* class with method signature {@code public static void launch(String className, String[] args)}
*
* @param className the classname of the generated class
* @param mainDelegator the class to delegate the main call to
* @param output for writing the generated bytes
*/
private static void generateMainClass(String className, Type mainDelegator,
JarOutputStream output) throws IOException {
String internalName = className.replace('.', '/');
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER,
internalName, null, Type.getInternalName(Object.class), null);
// Generate the default constructor, which just call super()
Method constructor = Methods.getMethod(void.class, "<init>");
GeneratorAdapter mg = new GeneratorAdapter(Opcodes.ACC_PUBLIC, constructor, null, null, classWriter);
mg.loadThis();
mg.invokeConstructor(Type.getType(Object.class), constructor);
mg.returnValue();
mg.endMethod();
// Generate the main method
// public static void main(String[] args) {
// System.out.println("Launch class .....");
// <MainDelegator>.launch(<className>, args);
// }
Method mainMethod = Methods.getMethod(void.class, "main", String[].class);
mg = new GeneratorAdapter(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, mainMethod, null,
new Type[] { Type.getType(Exception.class) }, classWriter);
mg.getStatic(Type.getType(System.class), "out", Type.getType(PrintStream.class));
mg.visitLdcInsn("Launch class " + className + " by calling " + mainDelegator.getClassName() + ".launch");
mg.invokeVirtual(Type.getType(PrintStream.class), Methods.getMethod(void.class, "println", String.class));
// The main classname is stored as string literal in the generated class
mg.visitLdcInsn(className);
mg.loadArg(0);
mg.invokeStatic(mainDelegator, Methods.getMethod(void.class, "launch", String.class, String[].class));
mg.returnValue();
mg.endMethod();
classWriter.visitEnd();
output.putNextEntry(new JarEntry(internalName + ".class"));
output.write(classWriter.toByteArray());
}
private ContainerLauncherGenerator() {
}
}