什么是Java Agent
Java Agent是在 JDK1.5 引入的一种可以动态修改 Java 字节码的技术。Java 类编译之后形成字节码被 JVM 执行,在 JVM 在执行这些字节码之前获取这些字节码信息,并且通过字节码转换器对这些字节码进行修改,来完成一些额外的功能。
简单来说,通过使用agent可以实现对java虚拟机进行监控与分析,甚至干预虚拟机的运行。
Instrument
JDK 中提供了一个名为java.lang.instrument的工具包:
该包的原文介绍如下:
简单来说,借助该包,开发者可以构建一个独立于应用程序的代理jar包,通过修改方法的字节码来实现监测在 JVM 上运行的程序。
启动代理的方式
1.命令行界面
java启动时通过添加JVM参数 -javaagent启动代理
-javaagent:<jarpath>[=<options>]
jarpath指定代理的jar包路径
jar包的manifest文件中需要包含属性Premain-Class指定代理类入口
代理类必须实现premain方法,在JVM初始化后会调用premain方法,premain的定义如下
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
JVM将首先寻找第一个,如果没有发现第一个,再寻找第二个
2. 运行时附加
通过attach机制,允许工具附加到正在运行的应用程序,并启动将工具的代理加载到正在运行的应用程序中。
jar包的manifest文件中需要包含属性Agent-Class指定代理类入口
代理类必须实现agentmain方法,agentmain的定义如下
public static void agentmain(String agentArgs, Instrumentation inst)
public static void agentmain(String agentArgs)
3. 与应用程序一起打包
代理可以与应用程序一起被打包到一个可执行的JAR文件中
jar包的manifest文件中需要包含属性Launcher-Agent-Class指定在主程序被调用前将会执行的代理类
manifest属性介绍
Premain-Class: 包含 premain 方法的类(类的全路径名)
Agent-Class: 包含 agentmain 方法的类(类的全路径名)
Launcher-Agent-Class: 包含 agentmain 方法的类(类的全路径名)
Boot-Class-Path: 设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。(可选)
Can-Redefine-Classes: true表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes: true 表示能重转换此代理所需的类,默认值为 false (可选)
Can-Set-Native-Method-Prefix: true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)
修改流程
- JVM启动
- 创建InstrumentationImpl对象
- 加载类
- 监听ClassFileLoadHook事件
- 调用InstrumentationImpl的loadClassAndCallPremain方法,调用指定的premain方法
- premain方法中添加实现ClassFileTransformer的自定义转换器实现修改字节码
- 重新加载修改后的class
demo实现
实现ClassFileTransformer
定义MyTransformer 类,重写transform方法,对业务类方法进行改写
package com.leon;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
public class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
// 检查是否是我们想要修改的类
if (!className.equals("com/leon/BusinessService")) {
return null; //不是我们关心的类,直接返回原始字节码
}
try {
//借助JavaAssist工具,进行字节码插桩
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.leon.BusinessService");
CtMethod personFly = cc.getDeclaredMethod("doBusiness");
//在目标方法前后,插入代码
personFly.insertBefore("System.out.println(\"--- before doBusiness ---\");");
personFly.insertAfter("System.out.println(\"--- after doBusiness ---\");");
return cc.toBytecode();
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
}
定义MyAgent
MyAgent中实现premain方法,加入自定义转换器
package com.leon;
import java.lang.instrument.Instrumentation;
public class MyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("agentArgs : " + agentArgs);
//加入自定义转换器
inst.addTransformer(new MyTransformer(), true);
}
}
MANIFEST.MF
pom中定义自动生成MANIFEST.MF文件,定义Premain-Class入口类
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>com.leon.MyAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
业务类
编写一个业务类
package com.leon;
public class BusinessService {
public void doBusiness () {
try {
System.out.println("doing business"); //模拟业务操作
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
不使用agent时运行
主函数调用业务服务
package com.leon;
public class Main {
public static void main(String[] args) {
BusinessService service = new BusinessService();
while (true) {
service.doBusiness();
}
}
}
打印如下:
只执行业务类
使用agent运行
启动时添加-javaagent参数
打印如下:
成功执行修改后的代码