AOP的关键在于拦截,如果在代码中直接写入要插入的代码则是最直接的AOP。这当然不是指在source中生写代码,而是希望在程序员不知觉的情况下修改了代码。
asm是个开源包,可以很方便地读写class的bytecode。网站是http://asm.ow2.org/。为了方便修改类建议下载Eclipse插件。
使用方法挺简单。首先实现一个ClassAdapter导出类,找到要修改的函数:
public class SOClassAdapter extends ClassAdapter { public SOClassAdapter(ClassVisitor cv) { super(cv); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { if (shouldModify(name, parasFieldName(name))) { MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); return new ModifyMethodAdapter(mv); } return super.visitMethod(access, name, desc, signature, exceptions); }
略。
在visitMethod中找到要修改的函数后,通过实现一个MethodAdapter的导出类修改函数,例子如下:
public class ModifyMethodAdapter extends MethodAdapter { public ModifyMethodAdapter(MethodVisitor mv) { super(mv); } @Override public void visitInsn(int opcode) { if (opcode == Opcodes.RETURN) { visitVarInsn(Opcodes.ALOAD, 0); // visitVarInsn(Opcodes.ALOAD, 1); visitMethodInsn(Opcodes.INVOKESTATIC, "asm/TopClass", "print", "(Lasm/TopClass;)V");// (Lasm/Bean;)V } super.visitInsn(opcode); } }
最后通过ClassReader读入类,ClassAdapter访问并修改类的字节码,通过ClassWriter回写类:
ClassReader cr = new ClassReader("asm/Bean");//这是通过系统的ClassLoader加载类 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassAdapter claAdapter = new SOClassAdapter(cw); cr.accept(claAdapter, ClassReader.SKIP_DEBUG); byte[] data = cw.toByteArray();//得到修改后的字节码
可以将修改后的类回写到class文件中:
String path = c.getResource(c.getSimpleName() + ".class").getPath(); File file = new File(path); FileOutputStream fout = new FileOutputStream(file); fout.write(data); fout.close();
到此我们已经知道如何去修改类了,但是怎么用呢,这个问题折腾了我好几天。最终确定了两个较简单的方案:
方法一:编译后重写class文件
可以写一个main方法,寻找要修改的类,然后用ClassAdapter重定义类,得到byte[],再用FileOutputStream把字节流写入文件。这个操作完成之后剩下的就是如何把编译、改类、打包、运行一系列流程整合起来。我们用的编译工具是maven:
在pom文件里加一个插件:
编译打包时执行命令:“clean compile exec:java package”即可。<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.1</version> <executions> <execution> <phase>main</phase> <goals> <goal>java</goal> </goals> </execution> </executions> <configuration> <mainClass>asm.Redefine</mainClass> <argument>-classpath</argument> <classpath /> </configuration> </plugin>
方法二:运行时在内存中重定义类
这种方法需要用到java5的Instrumentation。首先写一个premain方法:
public class PreMain { public static Instrumentation inst; public static void premain(String agentArgs, Instrumentation inst) { PreMain.inst = inst; System.out.println("获取inst----" + inst); } }将Instrumentation实例保存到static变量中。然后将PreMain类打成jar包(如agent.jar),在MANIFEST.MF中添加一下内容:
Manifest-Version: 1.0 Class-Path: . Premain-Class: asm.PreMain Can-Redefine-Classes: true
在虚拟机启动时添加参数:-javaagent:路径/agent.jar。tomcat是在catalina.bat/catalina.sh中加上一句:“SET JAVA_OPTS=%JAVA_OPTS% -javaagent:%CATALINA_HOME%/lib/agent.jar”
然后在自己的类中通过反射获取Instrumentation实例,在用它的redefineClasses重定义类:Class<?> premainClass = Class .forName("asm.PreMain"); Field field = premainClass.getField("inst"); Instrumentation inst = (Instrumentation) field.get(premainClass .newInstance()); log.info("从外部获取的Instrumentation(如果为null,则不会重定义类)----" + inst); if (null == inst) return; List<ClassDefinition> definitions = redefineClass(); inst.redefineClasses(definitions .toArray(new ClassDefinition[definitions.size()]));这两种方法适用于不同的场合,可酌情选择使用。
有一点需要注意:ClassReader读取类的时候,如果用new ClassReader(“类名”)则是从系统ClassLoader中读类,这样很可能找不到类
。Tomcat的是WebappClassLoader。不要紧,ClassReader还提供了一个构造函数:public ClassReader(final InputStream is) throws IOException。可以将读好的类的字节流传进来。获取字节流的方式可以用或者clazz.getClassLoader.getResourceAsStream。clazz.getResourceAsStream(clazz.getSimpleName() + ".class")