Java tutorial
/* * Advice Library * Copyright (C) 2012-2013 INESC-ID Software Engineering Group * http://www.esw.inesc-id.pt * * This file is part of the advice library. * * advice library is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * advice library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with advice library. If not, see <http://www.gnu.org/licenses/>. * * Author's contact: * INESC-ID Software Engineering Group * Rua Alves Redol 9 * 1000 - 029 Lisboa * Portugal */ package pt.ist.esw.advice; import static org.objectweb.asm.Opcodes.*; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.MethodNode; public class ProcessAnnotations { private final Type ADVICE = Type.getType(Advice.class); private final Type annotation; private final Type annotationInstance; private final Map<String, Object> defaultAnnotationElements; private final List<FieldNode> annotationFields; private final String annotationInstanceCtorDesc; private final ProgramArgs args; public ProcessAnnotations(ProgramArgs args) { this.args = args; annotation = Type.getType(args.annotationClass); annotationInstance = Type .getObjectType(GenerateAnnotationInstance.getAnnotationInstanceName(args.annotationClass)); Map<String, Object> annotationElements = new HashMap<String, Object>(); for (java.lang.reflect.Method element : args.annotationClass.getDeclaredMethods()) { if (element.getReturnType().isArray()) { throw new Error("FIXME: Annotations containing arrays are not yet supported"); } Object defaultValue = element.getDefaultValue(); if (defaultValue instanceof Class) { defaultValue = Type.getType((Class<?>) defaultValue); } annotationElements.put(element.getName(), defaultValue); } defaultAnnotationElements = Collections.unmodifiableMap(annotationElements); try { InputStream is = Thread.currentThread().getContextClassLoader() .getResourceAsStream(annotationInstance.getInternalName() + ".class"); ClassReader cr = new ClassReader(is); ClassNode cNode = new ClassNode(); cr.accept(cNode, 0); annotationFields = cNode.fields != null ? cNode.fields : Collections.<FieldNode>emptyList(); StringBuffer ctorDescriptor = new StringBuffer("("); for (FieldNode field : annotationFields) { ctorDescriptor.append(field.desc); } ctorDescriptor.append(")V"); annotationInstanceCtorDesc = ctorDescriptor.toString(); } catch (IOException e) { throw new RuntimeException( "Error opening " + annotationInstance + " class. Have you run GenerateAnnotationInstance?", e); } } public static void main(final String args[]) throws Exception { ProgramArgs progArgs = new ProgramArgs(args); ProcessAnnotations processor = new ProcessAnnotations(progArgs); processor.process(); } public void process() { for (File f : args.fileList) { processFile(f); } } protected void processFile(File file) { if (file.isDirectory()) { for (File subFile : file.listFiles()) { processFile(subFile); } } else { String fileName = file.getName(); if (fileName.toLowerCase().endsWith(".class")) { processClassFile(file); } } } protected void processClassFile(File classFile) { InputStream is = null; try { // get an input stream to read the bytecode of the class is = new FileInputStream(classFile); ClassReader cr = new ClassReader(is); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassVisitor cv = cw; // Add here other visitors to run AFTER the MethodTransformer cv = new MethodTransformer(cv, classFile); // Add here other visitors to run BEFORE the MethodTransformer cr.accept(cv, 0); writeClassFile(classFile, cw.toByteArray()); } catch (IOException e) { throw new RuntimeException("Error processing class file " + classFile.getPath(), e); } finally { if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } } protected static void writeClassFile(File classFile, byte[] bytecode) { FileOutputStream fos = null; try { fos = new FileOutputStream(classFile); fos.write(bytecode); } catch (IOException e) { throw new RuntimeException("Couldn't write class file" + classFile.getPath(), e); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } } private class MethodTransformer extends ClassVisitor { private final List<MethodNode> methods = new ArrayList<MethodNode>(); private final List<String> advisedMethodNames = new ArrayList<String>(); private final MethodNode advisedClInit; private final File classFile; private String className; public MethodTransformer(ClassVisitor cv, File originalClassFile) { super(ASM4, cv); classFile = originalClassFile; advisedClInit = new MethodNode(ACC_STATIC, "<clinit>", "()V", null, null); advisedClInit.visitCode(); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { className = name; cv.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { // Use a MethodNode to represent the method MethodNode mn = new MethodNode(access, name, desc, signature, exceptions); methods.add(mn); return mn; } @Override public void visitEnd() { MethodNode clInit = null; boolean isAnnotated = false; for (MethodNode mn : methods) { if (mn.name.equals("<clinit>")) { clInit = mn; continue; } for (AnnotationNode an : getAnnotations(mn)) { if (an.desc.equals(annotation.getDescriptor())) { //System.out.println("Method " + mn.name + " is tagged with annotation"); isAnnotated = true; // Create new advised method adviseMethod(mn, an); break; } } // Visit method, so it will be present on the output class mn.accept(cv); } if (isAnnotated) { // Insert <clinit> into class if (clInit != null) { // Merge existing clinit with our additions clInit.instructions.accept(advisedClInit); } else { advisedClInit.visitInsn(RETURN); } advisedClInit.visitMaxs(0, 0); advisedClInit.visitEnd(); advisedClInit.accept(cv); } else { // Preserve existing <clinit> if (clInit != null) { clInit.accept(cv); } } cv.visitEnd(); } /** * Returns the invisible or visible annotations list, depending on the RetentionPolicy of the client * annotation. **/ private List<AnnotationNode> getAnnotations(MethodNode mn) { Retention retAnnot = args.annotationClass.getAnnotation(Retention.class); RetentionPolicy policy = retAnnot == null ? RetentionPolicy.CLASS : retAnnot.value(); List<AnnotationNode> list = policy == RetentionPolicy.CLASS ? mn.invisibleAnnotations : mn.visibleAnnotations; return list != null ? list : Collections.<AnnotationNode>emptyList(); } /** * To advise method add annotated with @Annot, part of the class Xpto, and with signature * @Annot @SomethingElse public long add(Object o, int i) * we generate the following code: * * public static [final] Advice advice$add = ClientAdviceFactory.getInstance().newAdvice(annotation); * * @SomethingElse public long add(Object o, int i) { * static final class callable$add implements Callable { * Xpto arg0; * Object arg1; * int arg2; * * callable$add(Xpto arg0, Object arg1, int arg2) { * this.arg0 = arg0; * this.arg1 = arg1; * this.arg2 = arg2; * } * * public Object call() { * return Xpto.advised$add(arg0, arg1, arg2); * } * } * return advice$add.perform(new callable$add(this, o, i)); * } * * synthetic static long advised$add(Xpto this, Object o, int i) { * // original method * } * * Note that any annotations from the original method are removed from the advised$ version. **/ private void adviseMethod(MethodNode mn, AnnotationNode advisedAnnotation) { // Mangle name if there are multiple advised methods with the same name String methodName = getMethodName(mn.name); // Name for advice field String fieldName = "advice$" + methodName; // Name for callable class String callableClass = className + "$callable$" + methodName; // Generate new method which will invoke the advice with the Callable MethodVisitor advisedMethod = cv.visitMethod(mn.access, mn.name, mn.desc, mn.signature, mn.exceptions.toArray(new String[0])); // Remove advised annotation and copy other annotations from the original method to the newly created method getAnnotations(mn).remove(advisedAnnotation); copyAnnotations(mn, advisedMethod); // Create field to save advice cv.visitField(ACC_PUBLIC | ACC_STATIC | ACC_FINAL, fieldName, ADVICE.getDescriptor(), null, null); // Add code to clinit to initialize the field // Add default parameters from annotation Map<String, Object> annotationElements = new HashMap<String, Object>(defaultAnnotationElements); // Copy parameters from method annotation if (advisedAnnotation.values != null) { Iterator<Object> it = advisedAnnotation.values.iterator(); while (it.hasNext()) { // ASM stores annotation values as String1, Object1, String2, Object2, ... in the values list annotationElements.put((String) it.next(), it.next()); } } // Decide whether the annotation defines its own AdviceFactory and, if so, use that. Otherwise, use either the // factory specified in this program's execution parameters or the default factory. Type factoryType = (Type) annotationElements.get("adviceFactory"); if (factoryType == null) { factoryType = Type.getObjectType( (args.annotationFactoryClass != null ? args.annotationFactoryClass.getCanonicalName() : AdviceFactory.DEFAULT_ADVICE_FACTORY).replace('.', '/')); } advisedClInit.visitMethodInsn(INVOKESTATIC, factoryType.getInternalName(), "getInstance", "()" + Type.getType(AdviceFactory.class).getDescriptor()); // Push annotation parameters on the stack and create AnnotationInstance advisedClInit.visitTypeInsn(NEW, annotationInstance.getInternalName()); advisedClInit.visitInsn(DUP); for (FieldNode field : annotationFields) { // Support for enums if (fieldIsEnum(field)) { // ASM supplies enums as String[], while the defaults read by reflection are Enum instances Object value = annotationElements.get(field.name); Enum<?> enumValue = value instanceof String[] ? getEnumElement((String[]) value) : (Enum<?>) value; Type enumType = Type.getType(enumValue.getClass()); advisedClInit.visitFieldInsn(GETSTATIC, enumType.getInternalName(), enumValue.name(), enumType.getDescriptor()); } else { advisedClInit.visitLdcInsn(annotationElements.get(field.name)); } } advisedClInit.visitMethodInsn(INVOKESPECIAL, annotationInstance.getInternalName(), "<init>", annotationInstanceCtorDesc); // Obtain advice for this method advisedClInit.visitMethodInsn(INVOKEVIRTUAL, Type.getType(AdviceFactory.class).getInternalName(), "newAdvice", "(" + Type.getType(Annotation.class).getDescriptor() + ")" + ADVICE.getDescriptor()); advisedClInit.visitFieldInsn(PUTSTATIC, className, fieldName, ADVICE.getDescriptor()); // Repurpose original method modifyOriginalMethod(mn); // Generate replacement method generateMethodCode(mn, advisedMethod, fieldName, callableClass); // Generate callable class generateCallable(callableClass, mn); } private void copyAnnotations(MethodNode mn, MethodVisitor advisedMethod) { // InvisibleAnnotations if (mn.invisibleAnnotations != null) { for (AnnotationNode an : mn.invisibleAnnotations) { an.accept(advisedMethod.visitAnnotation(an.desc, false)); } } // VisibleAnnotations if (mn.visibleAnnotations != null) { for (AnnotationNode an : mn.visibleAnnotations) { an.accept(advisedMethod.visitAnnotation(an.desc, true)); } } // InvisibleParameterAnnotations if (mn.invisibleParameterAnnotations != null) { for (int i = 0; i < mn.invisibleParameterAnnotations.length; i++) { if (mn.invisibleParameterAnnotations[i] != null) { for (AnnotationNode an : mn.invisibleParameterAnnotations[i]) { an.accept(advisedMethod.visitParameterAnnotation(i, an.desc, false)); } } } } // VisibleParameterAnnotations if (mn.visibleParameterAnnotations != null) { for (int i = 0; i < mn.visibleParameterAnnotations.length; i++) { if (mn.visibleParameterAnnotations[i] != null) { for (AnnotationNode an : mn.visibleParameterAnnotations[i]) { an.accept(advisedMethod.visitParameterAnnotation(i, an.desc, true)); } } } } } private void modifyOriginalMethod(MethodNode mn) { // Rename original method mn.name = "advised$" + mn.name; // Remove annotations from original method mn.invisibleAnnotations = Collections.<AnnotationNode>emptyList(); mn.visibleAnnotations = Collections.<AnnotationNode>emptyList(); // Modify the access flags, setting the method as package protected, so that the callable can access it mn.access &= ~ACC_PRIVATE & ~ACC_PUBLIC; // Also mark it as synthetic, so Java tools ignore it mn.access |= ACC_SYNTHETIC; // Check for, and clear any attributes seen if (mn.attrs != null) { System.err.println("WARNING: Modified method " + mn.name + " has non-standard attributes"); } // Clear parameter annotations mn.visibleParameterAnnotations = null; mn.invisibleParameterAnnotations = null; if (!isStatic(mn)) { // Convert original method to static method with instance as first argument // Note that the bytecode is still valid, as ALOAD 0 (an access to this) will still have // the same semantics mn.access |= ACC_STATIC; mn.desc = "(L" + className + ";" + mn.desc.substring(1); } } private void generateMethodCode(MethodNode mn, MethodVisitor mv, String fieldName, String callableClass) { mv.visitCode(); mv.visitFieldInsn(GETSTATIC, className, fieldName, ADVICE.getDescriptor()); mv.visitTypeInsn(NEW, callableClass); mv.visitInsn(DUP); int pos = 0; // Push arguments for original method on the stack for (Type t : Type.getArgumentTypes(mn.desc)) { mv.visitVarInsn(t.getOpcode(ILOAD), pos); pos += t.getSize(); } mv.visitMethodInsn(INVOKESPECIAL, callableClass, "<init>", getCallableCtorDesc(mn)); mv.visitMethodInsn(INVOKEINTERFACE, ADVICE.getInternalName(), "perform", "(Ljava/util/concurrent/Callable;)Ljava/lang/Object;"); // Return value Type returnType = Type.getReturnType(mn.desc); if (returnType.getSort() == Type.OBJECT || returnType.getSort() == Type.ARRAY) { mv.visitTypeInsn(CHECKCAST, returnType.getInternalName()); } else if (isPrimitive(returnType)) { // Return is native, we have to unbox the value from the Advice boxUnwrap(returnType, mv); } mv.visitInsn(returnType.getOpcode(IRETURN)); mv.visitMaxs(0, 0); mv.visitEnd(); } private boolean isStatic(MethodNode mn) { return (mn.access & ACC_STATIC) > 0; } private String getCallableCtorDesc(MethodNode mn) { return mn.desc.substring(0, mn.desc.indexOf(')') + 1) + 'V'; } private String getMethodName(String methodName) { // Count number of advised methods with same name int count = 0; for (String name : advisedMethodNames) { if (name.equals(methodName)) { count++; } } // Add another one advisedMethodNames.add(methodName); return methodName + (count > 0 ? "$" + count : ""); } private void generateCallable(String callableClass, MethodNode mn) { Type returnType = Type.getReturnType(mn.desc); Type[] arguments = Type.getArgumentTypes(mn.desc); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); cw.visit(V1_6, ACC_FINAL, callableClass, "Ljava/lang/Object;Ljava/util/concurrent/Callable<" + (isPrimitive(returnType) ? toObject(returnType) : (returnType.equals(Type.VOID_TYPE) ? Type.getObjectType("java/lang/Void") : returnType)).getDescriptor() + ">;", "java/lang/Object", new String[] { "java/util/concurrent/Callable" }); cw.visitSource("Advice Library Automatically Generated Class", null); // Create fields to hold arguments { int fieldPos = 0; for (Type t : arguments) { cw.visitField(ACC_PRIVATE | ACC_FINAL, "arg" + (fieldPos++), t.getDescriptor(), null, null); } } // Create constructor { MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", getCallableCtorDesc(mn), null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V"); int localsPos = 0; int fieldPos = 0; for (Type t : arguments) { mv.visitVarInsn(ALOAD, 0); mv.visitVarInsn(t.getOpcode(ILOAD), localsPos + 1); mv.visitFieldInsn(PUTFIELD, callableClass, "arg" + fieldPos++, t.getDescriptor()); localsPos += t.getSize(); } mv.visitInsn(RETURN); mv.visitMaxs(0, 0); mv.visitEnd(); } // Create call method { MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "call", "()Ljava/lang/Object;", null, null); mv.visitCode(); int fieldPos = 0; for (Type t : arguments) { mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, callableClass, "arg" + fieldPos++, t.getDescriptor()); } mv.visitMethodInsn(INVOKESTATIC, className, mn.name, mn.desc); if (returnType.equals(Type.VOID_TYPE)) { mv.visitInsn(ACONST_NULL); } else if (isPrimitive(returnType)) { boxWrap(returnType, mv); } mv.visitInsn(ARETURN); mv.visitMaxs(0, 0); mv.visitEnd(); } // Write the callable class file in the same directory (package) as the original class file String callableFileName = callableClass.substring(Math.max(callableClass.lastIndexOf('/'), 0)) + ".class"; writeClassFile(new File(classFile.getParent() + File.separatorChar + callableFileName), cw.toByteArray()); } private final Object[][] primitiveWrappers = new Object[][] { { "java/lang/Boolean", Type.BOOLEAN_TYPE }, { "java/lang/Byte", Type.BYTE_TYPE }, { "java/lang/Character", Type.CHAR_TYPE }, { "java/lang/Short", Type.SHORT_TYPE }, { "java/lang/Integer", Type.INT_TYPE }, { "java/lang/Long", Type.LONG_TYPE }, { "java/lang/Float", Type.FLOAT_TYPE }, { "java/lang/Double", Type.DOUBLE_TYPE } }; private Type toObject(Type primitiveType) { for (Object[] map : primitiveWrappers) { if (primitiveType.equals(map[1])) { return Type.getObjectType((String) map[0]); } } throw new AssertionError(); } private boolean isPrimitive(Type type) { int sort = type.getSort(); return sort != Type.VOID && sort != Type.ARRAY && sort != Type.OBJECT && sort != Type.METHOD; } private void boxWrap(Type primitiveType, MethodVisitor mv) { Type objectType = toObject(primitiveType); mv.visitMethodInsn(INVOKESTATIC, objectType.getInternalName(), "valueOf", "(" + primitiveType.getDescriptor() + ")" + objectType.getDescriptor()); } private void boxUnwrap(Type primitiveType, MethodVisitor mv) { Type objectType = toObject(primitiveType); mv.visitTypeInsn(CHECKCAST, objectType.getInternalName()); mv.visitMethodInsn(INVOKEVIRTUAL, objectType.getInternalName(), primitiveType.getClassName() + "Value", "()" + primitiveType.getDescriptor()); } private boolean fieldIsEnum(FieldNode field) { return field.desc.charAt(0) == 'L' && !field.desc.equals("Ljava/lang/Object;") && !field.desc.equals("Ljava/lang/String;") && !field.desc.equals("Ljava/lang/Class;"); } @SuppressWarnings({ "unchecked", "rawtypes" }) private Enum<?> getEnumElement(String[] enumInfo) { try { Class<? extends Enum> enumClass = Class .forName(enumInfo[0].substring(1, enumInfo[0].length() - 1).replace('/', '.')) .asSubclass(Enum.class); return Enum.valueOf(enumClass, enumInfo[1]); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } } // smf: Shamelessly adapted from CompilerArgs in Fenix Framework's DML compiler public static class ProgramArgs { Class<? extends Annotation> annotationClass; Class<? extends AdviceFactory> annotationFactoryClass; List<File> fileList = new ArrayList<File>(); public ProgramArgs(Class<? extends Annotation> annotationClass, Class<? extends AdviceFactory> annotationFactoryClass) { this.annotationClass = annotationClass; this.annotationFactoryClass = annotationFactoryClass; } public ProgramArgs(Class<? extends Annotation> annotationClass, Class<? extends AdviceFactory> annotationFactoryClass, File file) { this(annotationClass, annotationFactoryClass); this.fileList.add(file); } public ProgramArgs(Class<? extends Annotation> annotationClass, Class<? extends AdviceFactory> annotationFactoryClass, List<File> fileList) { this(annotationClass, annotationFactoryClass); this.fileList.addAll(fileList); } public ProgramArgs(String[] args) throws Exception { if (args.length < 3) { error("wrong syntax"); } processCommandLineArgs(args); checkArguments(); } void checkArguments() { if (annotationClass == null) { error("annotation class is not specified"); } if (annotationFactoryClass == null) { message("no factory class specified: using defaults"); } if (fileList.isEmpty()) { error("no class files or dirs specified"); } } void processCommandLineArgs(String[] args) throws Exception { int num = 0; while (num < args.length) { num = processOption(args, num); } } int processOption(String[] args, int pos) throws Exception { if (args[pos].equals("-a")) { annotationClass = Class.forName(getNextArgument(args, pos)).asSubclass(Annotation.class); return pos + 2; } else if (args[pos].equals("-f")) { annotationFactoryClass = Class.forName(getNextArgument(args, pos)).asSubclass(AdviceFactory.class); return pos + 2; } else { fileList.add(new File(args[pos])); return pos + 1; } } String getNextArgument(String[] args, int pos) { int nextPos = pos + 1; if (nextPos < args.length) { return args[nextPos]; } else { error("option " + args[pos] + " requires argument"); } return null; } void error(String msg) { System.err.println("ProcessAnnotations: " + msg); System.err.println( "Syntax: ProcessAnnotations -a <annotation-class> [-f <advice-factory-class>] [class files or dirs]"); System.exit(1); } void message(String msg) { System.out.println("ProcessAnnotations: " + msg); } } }