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 效果已經出來了