简介 java在JDK1.5中提供了java.lang.Instrument包,该包提供了一些工具帮助开发人员在 Java 程序运行时动态的修改class,此文介绍其中的agent组件来实现基于jvm层面的aop.
命令 如果我们在cmd输入java -help就会看到关于javaagent命令的简介
1 2 3 4 5 6 7 java -help 用法: java [-options] class [args...] 其中选项包括: -javaagent:<jarpath>[=<选项>] 加载 Java 编程语言代理, 请参阅 java.lang.instrument
实例如下
1 java -javaagent:/to/agent.jar -jar /main.jar
其中javaagent参数后的路径就是我们要编写的代码,其可以在要代理运行的jar运行前进行一些class字节的操作,如果在此期间使用犹如javassist,或者asm字节码操作的框架则可以实现在类运行前对要运行的class进行修改.
agent简介
参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有2个要求:
这个jar 包的MANIFEST.MF 文件必须指定 Premain-Class 项。
Premain-Class 指定的那个类必须实现 premain()方法。 关于第一点我们在后续创建工程中使用maven 打包插件maven-assembly-plugin去解决这一点. 现在我们解释一下第二点: javaagent程序不需要实现任何接口,只需要创建一个类其中类中必须要有以下方法之一即可
public static void premain(String agentArgs, Instrumentation inst) public static void premain(String agentArgs)
一般我们使用第一个方法 ,参数 agentArgs 时通过命令行传给 Java Agent 的参数,inst 是Java Class 字节码转换的工具,Instrumentation 常用方法如下:
void addTransformer(ClassFileTransformer transformer, boolean canRetransform); 增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换我们可以使用ClassFileTransformer对象结合javassist或者asm对class文件进行修改 。
void redefineClasses(ClassDefinition… definitions) hrows ClassNotFoundException, UnmodifiableClassException; 在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。
本文中我们使用addTransformer方法在class文件加载前,我们对class字节码进行处理,添加打印日志代码,此方法接收一个ClassFileTransformer对象,Transformer 类包含了一个 transform 方法,它的签名会接受 ClassLoader、类名、要重定义的类所对应的 Class 对象、定义权限的 ProtectionDomain 以及这个类的原始字节。如果从 transform 方法中返回 null 的话,将会告诉运行时环境我们并没有对这个类进行变更。
1 2 3 public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { return new byte[0]; }
实现思路简介 我们实现的原理很简单,首先我们在运行的主工程中定义个注解,此注解的功能是标识出哪些方法和哪些参数需要打印log,其作用域为在方法上,fields属性为数组,我们将需要打印的参数的角标放入其中如,
1 2 3 4 5 6 //打印方法的第一个参数 @PrintLog(fields = {"1"}) public void test(String name,String name2){ }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.liu; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface PrintLog { String[] fields(); }
然后我们对需要打印日志的方法如上加上注解即可,在运行agent程序的时候我们会在class文件加载前,逐个扫描方法,查看是否有@PrintLog注解,如果有注解我们则应用javassist框架在class文件中加入打印代码,由于在之前文章讲述的类加载器的双亲委派模型,同一个class文件在双亲委派模式下只会加载一次,所以只有在class文件第一次被加载时才会触发此修改class的条件,因此也不必过多担心效率问题.
搭建agent工程 创建agent主题类 我们新建一个maven工程,首先新建一个类,其只包含上文简介的premain方法,并添加一个日志打印转换器,此方法会在代码里jar文件执行main方法前执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.liu; import java.lang.instrument.Instrumentation; /** * @author Liush * @description 自定义agent在main方法执行前执行 * @date 2019/9/23 15:32 **/ public class MyAgent { public static void premain(String agentArgs, Instrumentation inst) { inst.addTransformer(new LogPrintTransformer()); } }
上文提到premain方法中有一个Instrumentation对象,其的addTransformer方法可以添加一个创建ClassFileTransformer转换类,其可以在class第一次加载时对class文件进行处理,修改java代码因此我们现在创建一个ClassFileTransformer的实现类,类中主要使用javassist框架扫描加载的类的信息,和方法,查看方法上时候有我们之前标识的@PrintLog注解,如果有的话获取注解中的值,然后最后根据获取的值打印对应的参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 package com.liu; import javassist.*; import javassist.bytecode.AnnotationsAttribute; import javassist.bytecode.CodeAttribute; import javassist.bytecode.LocalVariableAttribute; import javassist.bytecode.MethodInfo; import javassist.bytecode.annotation.Annotation; import javassist.bytecode.annotation.ArrayMemberValue; import javassist.bytecode.annotation.MemberValue; import javassist.bytecode.annotation.StringMemberValue; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * @author Liush * @description * @date 2019/9/23 15:41 **/ public class LogPrintTransformer implements ClassFileTransformer { /** * 签名会接受 ClassLoader、类名、要重定义的类所对应的 Class 对象、定义权限的 ProtectionDomain 以及这个类的原始字节。 * 如果从 transform 方法中返回 null 的话,将会告诉运行时环境我们并没有对这个类进行变更。 */ @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { try { CtClass ctClass=ClassPool.getDefault().get(className.replaceAll("/", ".")); CtMethod[] ctMethods =ctClass.getDeclaredMethods(); for(CtMethod ctMethod:ctMethods){ if(getAnnotation(ctMethod)!=null) { List<String> value = getParamIndexes(getAnnotation(ctMethod)); ctMethod.insertBefore(createJavaString(className, ctMethod, value)); } } return ctClass.toBytecode(); } catch (NotFoundException e) { e.printStackTrace(); } catch (CannotCompileException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } //在javassist中$1代表方法的第一个参数,$2代表第二个参数以此类推可参考https://www.jianshu.com/p/b9b3ff0e1bf8 private String createJavaString(String className,CtMethod ctMethod,List<String> params){ StringBuilder stringBuilder=new StringBuilder(); for(String index:params){ stringBuilder.append("System.out.println($"); stringBuilder.append(index); stringBuilder.append(");"); } return stringBuilder.toString(); } //查找方法注解 public Annotation getAnnotation(CtMethod method) { MethodInfo methodInfo = method.getMethodInfo(); AnnotationsAttribute attInfo = (AnnotationsAttribute) methodInfo .getAttribute(AnnotationsAttribute.visibleTag); if (attInfo != null) { return attInfo.getAnnotation("com.liu.PrintLog"); } return null; } //获得注解中的属性值 public List<String> getParamIndexes(Annotation annotation) { ArrayMemberValue fields = (ArrayMemberValue) annotation.getMemberValue("fields"); if (fields != null) { MemberValue[] values = fields.getValue(); List<String> parameterIndexes = new ArrayList<>(); for (MemberValue val : values) { parameterIndexes.add(((StringMemberValue) val).getValue()); } return parameterIndexes; } return Collections.emptyList(); } }
生成MANIFEST.MF文件 使用agent必须在MANIFEST.MF中指定Premain-Class表示我们只想的agent的类,设置Can-Redefine-Classes和Can-Retransform-Classes属性为true,我们采用maven插件的方式进行打包,由maven自动生成MANIFEST.MF文件 在maven pom依赖中加入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <build> <plugins> <!-- http://maven.apache.org/shared/maven-archiver/index.html#class_manifest --> <plugin> <artifactId>maven-assembly-plugin</artifactId> <version>2.2</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <index>true</index> <manifestEntries> <!-- 设置MANIFEST中的属性 --> <Premain-Class>com.liu.MyAgent</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <!-- 将插件绑定maven的package命令--> <phase>package</phase> <!-- append to the packaging phase. --> <goals> <goal>single</goal> <!-- goals == mojos --> </goals> </execution> </executions> </plugin> </plugins> </build>
创建主工程 创建打印注解 此类为了标识哪些方法的哪些参数需要打印
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.liu; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface PrintLog { String[] fields(); }
创建一个测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.liu; /** * @author Liush * @description * @date 2019/9/23 18:12 **/ public class LogTest { @PrintLog(fields = {"1"}) public void test(String name,String name2){ System.out.println("over..........................."); } }
创建main方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.liu; /** * @author Liush * @description * @date 2019/9/23 18:06 **/ public class MainTest { public static void main(String[] args) { LogTest logTest=new LogTest(); logTest.test("liushaohuang111","liushaohuang2222"); } }
同样我是使用maven maven-assembly-plugin插件进行打包,与上面不同的是我们这次不要指定Premain-Class,Can-Redefine-Classes和Can-Retransform-Classes参数我们只需要声明main函数的入口即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <build> <plugins> <!-- http://maven.apache.org/shared/maven-archiver/index.html#class_manifest --> <plugin> <artifactId>maven-assembly-plugin</artifactId> <version>2.2</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <index>true</index> <manifest> <mainClass>com.liu.MainTest</mainClass> </manifest> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <!-- this is used for inheritance merges --> <phase>package</phase> <!-- append to the packaging phase. --> <goals> <goal>single</goal> <!-- goals == mojos --> </goals> </execution> </executions> </plugin> </plugins> </build>
打包运行 在两个工程目录下执行
运行javaagent命令
1 java -javaagent:.\agent-1.0-SNAPSHOT-jar-with-dependencies.jar -jar .\main_test-1.0-SNAPSHOT-jar-with-dependencies.jar
在控制台中出现,成功打印出传入的第一个参数,并且在原始方法执行打印overing之前执行打印
1 2 liushaohuang111 over...........................
更进一步 在实践中本人思考premain 中的Instrumentation 添加的转换器到底什么时候才起作用呢?这里再做一个实践,我们修改premain方法,往里面加入一行打印代码,为了方便在运行的时候发现什么时候触发转化器,其余代码和之前的代码一模一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 package com.liu; import javassist.*; import javassist.bytecode.AnnotationsAttribute; import javassist.bytecode.CodeAttribute; import javassist.bytecode.LocalVariableAttribute; import javassist.bytecode.MethodInfo; import javassist.bytecode.annotation.Annotation; import javassist.bytecode.annotation.ArrayMemberValue; import javassist.bytecode.annotation.MemberValue; import javassist.bytecode.annotation.StringMemberValue; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * @author Liush * @description * @date 2019/9/23 15:41 **/ public class LogPrintTransformer implements ClassFileTransformer { /** * 签名会接受 ClassLoader、类名、要重定义的类所对应的 Class 对象、定义权限的 ProtectionDomain 以及这个类的原始字节。 * 如果从 transform 方法中返回 null 的话,将会告诉运行时环境我们并没有对这个类进行变更。 */ @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { try { System.out.println("in transform...................."); CtClass ctClass=ClassPool.getDefault().get(className.replaceAll("/", ".")); CtMethod[] ctMethods =ctClass.getDeclaredMethods(); for(CtMethod ctMethod:ctMethods){ if(getAnnotation(ctMethod)!=null) { List<String> value = getParamIndexes(getAnnotation(ctMethod)); ctMethod.insertBefore(createJavaString(className, ctMethod, value)); } } return ctClass.toBytecode(); } catch (NotFoundException e) { e.printStackTrace(); } catch (CannotCompileException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } //在javassist中$1代表方法的第一个参数,$2代表第二个参数以此类推可参考https://www.jianshu.com/p/b9b3ff0e1bf8 private String createJavaString(String className,CtMethod ctMethod,List<String> params){ StringBuilder stringBuilder=new StringBuilder(); for(String index:params){ stringBuilder.append("System.out.println($"); stringBuilder.append(index); stringBuilder.append(");"); } return stringBuilder.toString(); } //查找方法注解 public Annotation getAnnotation(CtMethod method) { MethodInfo methodInfo = method.getMethodInfo(); AnnotationsAttribute attInfo = (AnnotationsAttribute) methodInfo .getAttribute(AnnotationsAttribute.visibleTag); if (attInfo != null) { return attInfo.getAnnotation("com.liu.PrintLog"); } return null; } //获得注解中的属性值 public List<String> getParamIndexes(Annotation annotation) { ArrayMemberValue fields = (ArrayMemberValue) annotation.getMemberValue("fields"); if (fields != null) { MemberValue[] values = fields.getValue(); List<String> parameterIndexes = new ArrayList<>(); for (MemberValue val : values) { parameterIndexes.add(((StringMemberValue) val).getValue()); } return parameterIndexes; } return Collections.emptyList(); } }
现在我们更改主项目中被代理的main方法代码,里面一共new了4次LogTest对象
第一个是第一次触发class加载(class文件只有被调用时候才会第一次加载到虚拟机),则会触发premain方法中的转换器
第二个是直接调用堆栈中的class又new一次对象对象,没有触发转化器
第三个我们使用自定义类加载器,并且不突破双亲委派模式(还是调用堆栈中的LogTest),同样也没有触发转化器
第四个,我们又创建了一个类加载器,并且突破双亲委派模式,重新加载一个LogTest,不再使用之前堆栈中的LogTest对象,最后发现触发了转化器
结论:只有在class重新被加载时才会触发ClassFileTransformer转换器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 package com.liu; import java.io.IOException; import java.io.InputStream; /** * @author Liush * @description * @date 2019/9/23 18:06 **/ public class MainTest { public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { //第一次次加载触发ClassFileTransformer LogTest logTest = new LogTest(); logTest.test("liushaohuang111", "liushaohuang2222"); //重新创建对象,不会触发premain中的ClassFileTransformer(类已经加载到堆栈中) LogTest logTest1 = new LogTest(); logTest1.test("shao111111111111", "shao22222222222222"); //不突破双亲委派类进行类加载,不会触发premain中的ClassFileTransformer(类已经加载到堆栈中) ClassLoader classLoader = new ClassLoader() { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream is = getClass().getResourceAsStream(fileName); if (is == null) { return super.loadClass(name); } byte[] b = new byte[is.available()]; is.read(b); return defineClass(name, b, 0, b.length); } catch (IOException e) { e.printStackTrace(); throw new ClassNotFoundException(name); } } }; Class aClass = classLoader.loadClass("com.liu.LogTest"); LogTest logTest2 = (LogTest)aClass.newInstance(); logTest2.test("Parents Delegation Model 1","Parents Delegation Model 2"); //突破双亲委派模式 ClassLoader classLoader2=new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream is = getClass().getResourceAsStream(fileName); if (is == null) { return super.loadClass(name); } byte[] b = new byte[is.available()]; is.read(b); return defineClass(name, b, 0, b.length); } catch (IOException e) { e.printStackTrace(); throw new ClassNotFoundException(name); } } }; Class aClass2 = classLoader.loadClass("com.liu.LogTest"); LogTest logTest3 = (LogTest)aClass.newInstance(); logTest3.test("No Parents Delegation Model1","No Parents Delegation Model 2"); } }
输出结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... in transform.................... liushaohuang111 over........................... shao111111111111 over........................... Parents Delegation Model 1 over........................... in transform.................... No Parents Delegation Model1 over........................... in transform.................... in transform....................