1 背景
近日在给公司同事分享Arthas 工具使用时候,被它强悍的功能震撼到了就好奇研究了下它的原理及底层实现,其实它是通过JAVA agent 来实现的,也就深入地学习了一下Java agent 技术觉得蛮有意思,也得到了一些启发,通过Java agent 我们可以做到很多意想不到的事情,在我们日常开发过程中你可能已经无形地接触到了它,例如一些APM 工具如:pinpoint,skywalking,cat, arthas,BTrace... 甚至我们的IDEA debug工具无时无刻都有java agent 的身影...如果你是一个Java 开发者,你一定很有必要去研究一下它。
2 Java agent 介绍
Java agent 又名Java 探针活Java 代理,也有人称它为 “插桩”,说的都是一个意思,只是大家给它起了一个比较通俗易懂的名字而已。“探针” 这个说法我感觉非常形象,JVM 一旦跑起来,对于外界来说,它就是一个黑盒子。而 Java Agent 可以像一支针一样插到 JVM 内部,探到我们想要的东西,并且可以注入东西进去。就像我们生病时去医院看医生,医生怎么诊断你身体的健康状况呢,这时候往往借助一个听诊器,把它放在你的胸口去听诊,就能大概了解你的健康状态了,怎么样,是不是很形象? 那又如何理解代理呢?比方说我们需要了解目标 JVM 的一些运行指标,我们可以通过 Java Agent 来实现,这样看来它就是一个代理的效果,我们最后拿到的指标是目标 JVM ,但是我们是通过 Java Agent 来获取的,对于目标 JVM 来说,它就像是一个代理。
Java agent本质上可以理解为一个插件,该插件就是一个精心提供的jar包,这个jar包通过JVMTI(JVM Tool Interface)完成加载,最终借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成对目标代码的修改。
2.1 java agent 技术的主要功能
java agent技术的主要功能如下:
- 可以在加载java文件之前做拦截把字节码做修改
- 可以在运行期将已经加载的类的字节码做变更
- 还有其他的一些小众的功能如:
获取所有已经被加载过的类
获取所有已经被初始化过了的类
获取某个对象的大小
将某个jar加入到bootstrapclasspath里作为高优先级被bootstrapClassloader加载
将某个jar加入到classpath里供AppClassloard去加载
2.2 java Instrumentation API
通过java agent技术进行类的字节码修改最主要使用的就是Java Instrumentation API。下面将介绍如何使用Java Instrumentation API进行字节码修改。
有两种方式拿到Instrumentation对象:
- 在jvm启动时指定agent,Instrumentation对象会通过agent的premain方法传递。它是Java 5 开始提供的方式。
- 在jvm启动后通过jvm提供的机制加载agent,Instrumentation对象会通过agent的agentmain方法传递。它是java6 开始提供的方式
Java Agent支持目标JVM启动时加载,也支持在目标JVM运行时加载,这两种不同的加载模式会使用不同的入口函数,如果需要在目标JVM启动的同时加载Agent,那么可以选择实现下面的方法:
[1] public static void premain(String agentArgs, Instrumentation inst);
[2] public static void premain(String agentArgs);
JVM将首先寻找[1],如果没有发现[1],再寻找[2]。如果希望在目标JVM运行时加载Agent,则需要实现下面的方法:
[1] public static void agentmain(String agentArgs, Instrumentation inst);
[2] public static void agentmain(String agentArgs);
这两组方法的第一个参数AgentArgs是随同 “–javaagent”一起传入的程序参数,如果这个字符串代表了多个参数,就需要自己解析这些参数。inst是Instrumentation类型的对象,是JVM自动传入的,我们可以拿这个参数进行类增强等操作。
3 两个小demo
下面我将用两种方式分别对premain 和 agentmain 两种方式介绍。
简单起见,我就对我的目标程序做一个耗时统计,在方法体前后声明两个变量并计算耗时。
3.1 premain 的方式
这里隐藏了一些公司的敏感的信息用“xxx” 代替
我的pom文件
<plugin>
<groupId>org.Apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>com.xxx.xxx.xxx.PreMainAgent</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
package com.xxx.xxx.capital;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
/**
* Created on 2021/9/5 10:07 下午. <br/>
* Description: <br/>
* description for class template.
*
* @author danniel.l
*/
public class PreMainAgent {
private static Instrumentation instrumentation;
public static void premain(String agentArgs, Instrumentation inst) {
instrumentation = inst;
System.err.println("我在main启动之前启动");
inst.addTransformer(new MyTransformer());
}
}
package com.xxx.xxx.capital;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
/**
* Created on 2021/9/5 11:49 下午. <br/>
* Description: <br/>
* description for class template.
*
* @author danniel.l
*/
public class MyTransformer implements ClassFileTransformer {
final static String prefix = "nlong startTime = System.currentTimeMillis();n";
final static String postfix = "nlong endTime = System.currentTimeMillis();n";
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer){
if (!className.startsWith("com/xxx/xxx/xxx/agenttest")) {
return null;
}
className = className.replace("/", ".");
CtClass ctclass = null;
try {
ctclass = ClassPool.getDefault().get(className);// 使用全称,用于取得字节码类<使用javassist>
for(CtMethod ctMethod : ctclass.getDeclaredMethods()){
String methodName = ctMethod.getName();
String newMethodName = methodName + "$old";// 新定义一个方法叫做比如sayHello$old
ctMethod.setName(newMethodName);// 将原来的方法名字修改
// 创建新的方法,复制原来的方法,名字为原来的名字
CtMethod newMethod = CtNewMethod.copy(ctMethod, methodName, ctclass, null);
// 构建新的方法体
StringBuilder bodyStr = new StringBuilder();
bodyStr.append("{");
bodyStr.append("System.out.println("==============Enter Method: " + className + "." + methodName + " ==============");");
bodyStr.append(prefix);
bodyStr.append(newMethodName + "($$);n");// 调用原有代码,类似于method();($$)表示所有的参数
bodyStr.append(postfix);
bodyStr.append("System.out.println("==============Exit Method: " + className + "." + methodName + " Cost:" +(endTime - startTime) +"ms " + "===");");
bodyStr.append("}");
newMethod.setBody(bodyStr.toString());// 替换新方法
ctclass.addMethod(newMethod);// 增加新方法
}
return ctclass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
目标增强的类
package com.xxx.xxx.xxx.agenttest;
import org.springframework.web.bind.annotation.RestController;
/**
* Created on 2021/9/6 12:20 上午. <br/>
* Description: <br/>
* description for class template.
*
* @author danniel.l
*/
@RestController
public class AgentTest {
public void test1(){
System.out.println("this is test1");
}
public void test2(){
System.out.println("this is test2");
}
}
启动时增加如下参数
-javaagent:/Users/user/IdeaProjects/bc/xxx-xxx/xxx-xxx-web/target/xxx-xxx-web-2.0.0-SNAPSHOT.jar
3.2 agentmain 的方式
3.2.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>agent-test</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.24.0-GA</version>
</dependency>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>/Library/Java/JavaVirtualmachines/jdk1.8.0_291.jdk/Contents/Home/lib/tools.jar</systemPath>
</dependency>
</dependencies>
<build>
<finalName>agentmain-test</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Agent-Class>agent.test.myagent.MyAgentTransformer</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.2.2
package agent.test.target;
/**
* Created on 2021/9/7 3:24 下午. <br/>
* Description: <br/>
* 需要增强的目标应用程序入口
*
* @author danniel.l
*/
public class TargetMain {
public static void main(String[] args) {
TargetTest targetTest = new TargetTest();
while (true) {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
targetTest.test();
targetTest.method();
}
}
}
package agent.test.target;
/**
* Created on 2021/9/7 3:25 下午. <br/>
* Description: <br/>
* 需要增强的目标应用程序类
*
* @author danniel.l
*/
public class TargetTest {
public void test(){
System.out.println("this is a test!");
}
public void method() {
System.err.println("this is a method!");
}
}
package agent.test.myagent;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
/**
* Created on 2021/9/7 3:44 下午. <br/>
* Description: <br/>
* Agent 程序入口.
*
* @author danniel.l
*/
public class MyAgentMain {
public static void main(String[] args) throws Exception {
// 需要增强的目标程序的名称,可根据args 参数传递进来
String targetApplicationName = "TargetMain";
// 需要增强的目标类,可根据args 参数传递进来
String targetClassName = "agent.test.target.TargetTest";
// 获取本机已启动的应用程序名称集合列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
if (vmd.displayName().endsWith(targetApplicationName)) {
// 通过VirtualMachine.attach() 附着上目标程序
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
// 把探针代理程序插桩到目标程序里去。。
virtualMachine.loadAgent("/Users/user/IdeaProjects/test/agent-test/target/agentmain-test.jar", targetClassName);
System.out.println("Attached target application successfully!");
virtualMachine.detach();
}
}
}
}
package agent.test.myagent;
import agent.test.target.TargetTest;
import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
/**
* Created on 2021/9/5 10:30 下午. <br/>
* Description: <br/>
* Agent 增强逻辑处理.
*
* @author danniel.l
*/
public class MyAgentTransformer {
public static void agentmain(String agentArgs, Instrumentation instrumentation) throws UnmodifiableClassException {
System.out.println("agentmain starts...."+ agentArgs);
instrumentation.addTransformer(new Transformer(agentArgs), true);
// 允许修改TargetTest类
instrumentation.retransformClasses(TargetTest.class);
}
private static class Transformer implements ClassFileTransformer {
private final String targetClassName;
public Transformer(String targetClassName) {
this.targetClassName = targetClassName;
}
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
if (className == null) {
return null;
}
className = className.replace("/", ".");
if (!className.equals(targetClassName)) {
return null;
}
System.out.println("transform className=: " + className);
ClassPool classPool = ClassPool.getDefault();
// 将要修改的类的classpath加入到ClassPool中,否则找不到该类
classPool.appendClassPath(new LoaderClassPath(loader));
try {
CtClass ctClass = classPool.get(className);
for (CtMethod ctMethod : ctClass.getDeclaredMethods()) {
if (Modifier.isPublic(ctMethod.getModifiers()) && !ctMethod.getName().equals("test")) {
// 修改字节码
ctMethod.addLocalVariable("begin", CtClass.longType);
ctMethod.addLocalVariable("end", CtClass.longType);
ctMethod.insertBefore("begin = System.currentTimeMillis();");
ctMethod.insertAfter("Thread.sleep(1000L);");
ctMethod.insertAfter("end = System.currentTimeMillis();");
ctMethod.insertAfter("System.out.println("方法" + ctMethod.getName() + "耗时"+ (end - begin) +"ms");");
}
}
ctClass.detach();
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}
}
整个目录结构如下
先执行TargetMain 这个目标程序启动效果如下:
再启动MyAgentMain 代理程序
最后再回到TargetMain 效果已经出来了