javaagent使用指南
今天打算写一下 Javaagent,一开始我对它的概念也比较陌生,知道大神写mybatis重sql打印的时候知道有这么一个技术自己恶补一下,顺便梳理下相关知识。
作为开始了解的你可以认为是: 字节级别的aop
Javaagent是java命令的一个参数。参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有2个要求:
在命令行输入 java可以看到相应的参数,其中有 和 java agent相关的:
-agentlib:<libname>[=<选项>] 加载本机代理库 <libname>, 例如 -agentlib:hprof 另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help -agentpath:<pathname>[=<选项>] 按完整路径名加载本机代理库 -javaagent:<jarpath>[=<选项>] 加载 Java 编程语言代理, 请参阅 java.lang.instrument
在上面-javaagent参数中提到了参阅java.lang.instrument,这是在rt.jar 中定义的一个包,该路径下有两个重要的类:
![在这里插
该包提供了一些工具帮助开发人员在 Java 程序运行时,动态修改系统中的 Class 类型。其中,使用该软件包的一个关键组件就是 Javaagent。从名字上看,似乎是个 Java 代理之类的,而实际上,他的功能更像是一个Class 类型的转换器,他可以在运行时接受重新外部请求,对Class类型进行修改。
从本质上讲,Java Agent 是一个遵循一组严格约定的常规 Java 类。 上面说到 javaagent命令要求指定的类中必须要有premain()方法,并且对premain方法的签名也有要求,签名必须满足以下两种格式:
public static void premain(String agentArgs, Instrumentation inst) public static void premain(String agentArgs)
JVM 会优先加载 带 Instrumentation 签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法。这个逻辑在sun.instrument.InstrumentationImpl 类中:
Instrumentation 接口定义如下:
public interface Instrumentation { //增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据, //参数 canRetransform 设置是否允许重新转换。 void addTransformer(ClassFileTransformer transformer, boolean canRetransform); //在类加载之前,重新定义 Class 文件, //ClassDefinition 表示对一个类新的定义, //如果在类加载之后,需要使用 retransformClasses 方法重新定义。 //addTransformer方法配置之后,后续的类加载都会被Transformer拦截。 //对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。 //类加载的字节码被修改后,除非再次被retransform,否则不会恢复。 void addTransformer(ClassFileTransformer transformer); //删除一个类转换器 boolean removeTransformer(ClassFileTransformer transformer); boolean isRetransformClassesSupported(); //在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的, //事实上,该方法是 update 了一个类。 void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; boolean isRedefineClassesSupported(); void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException; boolean isModifiableClass(Class<?> theClass); @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); @SuppressWarnings("rawtypes") Class[] getInitiatedClasses(ClassLoader loader); //获取一个对象的大小 long getObjectSize(Object objectToSize); void appendToBootstrapClassLoaderSearch(JarFile jarfile); void appendToSystemClassLoaderSearch(JarFile jarfile); boolean isNativeMethodPrefixSupported(); void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix); }
最为重要的是上面注释的几个方法,下面我们会用到。
使用 javaagent 需要几个步骤:
在执行以上步骤后,JVM 会先执行 premain 方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。当然,遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,那么就可以去做重写类这样的操作,结合第三方的字节码编译工具,比如ASM,javassist,cglib等等来改写实现类。
ps:字节码编译工具:Asm,javassist,byte budy,byte budy是基于asm进行的封装
通过上面的步骤我们用代码实现来实现。实现 javaagent 你需要搭建两个工程,一个工程是用来承载 javaagent类,单独的打成jar包;一个工程是javaagent需要去代理的类。即javaagent会在这个工程中的main方法启动之前去做一些事情。
工程目录结构如下:
-java-agent ----src --------main --------|------java --------|----------com.rickiyang.learn --------|------------PreMainTraceAgent --------|resources -----------META-INF --------------MANIFEST.MF
这个地方就是我们上面要求的规范
public class DemoAgent { public static void premain(String args, Instrumentation instrumentation) { System.out.println(DemoAgent.class.getName()+"premain()"); instrumentation.addTransformer(new DemoTransformer()); } }
DemoTransformer 这里就是编写我们自己的逻辑的地方:
public class DemoTransformer implements ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println(className); String clName = className.replace("/", "."); if("com.wfg.hello.Demo".equals(clName)){ System.out.println("======="+clName); try { CtClass ctClass = ClassPool.getDefault().get(clName); CtMethod ctMethod = ctClass.getDeclaredMethod("hello"); ctMethod.insertBefore("System.out.println(\"javaage 新增日志\");"); ctMethod.insertAfter("System.out.println(\"javaage after 新增日志\");"); return ctClass.toBytecode(); } catch (Exception e) { e.printStackTrace(); System.out.println("+++++++++"); } } return null; } }
另外要求的配置文件中的一堆要求我们这里不手工编写了maven插件支持我们使用插件的方式,打包自动生成了就
<?xml version="1.0" encoding="UTF-8"?> <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"> <parent> <artifactId>javaagentsdk</artifactId> <groupId>org.example</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>agentdemo</artifactId> <groupId>coderead</groupId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.18.1-GA</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.2</version> <configuration> <archive> <manifestEntries> <Project-name>${project.name}</Project-name> <Project-version>${project.version}</Project-version> <Premain-Class>com.wfg.DemoAgent</Premain-Class> <Can-Redefine-Classes>false</Can-Redefine-Classes> <Can-Retransform-Classes>false</Can-Retransform-Classes> </manifestEntries> </archive> <skip>true</skip> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <artifactSet> <includes> <include>org.javassist:javassist</include> </includes> </artifactSet> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.wfg.DemoAgent</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>utf8</encoding> </configuration> </plugin> </plugins> </build> </project>
上面配置完成就可以打包了,将生成的jar文件进行添加到vm参数中,无论是使用命令行,还是idea都可以;
一个java程序中-javaagent参数的个数是没有限制的,所以可以添加任意多个javaagent。所有的java agent会按照你定义的顺序执行,例如:
java -javaagent:agent1.jar -javaagent:agent2.jar -jar MyProgram.jar
程序执行的顺序将会是:
MyAgent1.premain -> MyAgent2.premain -> MyProgram.main
public class Demo { public static void hello(){ System.out.println("-===hello=========="); } public static void main(String[] args) { Demo.hello(); } }
我直接使用idea了,不演示命令行了,其实一样的:
这一步比忘记了
运行效果:
和我们熟悉的aop效果一样的,这种方式不用动业务代码,只需要增加运行参数就可以了,这是比较有优势的一点吧;
下面是使用javassist来动态将某个方法替换掉
这里模拟 date 中的 tostring方法被改写:
public class DateTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { // 操作Date类 if ("java/util/Date".equals(className)) { try { // 从ClassPool获得CtClass对象 final ClassPool classPool = ClassPool.getDefault(); final CtClass clazz = classPool.get("java.util.Date"); CtMethod toString = clazz.getDeclaredMethod("toString"); //这里对 java.util.Date.convertToAbbr() 方法进行了改写,在 return之前增加了一个 打印操作 String methodBody = "{\n" + " System.out.println(\"date tostring 被改写了\");\n" + " return \"aa\";\n" + " }"; toString.setBody(methodBody); // 返回字节码,并且detachCtClass对象 byte[] byteCode = clazz.toBytecode(); //detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载 clazz.detach(); return byteCode; } catch (Exception ex) { ex.printStackTrace(); } } // 如果返回null则字节码不会被修改 return null; } }
确实惊到我了,原来自己没接触过这一块。。。。。
上面介绍的Instrumentation是在 JDK 1.5中提供的,开发者只能在main加载之前添加手脚,在 Java SE 6 的 Instrumentation 当中,提供了一个新的代理操作方法:agentmain,可以在 main 函数开始运行之后再运行。
跟premain函数一样, 开发者可以编写一个含有agentmain函数的 Java 类:
//采用attach机制,被代理的目标程序VM有可能很早之前已经启动, //当然其所有类已经被加载完成,这个时候需要借助Instrumentation#retransformClasses(Class<?>... classes)让对应的类可以重新转换, //从而激活重新转换的类执行ClassFileTransformer列表中的回调 public static void agentmain (String agentArgs, Instrumentation inst) public static void agentmain (String agentArgs)
同样,agentmain 方法中带Instrumentation参数的方法也比不带优先级更高。开发者必须在 manifest 文件里面设置“Agent-Class”来指定包含 agentmain 函数的类。
在Java6 以后实现启动后加载的新实现是Attach api。Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面:
VirtualMachine 字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息(比如获取内存dump、线程dump,类信息统计(比如已加载的类以及实例个数等), loadAgent,Attach 和 Detach (Attach 动作的相反行为,从 JVM 上面解除一个代理)等方法,可以实现的功能可以说非常之强大 。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上 。
代理类注入操作只是它众多功能中的一个,通过loadAgent方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理。
VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。
attach实现动态注入的原理如下:
通过VirtualMachine类的attach(pid)方法,便可以attach到一个运行中的java进程上,之后便可以通过loadAgent(agentJarPath)来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法。
既然是两个进程之间通信那肯定的建立起连接,VirtualMachine.attach动作类似TCP创建连接的三次握手,目的就是搭建attach通信的连接。而后面执行的操作,例如vm.loadAgent,其实就是向这个socket写入数据流,接收方target VM会针对不同的传入数据来做不同的处理。
我们来测试一下agentmain的使用:
工程结构和 上面premain的测试一样,编写AgentMainTest,然后使用maven插件打包 生成MANIFEST.MF。
public class AgentMainTest { public static void agentmain(String agentArgs, Instrumentation instrumentation) { System.out.println("com.wfg.AgentMainTest.agentmain"); instrumentation.addTransformer(new DefineTransformer(), true); } static class DefineTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println("premain load Class:" + className); String clName = className.replace("/", "."); if("com.wfg.hello.Demo".equals(clName)){ System.out.println("======="+clName); try { CtClass ctClass = ClassPool.getDefault().get(clName); CtMethod ctMethod = ctClass.getDeclaredMethod("hello"); ctMethod.insertBefore("System.out.println(\"javaage 新增日志\");"); ctMethod.insertAfter("System.out.println(\"javaage after 新增日志\");"); return ctClass.toBytecode(); } catch (Exception e) { e.printStackTrace(); System.out.println("+++++++++"); } } return classfileBuffer; } } }
<?xml version="1.0" encoding="UTF-8"?> <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"> <parent> <artifactId>javaagentsdk</artifactId> <groupId>org.example</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>agentdemo-attach</artifactId> <groupId>coderead</groupId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.18.1-GA</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.2</version> <configuration> <archive> <!--自动添加META-INF/MANIFEST.MF --> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Project-name>${project.name}</Project-name> <Project-version>${project.version}</Project-version> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> <Agent-Class>com.wfg.AgentMainTest</Agent-Class> </manifestEntries> </archive> <skip>true</skip> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <artifactSet> <includes> <include>org.javassist:javassist</include> </includes> </artifactSet> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.wfg.AgentMainTest</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>utf8</encoding> </configuration> </plugin> </plugins> </build> </project>
打包后进行测试
public class TestAgentMain { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { //获取当前系统中所有 运行中的 虚拟机 System.out.println("running JVM start "); List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list) { //如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid //然后加载 agent.jar 发送给该虚拟机 System.out.println(vmd.displayName()); if (vmd.displayName().endsWith("com.wfg.hello.TestAgentMain")) { VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("/Users/wufagang/IdeaProjects/javaagentsdk/agentdemo-attach/target/agentdemo-attach-1.0-SNAPSHOT.jar"); virtualMachine.detach(); } } Demo.hello(); } }
可以看到实际上是启动了一个socket进程去传输agent.jar。先打印了“running JVM start”表名main方法是先启动了,然后才进入代理类的transform方法。
list()方法会去寻找当前系统中所有运行着的JVM进程,你可以打印vmd.displayName()看到当前系统都有哪些JVM进程在运行。因为main函数执行起来的时候进程名为当前类名,所以通过这种方式可以去找到当前的进程id。
注意:在mac上安装了的jdk是能直接找到 VirtualMachine 类的,但是在windows中安装的jdk无法找到,如果你遇到这种情况,请手动将你jdk安装目录下:lib目录中的tools.jar添加进当前工程的Libraries中。
instrument的底层实现依赖于JVMTI(JVM Tool Interface),它是JVM暴露出来的一些供用户扩展的接口集合,JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。JVMTIAgent是一个利用JVMTI暴露出来的接口提供了代理启动时加载(agent on load)、代理通过attach形式加载(agent on attach)和代理卸载(agent on unload)功能的动态库。而instrument agent可以理解为一类JVMTIAgent动态库,别名是JPLISAgent(Java Programming Language Instrumentation Services Agent),也就是专门为java语言编写的插桩服务提供支持的代理。
创建并初始化 JPLISAgent;
监听 VMInit 事件,在 JVM 初始化完成之后做下面的事情:
创建 InstrumentationImpl 对象 ;
监听 ClassFileLoadHook 事件 ;
调用 InstrumentationImpl 的loadClassAndCallPremain方法,在这个方法里会去调用 javaagent 中 MANIFEST.MF 里指定的Premain-Class 类的 premain 方法 ;
通过 JVM 的attach机制来请求目标 JVM 加载对应的agent,过程大致如下:
大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,或者笼统说就是类重定义(Class Redefine)的功能,但是有以下的局限性: