简介
在<使用agent和javassist实现基于jvm的aop日志打印系统>一文中,我使用了javaagent完成了一个基于JVM的aop,在main方法运行前动态的修改class文件的字节码,从而达到aop的效果,而java应用代码的编写者却感觉不到我们修改了代码,但是使用javaagent我们在启动java的main方时启动premain方法(虽然inst.addTransformer启动的ClassFileTransformer是永久存在的,也就是说后续用户自定义类加载器,并放弃双亲委派时(因为使用双亲委派模式每次加载的都是同一个class对象)也会调用ClassFileTransformer里的方法)(具体实践可查看使用agent和javassist实现基于jvm的aop日志打印系统 中的更进一步一节),但是这要求我们每次在主代码启动时必须加上javaagent参数去启动主代码包,如果我们转换器ClassFileTransformer需要变动的话那还是要停掉jvm,那么有无一种可以在运行时可以动态修改字节码的方法呢?答案就是JDK1.6提供的agentmain。
1 2 3 4
| public static void premain(String agentArgs, Instrumentation inst) { inst.addTransformer(new ClassFileTransformer() //省略实现类...);
}
|
实现思路
实例一个分层3个jar包
- agentmain代码负责从外部读取class文件,并且加载到虚拟机中
- 运行的测试工程
- jvm加载代码,从本机中查找目标虚拟机,将运行agentmain的jar包插入到目标虚拟机中完成热部署
agentmain代码
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
| package com.liu;
import java.io.IOException; import java.lang.instrument.*; import java.net.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths;
/** * @author Liush * @description * @date 2019/9/25 14:57 **/ public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException, URISyntaxException, IOException {
if(agentArgs.trim()==null){ return; } //从参数中截取文件路径和要修改的类 String[] args=agentArgs.split(" ");
//查找所有已经加载的类 Class[] classes=inst.getAllLoadedClasses(); for(Class<?> c:classes){
//查找需要重新加载的类 if(!c.getName().endsWith("."+args[1])){ continue; }
//获取修改后的类的字节码 //格式 file:///C:/Users/Administrator/Desktop/test/Test.class Path path = Paths.get(new URI(args[0])); byte[] classBytes = Files.readAllBytes(path);
//重新定义类,完成热部署 inst.redefineClasses(new ClassDefinition(c,classBytes)); } }
}
|
打包,我们需要在MANIFEST.MF 中指定Agent-Class和Can-Redefine-Classes,Can-Retransform-Classes属性,我们采用maven打包方式加入如下插件,并且打成jar包
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
| <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> <Agent-Class>com.liu.AgentMain</Agent-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>
|
测试工程
并没有什么特别循环执行Test.test()方法
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
| package com.liu;
/** * @author Liush * @description * @date 2019/9/25 16:31 **/ public class MainTest {
public static void main(String[] args) {
while (true){ Test test=new Test(); test.test(); System.out.println("running........................"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }
}
} }
|
Test 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package com.liu;
/** * @author Liush * @description * @date 2019/9/25 16:54 **/ public class Test {
public void test(){
System.out.println("未修改之前的数据..............");
}
}
|
jvm加载代码
注意VirtualMachine类是在${JAVA_HOME}/lib包下的tool.jar包下,需要在编译器中加入这个jar包,以idea为例点击Files->Project Structure->Libraiest 在此节目中添加tool.jar包即可
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
| package com.liu;
import com.sun.tools.attach.*;
import java.io.IOException; import java.util.List;
/** * @author Liush * @description * @date 2019/9/25 15:16 **/ public class JVMLoader {
public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
List<VirtualMachineDescriptor> list=VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list) {
//查找MainTest虚拟机 if (vmd.displayName().endsWith("MainTest")) { VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//在目标虚拟机中加载agentmainjar包,并且传入修改后的Test.class文件 和要修改的类这里是Test,传入一个字符串参数,这个字符串将在agentmain中进行分割得到对应的参数,比如这里使用" "做区分不同参数 virtualMachine.loadAgent("C:\\Users\\Administrator\\Desktop\\test\\agentmain-1.0-SNAPSHOT-jar-with-dependencies.jar ", "file:///C:/Users/Administrator/Desktop/test/Test.class Test"); System.out.println("ok"); virtualMachine.detach(); } }
}
}
|
开始执行
将agentmain打成jar包
运行测试工程main方法出现
1 2 3 4
| 未修改之前的数据.............. running........................ 未修改之前的数据.............. running........................
|
- 修改Test文件并且重新编译
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/25 16:54 **/ public class Test {
public void test(){
System.out.println("Test已经修改完毕..............");
}
}
|
执行jvm加载代码的main方法,在目标虚拟机中执行agentmain方法,在代码中传入agentmain包的路径,重新编译完成的Test文件的路径,和要替换的类(这是是Test类),详情查看jvm加载代码中的注释
结果之前的Test已经完成了热部署,但是我们并没有重启测试工程的JVM
1 2 3 4 5 6
| 未修改之前的数据.............. running........................ 未修改之前的数据.............. running........................ Test已经修改完毕.............. running........................
|
使用agentmain的限制
通过查看JDK Instrumentation 的redefineClasses说明发现以下限制,也就是用此方法热部署不能更改类的继承关系,类名,方法名,方法参数,而且如果在从新加载类后如果,之前已经存在正在运行的未重从定义的方法,那么运行的仍然是原来的方法
1 2 3 4 5 6 7
| 重定义可能会更改方法体、常量池和属性。重定义不得添加、移除、重命名字段或方法;不得更改方法签名、继承关系。在以后的版本中,可能会取消这些限制。在应用转换之前,类文件字节不会被检查、验证和安装。如果结果字节错误,此方法将抛出异常。
如果此方法抛出异常,则不会重定义任何类
如果重定义的方法有活动的堆栈帧,那么这些活动的帧将继续运行原方法的字节码。将在新的调用上使用此重定义的方法。
此方法不会引起任何初始化操作,JVM 惯例语义下发生的初始化除外。换句话说,重定义一个类不会引起其初始化方法的运行。静态变量的值将与调用之前的值一样。
|