概述
Agent分为两种:
1、在主程序之前运行的Agent,
2、在主程序之后运行的Agent(前者的升级版,1.6以后提供)。
主程序运行前的Agent
背景
javaAgent实现打印方法耗时
添加Maven依赖
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
</dependencies>
被增强的代码
新增一个java module
代码
// 启动类
public class APPMain {
public static void main(String[] args) {
System.out.println("APP 启动!!!");
AppInit.init();
}
}
// 模拟的应用初始化的类
public class AppInit {
public static void init() {
try {
System.out.println("APP初始化中...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
javaAgent增强实现类
代码
新增一个java module
public class RunTimeAgent {
public static void premain(String arg, Instrumentation instrumentation) {
System.out.println("探针启动!!!");
System.out.println("探针传入参数:" + arg);
instrumentation.addTransformer(new RunTimeTransformer());
}
}
RunTimeTransformer
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class RunTimeTransformer implements ClassFileTransformer {
private static final String INJECTED_CLASS = "com.zhj.test.init.AppInit";
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
String realClassName = className.replace("/", ".");
if (realClassName.equals(INJECTED_CLASS)) {
System.out.println("拦截到的类名:" + realClassName);
CtClass ctClass;
try {
// 使用javassist,获取字节码类
ClassPool classPool = ClassPool.getDefault();
ctClass = classPool.get(realClassName);
// 得到该类所有的方法实例,也可选择方法,进行增强
CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
for (CtMethod method : declaredMethods) {
System.out.println(method.getName() + "方法被拦截");
method.addLocalVariable("time", CtClass.longType);
method.insertBefore("System.out.println("---开始执行---");");
method.insertBefore("time = System.currentTimeMillis();");
method.insertAfter("System.out.println("---结束执行---");");
method.insertAfter("System.out.println("运行耗时: " + (System.currentTimeMillis() - time));");
}
return ctClass.toBytecode();
} catch (Throwable e) { //这里要用Throwable,不要用Exception
System.out.println(e.getMessage());
e.printStackTrace();
}
}
return classfileBuffer;
}
}
编译插件配置
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<!-- 指定maven编译的jdk版本。若不指定,maven3默认用jdk 1.5 maven2默认用jdk1.3 -->
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Menifest-Version>1.0</Menifest-Version>
<Premain-Class>com.zhj.agent.RunTimeAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
会在resources下创建META-INF/MANIFEST.MF文件
Manifest-Version: 1.0
Premain-Class: com.zhj.agent.RunTimeAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
件MANIFEST.MF参数说明:
| 参数 | 说明 |
|---|---|
| Manifest-Version | 文件版本 |
| Premain-Class | 包含 premain 方法的类(类的全路径名)main方法运行前代理 |
| Agent-Class | 包含 agentmain 方法的类(类的全路径名)main开始后可以修改类结构 |
| Boot-Class-Path | 设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。(可选) |
| Can-Redefine-Classes true | 表示能重定义此代理所需的类,默认值为 false(可选) |
| Can-Retransform-Classes true | 表示能重转换此代理所需的类,默认值为 false (可选) |
| Can-Set-Native-Method-Prefix true | 表示能设置此代理所需的本机方法前缀,默认值为 false(可选) |
运行测试程序
配置运行程序jvm参数
执行结果
探针启动!!!
探针传入参数:hello
APP 启动!!!
拦截到的类名:com.jc.agent.AppInit
init方法被拦截
---开始执行---
APP初始化中...
---结束执行---
运行耗时: 1005
主程序之后运行的Agent
概述
在案例一的基础上,我们如何实现在程序运行时去完成动态修改字节码呢?
动态修改字节码需要依赖于JDK为我们提供的JVM工具,也就是上边我们提到的Attach,通过它去加载我们的代理程序。
首先我们在代理程序中需要定义一个名字为agentmain的方法,它可以和上边我们提到的premain是一样的内容,也可根据agentmain的特性进行自己逻辑的开发。
/**
* agentmain 在 main 函数开始运行后才启动(依赖于Attach机制)
*/
public class RunTimeAgent {
public static void agentmain(String arg, Instrumentation instrumentation) {
System.out.println("agentmain探针启动!!!");
System.out.println("agentmain探针传入参数:" + arg);
instrumentation.addTransformer(new RunTimeTransformer());
}
}
然后就是我们需要将配置中设置,让其知道我们的探针需要加载这个类,在maven中设置如下,如果是META-INF/MANIFEST.MF文件同理。
<!--<Premain-Class>com.zhj.agent.agentmain.RunTimeAgent</Premain-Class>-->
<Agent-Class>com.zhj.agent.agentmain.RunTimeAgent</Agent-Class>
这样其实我们的探针就已经改造好了,然后我们需要在目标程序的main方法中植入一些代码,使其可以读取到我们的代理程序,这样我们也无需去配置JVM的参数,就可以加载探针程序。
public class APPMain {
public static void main(String[] args) {
System.out.println("APP 启动!!!");
for (VirtualMachineDescriptor vmd : VirtualMachine.list()) {
// 指定的VM才可以被代理
if (true) {
System.out.println("该VM为指定代理的VM");
System.out.println(vmd.displayName());
try {
VirtualMachine vm = VirtualMachine.attach(vmd.id());
vm.loadAgent("D:/Code/java/idea_project/agent-test/runtime-agent/target/runtime-agent-1.0-SNAPSHOT.jar=hello");
vm.detach();
} catch (Exception e) {
e.printStackTrace();
}
}
}
AppInit.init();
}
}
其中VirtualMachine是JDK工具包下的类,如果系统环境变量没有配置,需要自己在Maven中引入本地文件。
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>D:/Software/java_dev/java_jdk/lib/tools.jar</systemPath>
</dependency>
获取 Instrumentation 对象
可以通过bytebuddy 获取 Instrumentation 对象
参考:www.jianshu.com/p/f55bfa7d4…
动态加载javaagent主要是在程序运行过程中通过 ByteBuddyAgent.install(); 获得 Instrumentation inst 对象,而不是在启动的时候通过加入-javaagent来获得 Instrumentation inst 对象。
public class InstrumentationUtil {
private static Instrumentation obj = null;
public static synchronized Instrumentation INSTANCE() {
if (null == obj) {
obj = ByteBuddyAgent.install();
}
return obj;
}
}