java agent是基于java instrument实现,instrument的底层实现依赖于JVMTI,也就是JVM Tool Interface。
代码准备
本次实验在工程中新增了3个module,4个类
Module | Class | Describe |
---|---|---|
learn-main | Handle | 代理实验类 |
learn-main | HandleMain | 实验应用main class |
learn-module | AgentLauncher | agent Premain class |
learn-agent-main | AgentmainAttachMain | attach VirtualMachine main class |
java代码如下
Handle:
public class Handle {
public void invoke() {
System.out.println("Handle.invoke....");
}
public void call() {
System.out.println("Handle.call....");
}
public String hello(){
return "hello ";
}
}
HandleMain:
import java.util.Objects;
import java.util.Scanner;
import java.util.concurrent.Callable;
public class HandleMain {
public static void main(String[] args) throws Exception {
Handle handle = new Handle();
Scanner scanner = new Scanner(System.in);
while (true) {
String txt = scanner.next();
if (Objects.equals("exit", txt)) {
break;
}
handle.invoke();
handle.call();
System.out.println("Handle.hello():" + handle.hello());
}
}
import javassist.*;
import javassist.bytecode.CodeAttribute;
import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.Exception;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.security.ProtectionDomain;
import java.util.Objects;
import java.util.concurrent.Callable;
public class AgentLauncher {
// 启动时接入
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("premain.agentArgs: " + agentArgs);
handle(agentArgs, inst);
}
// 运行时接入(VirtualMachine attach)
public static void agentmain(String agentArgs, Instrumentation inst) throws Exception {
System.out.println("agentmain.agentArgs: " + agentArgs);
handle(agentArgs, inst);
System.out.println("isRetransformClassesSupported:" + inst.isRetransformClassesSupported());
System.out.println("isRedefineClassesSupported:" + inst.isRedefineClassesSupported());
// 重新transform已经加载的类
inst.retransformClasses(
Class.forName("com.saleson.learn.java.agent.Handle")
);
}
private static void handle(String agentArgs, Instrumentation inst) {
System.out.println("agentArgs: " + agentArgs);
if (StringUtils.isBlank(agentArgs)) {
throw new NullPointerException("agentArgs is null.");
}
String[] args = agentArgs.split(",");
if (args.length > 3) {
throw new IllegalArgumentException("agentArgs is illegal.");
}
String mode, className = null, methodName = null;
if (args.length == 1) {
mode = args[0];
} else if (args.length == 2) {
mode = args[0];
className = args[1];
} else {
mode = args[0];
className = args[1];
methodName = args[2];
}
javassistTransform(inst, className, methodName);
}
private static void javassistTransform(Instrumentation instrumentation, String className, String methodName) {
instrumentation.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader,
String cn, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
if (Objects.equals(className.replaceAll("\\.", "/"), cn)) {
try {
return javassistTransform(loader, classfileBuffer, methodName);
} catch (Throwable t) {
throw new IllegalClassFormatException(t.getClass().getName() + " -> " + t.getMessage());
}
}
return classfileBuffer;
}
}, true);
}
private static byte[] javassistTransform(ClassLoader loader, byte[] classfileBuffer, String methodName) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.appendClassPath(new LoaderClassPath(loader));
CtClass ctClass = pool.makeClass(new ByteArrayInputStream(classfileBuffer));
if ("*".equals(methodName) || StringUtils.isBlank(methodName)) {
CtMethod[] ctmethods = ctClass.getMethods();
for (CtMethod ctMethod : ctmethods) {
CodeAttribute ca = ctMethod.getMethodInfo2().getCodeAttribute();
if (ca == null) {
continue;
}
if (!ctMethod.isEmpty()) {
ctMethod.insertBefore("System.out.println(\"hello Im agent : " + ctMethod.getName() + "\");");
}
}
return ctClass.toBytecode();
}
CtMethod ctMethod = ctClass.getDeclaredMethod(methodName);
ctMethod.insertBefore("System.out.println(\"hello Im agent : " + ctMethod.getName() + "\");");
return ctClass.toBytecode();
}
}
AgentmainAttachMain:
import com.saleson.learn.util.Utils;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class AgentmainAttachMain {
public static void main(String[] args) {
// check args
if (args.length == 2 && !Utils.isBlank(args[0]) && !Utils.isBlank(args[1])) {
unsafeExec(() -> attachAgent(args[0], args[1]));
return;
} else if (args.length != 3 || Utils.isBlank(args[0]) || Utils.isBlank(args[1]) || Utils.isBlank(args[2])) {
throw new IllegalArgumentException("illegal args");
}
unsafeExec(() -> attachAgent(args[0], args[1], args[2]));
}
// 从表列表查找运行HandleMain的jvm pid
private static void attachAgent(String agentJarPath, String cfg) throws Exception {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor descriptor : list) {
if (descriptor.displayName().endsWith("HandleMain")) {
VirtualMachine virtualMachine = VirtualMachine.attach(descriptor.id());
virtualMachine.loadAgent(agentJarPath, cfg);
virtualMachine.detach();
}
}
}
// 指定jvm pid 加载Agent
private static void attachAgent(String targetJvmPid, String agentJarPath, String cfg) throws Exception {
VirtualMachine vmObj = VirtualMachine.attach(targetJvmPid);
if (vmObj != null) {
vmObj.loadAgent(agentJarPath, cfg);
vmObj.detach();
}
}
/**
* 获取异常的原因描述
*
* @param t 异常
* @return 异常原因
*/
public static String getCauseMessage(Throwable t) {
if (null != t.getCause()) {
return getCauseMessage(t.getCause());
}
return t.getMessage();
}
interface Exec {
void exec() throws Throwable;
}
private static void unsafeExec(Exec exec) {
try {
exec.exec();
} catch (Throwable t) {
t.printStackTrace();
System.err.println("load jvm failed : " + getCauseMessage(t));
System.exit(-1);
}
}
}
各module的 pom.xml
learn-module/pom.xml
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<!-- maven 打包插件 打原始jar包 第三方依赖打入jar包中-->
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<!-- <Class-Path>.</Class-Path>-->
<Premain-Class>com.saleson.learn.agent.instrument.AgentLauncher</Premain-Class>
<Agent-Class>com.saleson.learn.agent.instrument.AgentLauncher</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id> <!-- this is used for inheritance merges -->
<phase>package</phase> <!-- 指定在打包节点执行jar包合并操作 -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
learn-agent-main/pom.xml
<properties>
<tools-jar>${java.home}/../lib/tools.jar</tools-jar>
</properties>
<dependencies>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${tools-jar}</systemPath>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<manifest>
<mainClass>com.saleson.learn.agent.AgentmainAttachMain</mainClass>
</manifest>
<manifestEntries>
<Class-Path>. ${java.home}/../lib/tools.jar</Class-Path>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
在项目下执行mvn clean install 命令打包编译。
agent 接入的两种方式
1、启动时接入
启动脚本的格式
java -javaagent:{agent.jar 路径}[={agent 参数}] {mainClass}
启动脚本:
$ java -javaagent:../../learn-module/target/learn-module-1.0-SNAPSHOT-jar-with-dependencies.jar=com.saleson.learn.java.agent.Handle,* -jar learn-main-1.0-SNAPSHOT.jar
2、运行时接入
先启动运行HandleMain类
$ java -jar learn-main-1.0-SNAPSHOT.jar
然后通过ps命令查看jvm 的pid
$ ps -aux | grep learn-main
saleson 57375 0.0 0.0 4268776 828 s004 S+ 6:41下午 0:00.00 grep learn-main
saleson 57340 0.0 0.2 10035276 30568 s003 S+ 6:41下午 0:00.17 java -jar learn-main-1.0-SNAPSHOT.jar
运行AgentmainAttachMain类,完成agent运行时的接入
运行脚本格式:
java -jar {attachMain.jar} {pid} {agent.jar绝对路径} {agent参数}
运行脚本:
$ java -jar learn-agent-main-1.0-SNAPSHOT.jar 57340 /Users/saleson/IdeaProjects/learn/learn-module/target/learn-module-1.0-SNAPSHOT-jar-with-dependencies.jar com.saleson.learn.java.agent.Handle,*
运行后:
Idea调试
本地module调试
添加Debug Configuration
Field | Content |
---|---|
Main class | com.saleson.learn.java.agent.HandleMain |
VM options | -javaagent:learn-module/target/learn-module-1.0-SNAPSHOT.jar=com.saleson.learn.java.agent.Handle,* |
在AgentLauncher.premain()方法中设置断点,运行Debug
本地lib调试
在另一个项目中新增Main class和实验所用的类:
EHandle.java
EHandleMain.java
将learn-module-1.0-SNAPSHOT.jar以lib的形式添加到Idea工程中,操作路径:
File > Project Structure > Project Settings > Libraries
添加Debug Configuration
Field | Content |
---|---|
Main class | com.saleson.java.agent.EHandleMain |
VM options | -javaagent:/Users/saleson/IdeaProjects/learn/learn-module/target/learn-module-1.0-SNAPSHOT.jar=com.saleson.learn.java.agent.Handle,* |
在AgentLauncher.premain()方法中设置断点,运行Debug
采用jdwp进行调试
接下来使用运行时接入的方式来实验使用jdwp进行远程调试
添加jdwp参数运行learn-main-1.0-SNAPSHOT.jar
$ java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5050 -jar learn-main-1.0-SNAPSHOT.jar
在AgentLauncher.agentmain()方法中设置断点,再运行AgentmainAttachMain类(不要忘记pid等参数)
在learn工程(agent源码)中添加Remote Debug Configuration
运行learn-main-1.0-SNAPSHOT.jar的命令窗口将日志打印出来
参考
Java程序员必知:深入理解Instrument
本地调试和远程调试javaagent的premain方法
Java Agent(二)Attach机制及运行时加载agent
【精通 JVM 原理】浅析 JavaAgent & Instrumentation 机制
Java Agent基本简介和使用
javaagent使用指
Javaagent使用指南