java字节码增强 实现 1:asm 2: javassist
易用性来考虑的话使用 javassist
简单来个例子
public class Base {
public void process() {
System.out.println("process");
}
}
/**
* 测试类
* 这里我们来增强Base类的process 在process方法前后打印 start和 end
*
*/
public class Test {
public static void main(String[] args) {
ClassPool classPool = ClassPool.getDefault();
try {
//注意这里为全类名
CtClass ctClass = classPool.get("com.style.note.base.javassist.Base");
CtMethod process = ctClass.getDeclaredMethod("process");
process.insertBefore("{System.out.println(\"start\");}");
process.insertAfter("{System.out.println(\"end\");}");
//使用增强的class 创建出新的对应进行调用对应的方法
Class<?> baseClazz = ctClass.toClass();
Base base = (Base) baseClazz.newInstance();
base.process();
} catch (NotFoundException | CannotCompileException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
}
看增强后效果如图
这里我们就完成了一个字节码增强
这里也有存在问题 JVM中已经存在一个全类名的类 如果在尝试加载 JVM会进行报错 我们再来测试下 在Test类的main中第一行加载 初始化类的操作 Base b = new Base();
public class Test {
public static void main(String[] args) {
//首先进行初始化一下 Base
Base b = new Base();
ClassPool classPool = ClassPool.getDefault();
try {
CtClass ctClass = classPool.get("com.style.note.base.javassist.Base");
CtMethod process = ctClass.getDeclaredMethod("process");
process.insertBefore("{System.out.println(\"start\");}");
process.insertAfter("{System.out.println(\"end\");}");
Class<?> baseClazz = ctClass.toClass();
Base base = (Base) baseClazz.newInstance();
base.process();
} catch (NotFoundException | CannotCompileException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
}
JVM启动会进行保存
那如何解决 JVM 不允许运行时重加载类信息的问题呢?为了达到这个目的,需要借助java的类库 instrument
instrument 是 JVM 提供的一个可以修改已加载类的类库,专门为 Java 语言编写的插桩服务提供支持。它需要依赖 JVMTI 的 Attach API 机制实现,JVMTI 这一部分,我们将在下一小节进行介绍。在 JDK 1.6 以前,instrument 只能在 JVM 刚启动开始加载类时生效,而在 JDK 1.6 之后,instrument 支持了在运行时对类定义的修改。要使用 instrument 的类修改功能,我们需要实现它提供的 ClassFileTransformer 接口,定义一个类文件转换器。接口中的 transform() 方法会在类文件
被加载时调用,而在 transform 方法里,我们可以利用上文中的 ASM 或 Javassist对传入的字节码进行改写或替换,生成新的字节码数组后返回。
我们自定义实现 ClassFileTransformer 接口的类 同时 使用javassit来实现字节码增强
/**
*
* @author leon
* @date 2021-12-09 17:33:51
*/
public class Base {
public static void main(String[] args) {
while (true) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
process();
}
}
public static void process() {
System.out.println("process");
}
}
public class CustomTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
ClassPool classPool = ClassPool.getDefault();
try {
CtClass ctClass = classPool.get("com.style.note.base.instrument.Base");
CtMethod method = ctClass.getDeclaredMethod("process");
method.insertBefore("{System.out.println(\"start\");}");
method.insertAfter("{System.out.println(\"end \");}");
return ctClass.toBytecode();
} catch (NotFoundException | CannotCompileException | IOException e) {
e.printStackTrace();
}
return null;
}
}
现在有了 Transformer,那么它要如何注入到正在运行的 JVM 呢?还需要定义一个 Agent,借助 Agent 的能力将 Instrument 注入到 JVM 中。我们将在下一小节
介绍 Agent,现在要介绍的是 Agent 中用到的另一个类 Instrumentation。在 JDK
1.6 之后,Instrumentation 可以做启动后的 Instrument、本地代码(Native Code)
的 Instrument,以及动态改变 Classpath 等等。我们可以向 Instrumentation 中添加上文中定义的 Transformer,并指定要被重加载的类,代码如下所示。这样,当Agent 被 Attach 到一个 JVM 中时,就会执行类字节码替换并重载入 JVM 的操作。
public class Agent {
/**
* agentmain
*
* @param args args
* @param instrumentation instrumentation
*/
public static void agentmain(String args, Instrumentation instrumentation) {
System.out.println("-------------加载-----------------");
instrumentation.addTransformer(new CustomTransformer(), true);
try {
instrumentation.retransformClasses(Base.class);
System.out.println("agent load done ");
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
}
我们可以借助 JVMTI 的一部分能力,帮助动态重载类信息。
JVM TI(JVM TOOL INTERFACE,JVM 工具接口)是 JVM 提供的一套对 JVM进行操作的工具接口。通过 JVMTI,可以实现对 JVM 的多种操作,它通过接口注册各种事件勾子,在 JVM 事件触发时,同时触发预定义的勾子,以实现对各个 JVM事件的响应,事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、GC 开始和结束、方法调用进入和退出、临界区竞争与等待、VM 启动与退出等等。
而 Agent 就是 JVMTI 的一种实现,Agent 有两种启动方式,一是随 Java 进程启动而启动,经常见到的 java -agentlib 就是这种方式;二是运行时载入,通过attach API,将模块(jar 包)动态地 Attach 到指定进程 id 的 Java 进程内。Attach API 的作用是提供 JVM 进程间通信的能力,比如说我们为了让另外一个 JVM 进程把线上服务的线程 Dump 出来,会运行 jstack 或 jmap 的进程,并传递 pid 的参数,告诉它要对哪个进程进行线程 Dump,这就是 Attach API 做的事情。在下面,我们将通过 Attach API 的 loadAgent() 方法,将打包好的 Agent jar 包动态 Attach 到目标 JVM 上。具体实现起来的步骤如下:
- 定义agent类 实现agentmain方法如上Agent类
- 将agent打包为包含MANIFEST 还有所依赖jar的 jar包(这里是 javassist-3.28.0-GA.jar )其中MANIFEST中Agent-Class 为Agent类全类名
MANIFEST文件如下
Manifest-Version: 1.0
Agent-Class: com.style.note.base.instrument.Agent
Created-By: leon
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Boot-Class-Path: javassist-3.28.0-GA.jar
jar cvfm agent.jar MANIFEST.MF Agent.java javassist-3.28.0-GA.jar
- 最后利用 Attach API,将我们打包好的 jar 包 Attach 到指定的 JVM pid 上
public class Attache {
public static void main(String[] args) {
try {
//目标JVM pid
VirtualMachine attach = VirtualMachine.attach("28353");
attach.loadAgent("/usr/local/develop/workplace/note/src/main/java/com/style/note/base/instrument/agent.jar");
} catch (AttachNotSupportedException | IOException | AgentLoadException | AgentInitializationException e) {
e.printStackTrace();
}
}
}
- 由于在 MANIFEST.MF 中指定了 Agent-Class,所以在 Attach 后,目标
JVM 在运行时会走到 Agent 类中定义的 agentmain() 方法,而在这个
方法中,我们利用 Instrumentation,将指定类的字节码通过定义的类转化器CustomTransformer 做了 Base 类的字节码替换(通过 javassist),并完成了类的重新加载。由此,我们达成了“在 JVM 运行时,改变类的字节码并重新载入类信息”的目的。
以下为运行时重新载入类的效果:先运行 Base 中的 main() 方法,启动一个JVM,可以在控制台看到每隔五秒输出一次”process”。接着执行 Attache 中的main() 方法,并将上一个 JVM 的 pid 传入。此时回到上一个 main() 方法的控制台,可以看到现在每隔五秒输出”process”前后会分别输出”start”和”end”,也就是说
完成了运行时的字节码增强,并重新载入了这个类。
这样就完成了字节码的动态增强
字节码增强技术的可使用范围就不再局限于 JVM 加载类前了。通过上述
几个类库,我们可以在运行时对 JVM 中的类进行修改并重载了。通过这种手段,可
以做的事情就变得很多了:
● 热部署:不部署服务而对线上服务做修改,可以做打点、增加日志等操作。
● Mock:测试时候对某些服务做 Mock。
● 性能诊断工具:比如 bTrace 就是利用 Instrument,实现无侵入地跟踪一个正
在运行的 JVM,监控到类和方法级别的状态信息。
部分内容摘录自美团字节码增强文章分享