不重启 JVM,如何替换掉已经加载的类?
场景
在遥远的希艾斯星球爪哇国塞沃城中,两名年轻的程序员正在为一件事情苦恼,程序出问题了,一时看不出问题出在哪里,于是有了以下对话:
“Debug一下吧。”
“线上机器,没开Debug端口。”
“看日志,看看请求值和返回值分别是什么?”
“那段代码没打印日志。”
“改代码,加日志,重新发布一次。”
“怀疑是线程池的问题,重启会破坏现场。”
长达几十秒的沉默之后:“据说,排查问题的最高境界,就是只通过Review代码来发现问题。”
比几十秒长几十倍的沉默之后:“我轮询了那段代码一十七遍之后,终于得出一个结论。”
“结论是?”
“我还没到达只通过Review代码就能发现问题的至高境界。”
方案
BTrace
阿里的Arthas
其他楼主未知的工具
上面的Btrace在阿里的Arthas出来前是比较流行的,但个人认为使用起来还是繁琐的。具体使用的入门教程参见:BTrace入门
在Arthas出来后,个人建议使用这个,这个相比Btrace使用起来要简单多了,而且内置了一些功能,具体的使用教程参见官网:Arthas官方教程
本文
透过现象看本质,其实无论是Btrace还是Arthas,他们的底层实现原理脱离不了jvm自带的两个api:
- Instrumentation API
- Attach API
本文就是用这两个api来具体实践一下
本文所用到的完整代码已上传到码云: instrument
模拟业务代码
public class BusinessClass {
public void businessMethod() {
System.out.println("businessMethod code: " + new Date());
}
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
new BusinessClass().businessMethod();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
}
}
上面的代码打成一个可执行jar包,然后执行 java -jar 跑起来 ,假设跑起来后的pid是22820,记下来就行后续会用。
Agent代码
public class InstrumentAgent {
public static void agentmain(String agentArgs, Instrumentation inst) {
try {
System.out.println("Agent Main Start");
//创建一个ClassFileTransformer对象,在这个对象的transform方法中完成真正的字节码替换
BusinessTransformer businessTransformer = new BusinessTransformer();
inst.addTransformer(businessTransformer, true);
//触发BusinessClass的二进制更新,执行该代码后会执行ClassFileTransformer对象的transform方法
inst.retransformClasses(Class.forName("com.rocky.practice.instrument.BusinessClass"));
//移除(如果不移除,多次执行attach,那么transform方法会执行多次,没有意义)
inst.removeTransformer(businessTransformer);
System.out.println("Agent Main End: " + Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
上面的类需要依赖jdk lib目录下的tools.jar,所以在maven中需要添加如下依赖:
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.5.0</version>
<scope>system</scope>
<systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath>
</dependency>
ClassFileTransformer代码
public class BusinessTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("start transform: " + className + " " + Thread.currentThread().getName());
//这里只重新定义BusinessClass类,采用了javassist框架来完成类的重定义,也可采用其他二进制框架,比如ASM
if (className.equalsIgnoreCase("com/rocky/practice/instrument/BusinessClass")) {
try {
ClassPool classPool = ClassPool.getDefault();
try {
CtClass ctClass = classPool.get(className.replaceAll("/", "."));
CtMethod ctMethod = ctClass.getDeclaredMethod("businessMethod");
if (!ctMethod.isEmpty()) {
ctMethod.setBody("" +
"{" +
"System.out.println(\"instrument api success\");" +
"}");
}
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
} catch (Throwable e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
}
这段代码完成的功能:重写BusinessClass中businessMethod的方法体,并生成类的二进制,功能实现需要依赖javassist框架,所以maven中添加依赖:
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>
上面的两个类打包成另外一个jar包,比如叫agent.jar。 打这个jar包的时候需要注意下,需要自定义manifest.mf:
Manifest-Version: 1.0
Agent-Class: com.rocky.practice.instrument.InstrumentAgent
Can-Retransform-Classes: true
这里我打jar包的时候,折腾了一下,最终通过maven的插件轻松解决指定manifest.mf的问题
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<!--在这里指定manifest.mf的路径-->
<manifestFile>
src/main/resources/META-INF/MANIFEST.MF
</manifestFile>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
Attach API
public class AttachMain {
public static void main(String[] args) {
try {
VirtualMachine attach = VirtualMachine.attach("9044");
attach.loadAgent("D:\\IdeaProjects\\java_practice_public\\instrument\\instrument_project\\target\\instrument_project-1.0-SNAPSHOT-jar-with-dependencies.jar");
Thread.sleep(1000000);
attach.detach();
System.out.println("detached");
} catch (Exception e) {
e.printStackTrace();
}
}
}
这个代码中的9044修改为上面得到的pid,下面那行代码的jar包路径改为刚生成的agent.jar的路径,然后执行。顺利的话,这个main执行后,跑bussinessclass代码的进程讲会打印出类似下面的语句:
![enter description here](https://cdn.justdopay.com/xiaoshujiang/1628764982862.png)
总结:
通过attach api能连上目标pid进程,然后会执行agent.jar里的premain方法,在这个方法里就可以通过Instrumentation实例对象触发对某个类的替换
用过arthas的同学,其中有个重新定义类的功能,相信通过本文也能了解其实现原理了。
多一嘴:idea的破解之前有个破解的agent,是不是也是同样的原理呢?
本文所用到的完整代码已上传到码云: instrument
至此,本文也就实现了标题所实现的功能,开头描述的场景也就得到解决方案,下期再见
系统推荐
- 随记
- MongoDB高可用
- 测试两台服务器间的网速
- gperftools
- 异地多活
- SQL优化
- Markdown软件比对
- vuepress-theme-hope使用心得
- Nginx的双向认证配置
- Docker跨主机通信方案
- PostgreSQL高可用
- Linux
- 随机毒鸡汤:只要自己躺平,就不会被任何人击倒!