Java tutorial
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. */ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.giraph.debugger.instrumenter; import java.io.IOException; import java.lang.reflect.Modifier; import java.nio.file.Files; import java.util.Collection; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.NotFoundException; import javassist.bytecode.ConstPool; import javassist.bytecode.Descriptor; import org.apache.giraph.graph.Computation; import org.apache.giraph.master.MasterCompute; import org.apache.log4j.Logger; import com.google.common.collect.Lists; import com.google.common.collect.Sets; /** * The main class that instruments user's ordinary Giraph Computation class with * necessary changes for debugging. */ public class InstrumentGiraphClasses { /** * Logger for this class. */ private static Logger LOG = Logger.getLogger(InstrumentGiraphClasses.class); /** * Suffix to append to the original class names. */ private static final String ORIGINAL_CLASS_NAME_SUFFIX = System.getProperty("giraph.debugger.classNameSuffix", "Original"); /** * Path prefix for determining the temporary directory name used for * instrumentation. */ private static final String TMP_DIR_NAME_PREFIX = InstrumentGiraphClasses.class.getSimpleName(); /** * Disallowing instantiation. */ private InstrumentGiraphClasses() { } /** * Main entry point for instrumenting Giraph Computation and MasterCompute * classes with Graft classes. * * @param args Command-line arguments. * @throws IOException * @throws ClassNotFoundException */ public static void main(String[] args) throws IOException, ClassNotFoundException { // CHECKSTYLE: stop Regexp if (args.length < 1) { System.err.println( "Usage: java ... " + TMP_DIR_NAME_PREFIX + " GIRAPH_COMPUTATION_CLASS_NAME [OUTPUT_DIR]"); System.exit(1); } Collection<String> userComputationClassNames = Sets.newHashSet(args[0]); String outputDir = args.length > 1 ? args[1] : null; String masterComputeClassName = args.length > 2 ? args[2] : null; // Additional Computation classes boolean shouldAnalyzeMaster = masterComputeClassName != null; for (int i = 3; i < args.length; i++) { userComputationClassNames.add(args[i]); // Don't analyze the MasterCompute class when a chosen list of // Computation classes were given shouldAnalyzeMaster = false; } try { Collection<CtClass> classesModified = Sets.newHashSet(); ClassPool classPool = ClassPool.getDefault(); if (masterComputeClassName != null) { if (shouldAnalyzeMaster) { // Collect all Computation class names referenced in // masterComputeClassName LOG.info("Analyzing MasterCompute class: " + masterComputeClassName); userComputationClassNames .addAll(collectComputationClassNames(masterComputeClassName, classPool)); } LOG.info("Instrumenting MasterCompute class: " + masterComputeClassName); classesModified.addAll(instrumentSandwich(masterComputeClassName, AbstractInterceptingMasterCompute.class.getName(), UserMasterCompute.class.getName(), BottomInterceptingMasterCompute.class.getName(), classPool)); } for (String userComputationClassName : userComputationClassNames) { LOG.info("Instrumenting Computation class: " + userComputationClassName); classesModified.addAll(instrumentSandwich(userComputationClassName, AbstractInterceptingComputation.class.getName(), UserComputation.class.getCanonicalName(), BottomInterceptingComputation.class.getName(), classPool)); } // Finally, write the modified classes so that a new jar can be // created or an existing one can be updated. String jarRoot = outputDir != null ? outputDir : Files.createTempDirectory(TMP_DIR_NAME_PREFIX).toString(); LOG.info("Writing " + classesModified.size() + " instrumented classes to " + jarRoot); for (CtClass c : classesModified) { LOG.debug(" writing class " + c.getName()); c.writeFile(jarRoot); } LOG.info("Finished instrumentation"); if (outputDir == null) { // Show where we produced the instrumented .class files (unless // specified) System.out.println(jarRoot); } System.exit(0); } catch (NotFoundException e) { e.printStackTrace(); System.err.println("Some Giraph Computation or MasterCompute classes " + "were not found"); System.exit(1); } catch (CannotCompileException e) { e.printStackTrace(); System.err.println("Cannot instrument the given Giraph Computation or " + "MasterCompute classes"); System.exit(2); } catch (IOException e) { e.printStackTrace(); System.err .println("Cannot write the instrumented Giraph Computation and/or " + "MasterCompute classes"); System.exit(4); } // CHECKSTYLE: resume Regexp } /** * Finds all class names referenced in the master compute class. * * @param masterComputeClassName The name of the master compute class. * @param classPool The Javassist class pool being used. * @return A collection of class names. * @throws NotFoundException */ protected static Collection<String> collectComputationClassNames(String masterComputeClassName, ClassPool classPool) throws NotFoundException { Collection<String> classNames = Lists.newArrayList(); CtClass computationClass = classPool.get(Computation.class.getName()); CtClass rootMasterComputeClass = classPool.get(MasterCompute.class.getName()); CtClass mc = classPool.get(masterComputeClassName); while (mc != null && !mc.equals(rootMasterComputeClass)) { // find all class names appearing in the master compute class @SuppressWarnings("unchecked") Collection<String> classNamesInMasterCompute = Lists.newArrayList(mc.getRefClasses()); // as well as in string literals ConstPool constPool = mc.getClassFile().getConstPool(); for (int i = 1; i < constPool.getSize(); i++) { switch (constPool.getTag(i)) { case ConstPool.CONST_String: classNamesInMasterCompute.add(constPool.getStringInfo(i)); break; default: break; } } // collect Computation class names for (String className : classNamesInMasterCompute) { // CHECKSTYLE: stop IllegalCatch try { if (classPool.get(className).subtypeOf(computationClass)) { classNames.add(className); } } catch (NotFoundException e) { // ignored assert true; } catch (Exception e) { e.printStackTrace(); } // CHECKSTYLE: resume IllegalCatch } // repeat above for all superclasses of given masterComputeClass mc = mc.getSuperclass(); } return classNames; } /** * Instruments the given class with a bottom and top class (sandwich), * rewiring the inheritance chain and references. * * @param targetClassName * The class to be placed in the middle of the sandwich * instrumentation. * @param topClassName * The class to be placed as a base class (on top) of the target * class (middle), which intercepts any calls made from the target * class. * @param mockTargetClassName * The name of the mock class used to compile the top class. * @param bottomClassName * The class to be placed as a extending class (beneath) of the * target class, which will receive any calls to the target class * first. * @param classPool * The Javassist class pool being used. * @return A collection of instrumented Javassist CtClasses. * @throws NotFoundException * @throws CannotCompileException * @throws ClassNotFoundException */ protected static Collection<CtClass> instrumentSandwich(String targetClassName, String topClassName, String mockTargetClassName, String bottomClassName, ClassPool classPool) throws NotFoundException, CannotCompileException, ClassNotFoundException { Collection<CtClass> classesModified = Sets.newHashSet(); // Load the involved classes with Javassist LOG.debug("Looking for classes to instrument: " + targetClassName + "..."); String alternativeClassName = targetClassName + ORIGINAL_CLASS_NAME_SUFFIX; CtClass targetClass = classPool.getAndRename(targetClassName, alternativeClassName); // We need two classes: one at the bottom (subclass) and // another at the top (superclass). CtClass topClass = classPool.get(topClassName); CtClass bottomClass = classPool.getAndRename(bottomClassName, targetClassName); LOG.debug(" target class to instrument (targetClass):\n" + getGenericsName(targetClass)); LOG.debug(" class to instrument at top (topClass):\n" + getGenericsName(topClass)); LOG.debug(" class to instrument at bottom (bottomClass):\n" + getGenericsName(bottomClass)); // 1. To intercept other Giraph API calls by user's computation // class: // We need to inject a superclass at the highest level of the // inheritance hierarchy, i.e., make the top class become a // superclass of the class that directly extends // AbstractComputation. // 1-a. Find the user's base class that extends the top class' // superclass. LOG.debug("Looking for user's top class that extends " + getGenericsName(topClass.getSuperclass())); CtClass targetTopClass = targetClass; while (!targetTopClass.getName().equals(Object.class.getName()) && !targetTopClass.getSuperclass().equals(topClass.getSuperclass())) { targetTopClass = targetTopClass.getSuperclass(); } if (targetTopClass.getName().equals(Object.class.getName())) { throw new NotFoundException( targetClass.getName() + " must extend " + topClass.getSuperclass().getName()); } LOG.debug(" class to inject topClass on top of (targetTopClass):\n" + getGenericsName(targetTopClass)); // 1-b. Mark user's class as abstract and erase any final modifier. LOG.debug("Marking targetClass as abstract and non-final..."); int modClass = targetClass.getModifiers(); modClass |= Modifier.ABSTRACT; modClass &= ~Modifier.FINAL; targetClass.setModifiers(modClass); classesModified.add(targetClass); if (!targetTopClass.equals(topClass)) { // 1-c. Inject the top class by setting it as the superclass of // user's class that extends its superclass (AbstractComputation). LOG.debug("Injecting topClass on top of targetTopClass..."); targetTopClass.setSuperclass(topClass); targetTopClass.replaceClassName(topClass.getSuperclass().getName(), topClass.getName()); classesModified.add(targetTopClass); // XXX Unless we take care of generic signature as well, // GiraphConfigurationValidator will complain. String jvmNameForTopClassSuperclass = Descriptor.of(topClass.getSuperclass()).replaceAll(";$", ""); String jvmNameForTopClass = Descriptor.of(topClass).replaceAll(";$", ""); String genSig = targetTopClass.getGenericSignature(); if (genSig != null) { String genSig2 = genSig.replace(jvmNameForTopClassSuperclass, jvmNameForTopClass); targetTopClass.setGenericSignature(genSig2); } } // 1-d. Then, make the bottomClass extend user's computation, taking // care of generics signature as well. LOG.debug("Attaching bottomClass beneath targetClass..."); bottomClass.replaceClassName(mockTargetClassName, targetClass.getName()); bottomClass.setSuperclass(targetClass); bottomClass.setGenericSignature(Descriptor.of(targetClass)); classesModified.add(bottomClass); // 2. To intercept compute() and other calls that originate from // Giraph: // We need to extend user's computation class pass that as the // computation class to Giraph. The new subclass will override // compute(), so we may have to remove the "final" marker on user's // compute() method. // 2-a. Find all methods that we override in bottomClass. LOG.debug("For each method to intercept," + " changing Generics signature of bottomClass, and" + " erasing final modifier..."); for (CtMethod overridingMethod : bottomClass.getMethods()) { if (!overridingMethod.hasAnnotation(Intercept.class)) { continue; } Intercept annotation = (Intercept) overridingMethod.getAnnotation(Intercept.class); String targetMethodName = annotation.renameTo(); if (targetMethodName == null || targetMethodName.isEmpty()) { targetMethodName = overridingMethod.getName(); } // 2-b. Copy generics signature to the overriding method if // necessary. CtMethod targetMethod = targetClass.getMethod(targetMethodName, overridingMethod.getSignature()); LOG.debug(" from: " + overridingMethod.getName() + overridingMethod.getGenericSignature()); LOG.debug(" to: " + targetMethod.getName() + targetMethod.getGenericSignature()); if (overridingMethod.getGenericSignature() != null) { overridingMethod.setGenericSignature(targetMethod.getGenericSignature()); classesModified.add(overridingMethod.getDeclaringClass()); } // 2-c. Remove final marks from them. int modMethod = targetMethod.getModifiers(); if ((modMethod & Modifier.FINAL) == 0) { continue; } modMethod &= ~Modifier.FINAL; targetMethod.setModifiers(modMethod); LOG.debug(" erasing final modifier from " + targetMethod.getName() + "() of " + targetMethod.getDeclaringClass()); // 2-d. Rename the overriding method if necessary. if (!overridingMethod.getName().equals(targetMethodName)) { overridingMethod.setName(targetMethodName); classesModified.add(overridingMethod.getDeclaringClass()); } // 2-e. Remember them for later. classesModified.add(targetMethod.getDeclaringClass()); } LOG.debug("Finished instrumenting " + targetClassName); LOG.debug(" topClass=\n" + getGenericsName(topClass) + "\n" + topClass); LOG.debug(" targetTopClass=\n" + getGenericsName(targetTopClass) + "\n" + targetTopClass); LOG.debug(" targetClass=\n" + getGenericsName(targetClass) + "\n" + targetClass); LOG.debug(" bottomClass=\n" + getGenericsName(bottomClass) + "\n" + bottomClass); return classesModified; } /** * Format Javassist class name for log messages. * @param clazz The Javassist class. * @return Formatted name of the class including Generic type signature. */ protected static String getGenericsName(CtClass clazz) { return clazz.getName() + " (" + clazz.getGenericSignature() + ")"; } }