0%

使用agentmain进行运行时热部署

简介

在<使用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包

  1. agentmain代码负责从外部读取class文件,并且加载到虚拟机中
  2. 运行的测试工程
  3. 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();
}
}

}


}

开始执行

  1. 将agentmain打成jar包

  2. 运行测试工程main方法出现

1
2
3
4
未修改之前的数据..............
running........................
未修改之前的数据..............
running........................
  1. 修改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已经修改完毕..............");

}


}
  1. 执行jvm加载代码的main方法,在目标虚拟机中执行agentmain方法,在代码中传入agentmain包的路径,重新编译完成的Test文件的路径,和要替换的类(这是是Test类),详情查看jvm加载代码中的注释

  2. 结果之前的Test已经完成了热部署,但是我们并没有重启测试工程的JVM

    1
    2
    3
    4
    5
    6
    未修改之前的数据..............
    running........................
    未修改之前的数据..............
    running........................
    Test已经修改完毕..............
    running........................

使用agentmain的限制

通过查看JDK Instrumentation 的redefineClasses说明发现以下限制,也就是用此方法热部署不能更改类的继承关系,类名,方法名,方法参数,而且如果在从新加载类后如果,之前已经存在正在运行的未重从定义的方法,那么运行的仍然是原来的方法

1
2
3
4
5
6
7
重定义可能会更改方法体、常量池和属性。重定义不得添加、移除、重命名字段或方法;不得更改方法签名、继承关系。在以后的版本中,可能会取消这些限制。在应用转换之前,类文件字节不会被检查、验证和安装。如果结果字节错误,此方法将抛出异常。

如果此方法抛出异常,则不会重定义任何类

如果重定义的方法有活动的堆栈帧,那么这些活动的帧将继续运行原方法的字节码。将在新的调用上使用此重定义的方法。

此方法不会引起任何初始化操作,JVM 惯例语义下发生的初始化除外。换句话说,重定义一个类不会引起其初始化方法的运行。静态变量的值将与调用之前的值一样。