Java Agent探针技术
网上介绍Java Agent的技术帖子有很多,但是根据某一篇帖子能跑出结果的我是没有找到。如果帖子里介绍的逻辑思想和代码是没有问题的,我们的理解和操作也是没有问题,那么不应该跑不出结果。我搜集结合多篇帖子,加上自己的理解,整理出了这篇Java Agent探针技术。附带执行结果,仅供各位网友参考。如有疑问,欢迎联系交流。
概要
Java Agent直译过来叫做Java代理,还有另一种称呼叫做Java探针。首先说Java Agent是一个jar包,只不过这个jar包不能独立运行,他需要依附到我们的目标JVM进程中。我们来理解一下这两种称呼:
代理:比方说我们需要了解目标JVM的一些运行指标,我们可以通过Java Agent来实现,这样看来它就是一个代理的效果,我们最后拿到的指标是目标JVM,但是我们是通过Java Agent来获取的,对于目标JVM来说,它就像是一个代理;
探针:这个说法我感觉非常形象,JVM一旦跑起来,对于外接来说,它就是一个黑盒。而Java Agent可以像一支针一样插到JVM内部,探到我们想要的东西,并且可以注入东西进去。1
2
3拿IDEA调试器来说,当开启调试功能后,在debugger面板中可以看到当前上下文变量的结构和内容,还可以再watches面板中运行一些简单的代码,比如取值、赋值操作。
Btrace、Arthas这些线上排查问题的工具,比方说接口没有按预期的结果返回,但日志又没有报错。这时我们只要清楚方法的所在包名、类名、方法名等,不用修改部署服务,就能查到调用的参数、返回值、异常等信息。
上面只是说到了探测的功能,而热部署功能就不仅仅是探测这么简单了。热部署的意思是说在不重启服务的情况下,保证最新的代码逻辑在服务生效。当我们修改某个类后,通过Java Agent的instrument机制,把之前的字节码替换为新代码锁对应的字节码。
Java Agent结构
1 | Java Agent最终以jar包的形式存在。主要包含两个部分,一部分是实现代码,一部分是配置文件。配置文件放在META-INF目录下,文件名为MANIFEST.MF |
MANIFEST.MF配置主要包含以下项:1
2
3
4
5
6
7
8
9
10Manifest-Version: 版本号
Created-By: 创作者
Premain-Class: 包含premain方法的类(类的全路径名)main方法运行前代理
Agent-Class: 包含agentmain方法的类(类的全路径名)另一种代理,main开始后可以修改类结构
Boot-CLass-Path:设置引导类加载器搜索的路径列表,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。
路径使用分层URI的路径组件语法。如果该路径以斜杠字符("/")开头,则为绝对路径。相对路径根据代理JAR文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。
如果代理是在VM启动之后某一时刻启动的,则忽略不表示JAR文件的路径。说白了就是agent依赖的类
Can-Redefine-CLasses: true表示能重定义此代理所需的类,默认值为false
Can-Retransform-CLasses: true表示能重转换此代理所需的类,默认值为false
Can-Set-Native-Method-Prefix: true表示能设置此代理所需的本机方法前缀,默认值为false
入口类实现agentmain和premain两个方法即可,方法要实现什么功能就由你的需求决定。
具体代码
javassis-demo:MyCustomAgent.java
MyCustomAgent.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39package com.xh.javassis.demo;
import java.lang.instrument.Instrumentation;
/**
* TODO
*
* @author xiehui1956@gmail.com on
* @version 1.0.0
* @date 2021/10/13
*/
public class MyCustomAgent {
/**
* jvm 参数形式启动,运行此方法
*
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("premain");
inst.addTransformer(new MyTransformer(), true);
}
/**
* 动态 attach 方式启动,运行此方法
*
* @param agentArgs
* @param inst
*/
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("agentmain");
inst.addTransformer(new MyTransformer(), true);
}
public static void main(String[] args) {
System.out.println(121);
}
}
javassis-demo:MyTransformer.java
MyTransformer.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45package com.xh.javassis.demo;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
/**
* TODO
*
* @author xiehui1956@gmail.com on
* @version 1.0.0
* @date 2021/10/13
*/
public class MyTransformer implements ClassFileTransformer {
public MyTransformer() {
System.out.println("进入 MyTransformer");
}
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined
, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.equals("com/xh/my/agent/Person")) {
System.out.println("正在加载类:" + className);
ClassPool classPool = ClassPool.getDefault();
try {
CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod ctMethod = ctClass.getDeclaredMethod("test");
System.out.println("获取方法名称:" + ctMethod.getName());
ctMethod.insertBefore("System.out.println(\" 动态插入的打印语句 \");");
ctMethod.insertAfter("System.out.println($_);");
byte[] transformed = ctClass.toBytecode();
return transformed;
} catch (Exception e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
}
javassis-demo:META-INF/MANIFEST.MF
MANIFEST.MF1
2
3
4
5Manifest-Version: 1.0
Agent-Class: com.xh.javassis.demo.MyCustomAgent
Premain-Class: com.xh.javassis.demo.MyCustomAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
my-agent:RunJvm.java
RunJvm.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package com.xh.my.agent;
import java.util.Scanner;
/**
* TODO
*
* @author xiehui1956@gmail.com on
* @version 1.0.0
* @date 2021/10/13
*/
public class RunJvm {
public static void main(String[] args) {
System.out.println("按数字键 1 调用测试方法");
while (true){
Scanner reader = new Scanner(System.in);
int number = reader.nextInt();
if (1 == number){
Person person = new Person();
person.test();
}
}
}
}
my-agent:Person.java
Person.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package com.xh.my.agent;
/**
* TODO
*
* @author xiehui1956@gmail.com on
* @version 1.0.0
* @date 2021/10/13
*/
public class Person {
public String test() {
System.out.println("say");
return "I'm ok";
}
}
my-agent:AttachAgent.java
AttachAgent.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package com.xh.my.agent;
import com.sun.tools.attach.VirtualMachine;
/**
* TODO
*
* @author xiehui1956@gmail.com on
* @version 1.0.0
* @date 2021/10/14
*/
public class AttachAgent {
public static void main(String[] args) throws Exception {
// 进程id
VirtualMachine virtualMachine = VirtualMachine.attach("68244");
// 代理agent的jar路径
virtualMachine.loadAgent("/Users/brucexie/Documents/study-work/server/study-space/java-agent/javassis-demo/target/javassis-demo-1.0-SNAPSHOT-jar-with-dependencies.jar");
}
}
my-agent:META-INF/MANIFEST.MF
MANIFEST.MF1
2Manifest-Version: 1.0
Main-Class: com.xh.my.agent.RunJvm
执行命令及结果
代码执行
执行步骤:1
2
3
4
51. 编译javassis-demo工程
2. 启动RunJvm
3. 获取RunJvm进程和javassis-demo工程打包出来的jar,设置到AttachAgent
4. 启动AttachAgent
5. RunJvm输入1查看打印结果
执行结果
图1
命令行执行
执行步骤:1
21. 编译my-agent工程
2. 执行以下命令,查看打印结果
执行命令1
java-javaagent:target/javassis-demo-1.0-SNAPSHOT-jar-with-dependencies.jar-jar my-agent.jar
执行结果
图2
代码近期会上传到github,需要的可以联系我。