java探针、字节码插庄都是指的agent技术,agent技术可以构建一个独立于应用程序的代理程序(即为Agent),用来协助监测、运行甚至替换其他JVM上的程序(即替换字节码)。使用它可以实现虚拟机级别的AOP功能。
实现java agent,有两种类型的:
1.运行在主程序之前通过命令加载agent的jar包。
2.运行在主程序之后通过VirtualMachine来加载agent。
1 运行在主程序之前
agent程序需要实现premain方法,premain方法有两种签名,虚拟机会首先尝试运行第一个方法,如果没有才会运行第二个方法。
/**
* @param agentArgs 加载agent时传递给agent的参数
* @param inst 提供检测 Java 编程语言代码所需的服务。检测是向方法中添加字节码,
* 以搜集各种工具所使用的数据
*/
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);
agent作为一个单独的jar包存在,程序打包需要有manifest文件,即jar包里有META-INF目录,目录下有MANIFEST.MF文件,并且需要在文件种指定agent主程序,通过Premain-Class参数指定,如:
Premain-Class: org.example.javaagent.MyJavaAgent
如果是maven程序可在pom.xml种配置如下参数打包时生成MANIFEST.MF文件。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<!-- 参数方式启动agent需要这个 -->
<Premain-Class>
org.example.javaagent.MyJavaAgent
</Premain-Class>
<!-- 启动后附加启动agent需要这个 -->
<Agent-Class>
org.example.javaagent.MyJavaAgent
</Agent-Class>
<!-- 是否可以重新转换类 -->
<Can-Retransform-Classes>
true
</Can-Retransform-Classes>
<!-- 是否可以重新定义类 -->
<Can-Redefine-Classes>
true
</Can-Redefine-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
1.1 加载Agent程序
启动时加入命令
java -javaagent:E:\self\learn\javaagent-demo-1.0-SNAPSHOT.jar=hellojavaagent -jar 主程序.jar org.example.javaagent.App
-javaagent 必须写在-jar之前否则不生效,javaagent的参数:E:\self\learn\javaagent-demo-1.0-SNAPSHOT.jar是编写的agent程序所在的jar包,等号后的hellojavaagent是传给agent的参数。
1.2 具体实现
1.2.1创建应用程序工程
public class OrderService {
public void order() {
System.out.println(Thread.currentThread().getName()+"下了一个订单。。。");
}
}
public class App {
public static void main(String[] args) throws InterruptedException {
for(;;) {
Thread.sleep(500);
new OrderService().order();
}
}
}
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>app</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.26.0-GA</version>
</dependency>
</dependencies>
</project>
1.2.2 创建agent工程
public class MyJavaAgent {
/**
* 这个是静态调用,在启动jvm添加-javaagent参数后会调用
* 运行在main方法之前,虚拟机会先尝试运行这个
* @param agentArgs
* @param inst
* @return: void
* @Author: jt-ape
* @Date: 2021/1/28 23:18
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("调用方法:premain(String agentArgs, Instrumentation inst)");
System.out.println("javaagent 参数:"+agentArgs);
inst.addTransformer(new MyClassFileTransformer(),true);
}
/**
* 运行在main方法之前,不存在上面方法,虚拟机才会运行这个
* @param agentArgs
* @return: void
* @Author: jt-ape
* @Date: 2021/1/28 23:18
*/
public static void premain(String agentArgs) {
}
}
代码解释
Instrumentation:增强器
(1)add/removeTransformer:添加/删除 ClasFileTransformer;
(2)retransformerClasses:指定哪些类,在已加载的情况下,重新进行转换处理,即触发重新加载类定义;对于重新加载的类不能修改旧有的类声明,比如:不能增加属性、不能修改方法声明等;
(3)redefineClasses:指定哪些类,触发重新加载类定义,与上面不同的是不会重新进行转换处理,而是把处理结果 bytecode 直接给 JVM;
(4)getAllLoadedClasses:获取当前已加载的 Class 集合;
(5)getInitiatedClasses:获取由某个特定 ClassLoader 加载的类定义;
(6)getObjectSize:获得一个对象占用的空间大小;
(7)appendToBootstrapClassLoaderSearch/appentToSystemClassLoaderSearch:增加 BootstrapClassLoader/SystemClassLoader 搜索路径;
(8)isNativeMethodPrefixSupported/SetNativeMethodPrefix:判断 JVM 是否支持拦截 Native Method;
通过Instrumentation实例添加了一个转换器 ClassFileTransformer,该转换器用于改变运行时的字节码(class File),这个改变发生在jvm加载这个类之前,对所有的类加载器有效,即jvm加载某个类之前会调用ClassFileTransformer的transform方法。
如果一个transformer不想改变任何代码,那么返回null。否则,应该创建一个新的byte[],不能修改classfileBuffer。
存在多个transformers时,每个transformer会进行链式调用。一个transformer抛出异常,后续的transformer依然会执行,抛异常和返回Null效果相同。
在向Instrumentation#addTransformer添加转换器的时候,会指定canRetransform(默认为false),决定retransformation是否可用。
一旦一个transformer被注册到instrumentation中,每当一个类被定义(ClassLoader.defineClass)或被重新定义(Instrumentation.redefineClasses)时,它都会被调用。
如果retransformation可用,那么一个类被retransformation(Instrumentation.retransformClasses)时,transformer也会被调用。
是否可以被重新转换也受到MANIFEST.MF文件种Can-Retransform-Classes参数的控制,为true则可以。
是否可以被重新定义也受到MANIFEST.MF文件中Can-Redefine-Classes参数的控制,为true则可以。
public class MyClassFileTransformer implements ClassFileTransformer {
/**
* 字节码被加载到虚拟机前会调用这个方法,通过javassist字节码技术改变class
* @param loader 类加载器
* @param className 类的全限定名
* @param classBeingRedefined 如果这是由重新定义或重新转换触发的,则这个类存在重新定义或重新转换,否则为null
* @param protectionDomain 正在定义或重新定义的类的保护域
* @param classfileBuffer 类文件格式的输入字节缓冲区-不能修改
* return byte[] 不想改变任何代码,那么返回null。否则,应该创建一个新的byte[]
*/
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.equals("org/example/javaagent/service/OrderService")) {
System.out.println(className + ":进入ClassFileTransformer");
try {
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(new LoaderClassPath(loader));
CtClass clazz = classPool.makeClass(new ByteArrayInputStream(classfileBuffer), false);
//重写toString方法,将sex属性加入返回结果中。
CtMethod method = clazz.getDeclaredMethod("order");
method.insertBefore("System.out.print(\"我在这个方法前面加点东西哈\");");
return clazz.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
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>javaagent-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.26.0-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<!-- 参数方式启动agent需要这个 -->
<Premain-Class>
org.example.javaagent.MyJavaAgent
</Premain-Class>
<!-- 启动后附加启动agent需要这个 -->
<Agent-Class>
org.example.javaagent.MyJavaAgent
</Agent-Class>
<!-- 是否可以重新转换类 -->
<Can-Retransform-Classes>
true
</Can-Retransform-Classes>
<!-- 是否可以重新定义类 -->
<Can-Redefine-Classes>
true
</Can-Redefine-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>6</source>
<target>6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
1.2.3 测试运行
将agent工程打包放到指定目录,运行App的main方法
可以看到通过agent改变了OrderService的代码。
2 运行在主程序之后
这种方式agent程序需要实现agentmain方法,agentmain方法有两种签名,虚拟机会首先尝试运行第一个方法,如果没有才会运行第二个方法。
/**
* @param agentArgs 加载agent时传递给agent的参数
* @param inst 提供检测 Java 编程语言代码所需的服务。检测是向方法中添加字节码,
* 以搜集各种工具所使用的数据
*/
public static void agentmain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs);
agent程序打包需要有manifest文件,即jar包里有META-INF目录,目录下有MANIFEST.MF文件,并且需要在文件种指定agent主程序,通过Agent-Class参数指定,如:
Agent-Class: org.example.javaagent.MyJavaAgent
如果maven程序,配置如上。
2.1 加载Agent程序
这种方式加载Agent程序需要通过VirtualMachine类attach到主程序的jvm上执行,VirtualMachine在tools.jar里所以需要引入tools.jar的依赖,tools.jar的路径:JAVA_HOME\lib\tools.jar
VirtualMachine attach = VirtualMachine.attach(jvm);
// 传入agent程序的jar包路径,第二个参数时传给agent程序的参数
VirtualMachine attach.loadAgent("E:\\self\\learn\\javaagent-demo-1.0-SNAPSHOT.jar","agentagent");
2.2 具体实现
2.2.1 创建应用程序工程
同1.2.1章节
2.2.2 创建agent工程
同1.2.2章节,需要修改MyJavaAgent.java
public class MyJavaAgent {
/**
* 动态调用,虚拟机启动之后执行
* 虚拟机会先尝试运行这个
* @param agentArgs
* @param inst
* @return: void
* @Author: jt-ape
* @Date: 2021/1/28 23:54
*/
public static void agentmain(String agentArgs, Instrumentation inst){
System.out.println("调用方法:agentmain(String agentArgs, Instrumentation inst)");
System.out.println("javaagent 参数:"+agentArgs);
// 添加转换器,并设置为可以重新转换
inst.addTransformer(new MyClassFileTransformer(),true);
Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class allLoadedClass : allLoadedClasses) {
if (allLoadedClass.getName().equals("org.example.javaagent.service.OrderService")) {
try {
// 重新转换目标类,由于目标类已经加载了,所以需要重新转换,才会调用用ClassFileTransformer的transform方法
inst.retransformClasses(allLoadedClass);
break;
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
}
}
/**
* 不存在上面的agentmain方法才会调用本方法
* @param agentArgs
* @return: void
* @Author: jt-ape
* @Date: 2021/1/30 14:27
*/
public static void agentmain(String agentArgs){
}
}
由于目标程序已经启动类已经加载,所以添加转换器后,还需要调用Instrumentation的retransformClasses方法重新转换目标类。否则将不会调用转换器ClassFileTransformer的transform方法。
同样需要将javaagent-demo工程打包放到指定目录
2.2.3 创建工程使目标jvm加载agent程序
public class Attach {
public static void main(String[] args) throws Exception {
// 查找所有JVM经常
List<VirtualMachineDescriptor> attachs = VirtualMachine.list();
attachs.stream().forEach(jvm -> {
System.out.println(jvm.displayName()+":"+ jvm.id());
});
System.out.println("请输入jvm进程ID");
Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine();
for(VirtualMachineDescriptor jvm:attachs){
if(s.equals(jvm.id())) {
VirtualMachine attach = null;
try {
// 获取目标jvm
attach = VirtualMachine.attach(jvm);
// 加载agent,第二个参数为传递给agent的参数
attach.loadAgent("E:\\self\\learn\\javaagent-demo-1.0-SNAPSHOT.jar","agentagent");
attach.detach();
} catch (AttachNotSupportedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (AgentLoadException e) {
e.printStackTrace();
} catch (AgentInitializationException e) {
e.printStackTrace();
}
break;
}
}
}
}
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>javaagent-dync</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>jdk.tools</groupId>
<artifactId>jdk.tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>D:\jdk\jdk1.8.0_211-64\lib\tools.jar</systemPath>
</dependency>
</dependencies>
</project>
2.2.4 测试运行
首先运行主程序App的main方法
运行Attach.java,输入App的进程ID
查看App的控制台结果
可以看到执行了agent程序。
参考链接:
https://www.jianshu.com/p/f5efc53ced5d
https://www.cnblogs.com/jhxxb/p/11570503.html
https://blog.csdn.net/ljz2016/article/details/83309599/