我们知道agent技术可以在不入侵项目源代码的情况下来在项目初始化之前执行一些操作,而ASM技术可以直接修改字节码,两个结合起来就可以实现很多有意思的功能,比如监控所有方法的执行时间,代码如下。
1、代理类AgentTest
public class AgentTest {/*** 在主程序运行之前执行* @param agentArgs* @param isnt*/public static void premain(String agentArgs,Instrumentation inst) {// System.out.println("premain start");// System.out.println(agentArgs);inst.addTransformer(new AopAgentTransformer());}/*** 在主程序运行之后执行* @param args* @param inst*//* public static void agentmain(String args,Instrumentation inst) {System.out.println("loadagent after main run.args="+args);Class<?>[] classes = inst.getAllLoadedClasses();for (Class<?> cls : classes) {System.out.println(cls.getName());}System.out.println("agent run completely");}*/}
这里用的是在程序运行之前执行,因为我们要修改字节码,那么肯定要在主程序运行之前执行,在premain方法中,可以获得一个Instrumentation对象,我们可以向其中加入一个ClassFileTransformer对象,Transformer对象的实现如下所示:
2、AopAgentTransformer
public class AopAgentTransformer implements ClassFileTransformer{public byte[] transform(ClassLoader loader, String className,Class<?> classBeingRedefined, ProtectionDomain protectionDomain,byte[] classfileBuffer) throws IllegalClassFormatException {System.out.println("Transforming类" + className+"的字节码开始");/*TODO修改字节码,并返回修改后的字节码*/byte[] transformed = null;try {ClassReader cr = new ClassReader(new java.io.ByteArrayInputStream(classfileBuffer));ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);//ClassAdapter ca = new ClassAdapter(cw);TimeCountAdpter ca = new TimeCountAdpter(cw);cr.accept(ca, ClassReader.EXPAND_FRAMES);transformed = cw.toByteArray();}catch (RuntimeException re){re.printStackTrace();}catch (IOException e) {System.err.println("can't transform "+ className+" "+e);e.printStackTrace();}return transformed;}}
JVM会调用transform方法,这样就可以在JVM加载字节码文件时,获取到字节码并进行修改。
3、ASM修改字节码TimeCountAdpter
/*** 监控所有方法的执行时间* @author forever**/public class TimeCountAdpter extends ClassVisitor implements Opcodes {private String owner;private boolean isInterface;private String filedName = "UDASMCN";private int acc = Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC + Opcodes.ACC_FINAL;private boolean isPresent = false;private String methodName;public TimeCountAdpter(ClassVisitor classVisitor) {super(ASM6, classVisitor);}@Overridepublic void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {super.visit(version, access, name, signature, superName, interfaces);owner = name;isInterface = (access & ACC_INTERFACE) != 0;}@Overridepublic FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {if (name.equals(filedName)) {isPresent = true;}return super.visitField(access, name, descriptor, signature, value);}@Overridepublic MethodVisitor visitMethod(int access, String name, String descriptor, String signature,String[] exceptions) {MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);if (!isInterface && mv != null && !name.equals("<init>") && !name.equals("<clinit>")) {methodName = name;AddTimerMethodAdapter at = new AddTimerMethodAdapter(mv);at.aa = new AnalyzerAdapter(owner, access, name, descriptor, at);at.lvs = new LocalVariablesSorter(access, descriptor, at.aa);return at.lvs;}return mv;}public void visitEnd() {if (!isInterface) {FieldVisitor fv = cv.visitField(acc, filedName, "Ljava/lang/String;", null, owner);if (fv != null) {fv.visitEnd();}}cv.visitEnd();}class AddTimerMethodAdapter extends MethodVisitor {private int time;private int maxStack;public LocalVariablesSorter lvs;public AnalyzerAdapter aa;public AddTimerMethodAdapter(MethodVisitor methodVisitor) {super(ASM6, methodVisitor);}@Overridepublic void visitCode() {mv.visitCode();mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);time = lvs.newLocal(Type.LONG_TYPE);mv.visitVarInsn(LSTORE, time);maxStack = 4;}@Overridepublic void visitInsn(int opcode) {if (((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) && !isPresent) {mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);mv.visitVarInsn(LLOAD, time);mv.visitInsn(LSUB);mv.visitVarInsn(LSTORE, time);mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitTypeInsn(NEW, "java/lang/StringBuilder");mv.visitInsn(DUP);mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);mv.visitFieldInsn(GETSTATIC, owner, filedName, "Ljava/lang/String;");mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append","(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);mv.visitLdcInsn(" " + methodName + ":");mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append","(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);mv.visitVarInsn(LLOAD, time);mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;",false);mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);maxStack = Math.max(aa.stack.size() + 4, maxStack);}mv.visitInsn(opcode);}@Overridepublic void visitMaxs(int maxStack, int maxLocals) {super.visitMaxs(Math.max(maxStack, this.maxStack), maxLocals);}}}
4、编译maven install
这里的项目主要参考的是我上一篇文章的agentTest:,但是因为pom.xml中加入了其他jar包,所以为了把外部的jar包也打包进去,pom.xml要改为如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.suibibk</groupId><artifactId>AgentTest</artifactId><version>0.0.1-SNAPSHOT</version><packaging>jar</packaging><dependencies><dependency><groupId>org.ow2.asm</groupId><artifactId>asm</artifactId><version>6.2.1</version></dependency><dependency><groupId>org.ow2.asm</groupId><artifactId>asm-util</artifactId><version>6.2.1</version></dependency><dependency><groupId>org.ow2.asm</groupId><artifactId>asm-commons</artifactId><version>6.2.1</version></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.1.2</version><configuration><archive><manifest><addClasspath>true</addClasspath></manifest><manifestEntries><Premain-Class>com.suibibk.AgentTest</Premain-Class><!-- <Agent-Class>com.suibibk.AgentTest</Agent-Class> --></manifestEntries></archive></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>1.4</version><configuration><createDependencyReducedPom>false</createDependencyReducedPom></configuration><executions><execution><!-- 执行package的phase --><phase>package</phase><!-- 为这个phase绑定goal --><goals><goal>shade</goal></goals><configuration><!-- 过滤掉以下文件,不打包 :解决包重复引用导致的打包错误--><filters><filter><artifact>*:*</artifact><excludes><exclude>META-INF/*.SF</exclude><exclude>META-INF/*.DSA</exclude><exclude>META-INF/*.RSA</exclude></excludes></filter></filters><transformers><transformerimplementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"><resource>META-INF/spring.handlers</resource></transformer><transformerimplementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"><resource>META-INF/spring.schemas</resource></transformer></transformers></configuration></execution></executions></plugin></plugins></build></project>
5、测试方法
public class Test2 {public static void main(String[] args) {//System.out.println("main2");test();test2();}public static void test() {System.out.println("我是Test2的test方法");}public static void test2() {System.out.println("我是Test2的test2方法");}}
需要加上VM参数:
-javaagent:D:\Work\eclipse_workspace\AgentTest\target\AgentTest-0.0.1-SNAPSHOT.jar=haha
运行输出如下内容,单位为纳秒:
Transforming类sun/misc/URLClassPath$FileLoader$1的字节码开始sun/misc/URLClassPath$FileLoader$1 getCodeSourceURL:1900sun/misc/URLClassPath$FileLoader$1 getInputStream:142300sun/misc/URLClassPath$FileLoader$1 getContentLength:124600Transforming类com/suibibk/test/Test2的字节码开始我是Test2的test方法com/suibibk/test/Test2 test:83100我是Test2的test2方法com/suibibk/test/Test2 test2:44600com/suibibk/test/Test2 main:314500
完成!
参考:
JavaAgent:在主程序运行之前和之后的代理程序
史上最通俗易懂的ASM教程
基于javaAgent和ASM字节码技术跟踪java程序调用链
JVM插码之五:Java agent+ASM实战—监控所有方法执行时间
