Java Agent探针技术

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
10
Manifest-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.java

1
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
package 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.java

1
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
45
package 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");
}

@Override
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.MF

1
2
3
4
5
Manifest-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.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package 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.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package 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.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package 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.MF

1
2
Manifest-Version: 1.0
Main-Class: com.xh.my.agent.RunJvm

执行命令及结果

代码执行

执行步骤:

1
2
3
4
5
1. 编译javassis-demo工程
2. 启动RunJvm
3. 获取RunJvm进程和javassis-demo工程打包出来的jar,设置到AttachAgent
4. 启动AttachAgent
5. RunJvm输入1查看打印结果

执行结果
PerpetualCache图1

命令行执行

执行步骤:

1
2
1. 编译my-agent工程
2. 执行以下命令,查看打印结果

执行命令

1
java-javaagent:target/javassis-demo-1.0-SNAPSHOT-jar-with-dependencies.jar-jar my-agent.jar

执行结果
PerpetualCache图2

代码近期会上传到github,需要的可以联系我。