JavaAgent
简单介绍
Java Agent是Java平台提供的一个强大工具,它可以在运行时修改或增强Java应用程序的行为。是在JDK1.5以后引入的,它能够在不影响正常编译的情况下修改字节码,相当于是在main方法执行之前的拦截器,也叫premain,也就是会先执行premain方法然后再执行main方法。
使用场景如下:
代码注入增强:允许在程序运行时对字节码进行操作,可以实现功能的增强。
性能监控调优:可以监控应用程序方法执行时间、调用次数,类加载的一些信息进行性能检测,以及对一些问题的定位分析,比如一些性能监控和诊断工具如Pinpoint、Skywalking、Zipkin、Arthas等。
日志记录审计:Java Agent可以在方法执行前后记录方法的调用信息,包括方法名、参数、返回值等,动态记录应用程序的运行日志。
具体实现
main & 基本环境搭建 (用于主要测试)
这里我们需要对JavaAgent
所需要的基础环境
进行一个简单的搭建, 方便下面的学习与理解.
首先创建一个MyMainApp
基于Maven
的项目, 该项目pom.xml
:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<!-- 后续使用 Maven 进行打包即可 -->
<configuration>
<archive>
<manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
</dependency>
</dependencies>
在这里我们配置maven-jar-plugin
的manifestFile
属性, 配置该属性则是为了对我们的MANIFEST.MF
文件进行指明.
随后创建resources/META-INF/MANIFEST.MF
文件中进行指明JAR包
运行时所运行的主类:
Main-Class: com.heihu577.MyApp (对应了 MyApp::main 方法, 这里需要多打一个换行符)
随后我们创建一个com.heihu577.MyApp
类, 内容如下:
public class MyApp {
public static void main(String[] args) throws InterruptedException {
System.out.println("[MyApp] Hello, My name is Heihu577~");
try {
// 获取虚拟机进程PID
String jvmPid = ManagementFactory.getRuntimeMXBean().getName();
System.out.println("[MyApp] 当前Java进程的PID是: " + jvmPid);
} catch (Exception e) {
e.printStackTrace();
}
while (true) { // 放置程序提前中止, 使用 while 循环进行保持进程
}
}
}
随后使用maven打包插件将其打包成jar, 命名为main.jar
文件, 并且运行结果如下:
PS C:UsersAdministratorDesktop> java -jar main.jar
[MyApp] Hello, My name is Heihu577~
[MyApp] 当前Java进程的PID是: 23356@heihubook
这是一个最简单的Java程序案例, 只是简单的将程序进行打包, 随后执行即可.
MANIFEST.MF
我们需要先理解MANIFEST.MF
文件是用来做什么的: MANIFEST.MF
文件是 Java 归档文件(如 JAR 文件)中的一个特殊文件,用于存储关于归档文件及其内容的元数据信息。这些元数据信息可以包括主类、类路径、扩展列表、安全证书等。以下是 MANIFEST.MF
文件的一些主要用途.
Implementation-Version: 1.0.0 // 用于记录 JAR 文件的版本信息。
Main-Class: com.example.MyApp // 用于指定当用户运行 JAR 文件时,哪个类的 `main` 方法将被调用。
Class-Path: lib/dependency1.jar lib/dependency2.jar // 用于指定其他依赖的 JAR 文件或类路径。
Extension-List: extension1 extension2 // 用于指定 JAR 文件依赖的扩展。
extension1-Extension-Name: com.example.extension1
extension2-Extension-Name: com.example.extension2
而我们刚刚的案例是在Maven
的打包插件中进行引入了MANIFEST.MF
文件, 而其实Maven
的打包插件也可以通过直接配置参数的方式进行打包, 如下:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<!--<manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>-->
<manifest> <!-- 使用这种方式进行打包, 无需指明 MANIFEST.MF 文件 -->
<mainClass>com.heihu577.MyApp</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
这两种方式是一样的, 只是写法不同而已. 建议使用/META-INF/MANIFEST.MF
文件的方式, 因为IDEA
工具打包是通过这个文件进行打包的.
建议阅读: https://www.cnblogs.com/leondryu/p/18079568
premain & agentmain
这里将premain & agentmain
一起说明, 其原因则是新创建一个Java项目, 其中包含了这两个功能模块.
环境搭建 (用于理解)
这里我们如同上面步骤一样, 先进行一个项目所需要的环境进行搭建. pom.xml:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
</plugins>
</build>
随后我们创建MyPremainTest
类, 其内容如下:
public class MyPremainTest {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("[MyPremainTest::premain] 你传递过来的参数: " + agentArgs);
}
public static void premain(String agentArgs) {
System.out.println("[MyPremainTest::premain] 你传递过来的参数: " + agentArgs);
// 如果存在 public static void premain(String agentArgs, Instrumentation inst), 则不会进入该方法
}
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("[MyPremainTest::agentmain] 你传递过来的参数: " + agentArgs);
}
public static void agentmain(String agentArgs) {
System.out.println("[MyPremainTest::agentmain] 你传递过来的参数: " + agentArgs);
// 如果存在 public static void agentmain(String agentArgs, Instrumentation inst), 则不会进入该方法
}
}
随后我们创建resources/META-INF/MANIFEST.MF
文件, 内容如下:
Manifest-Version: 1.0 (指明版本号)
Can-Redefine-Classes: true (能够重新定义Class)
Can-Retransform-Classes: true (能够转换Class)
Premain-Class: com.heihu577.MyPremainTest (对应了 MyPremainTest::premain 方法)
Agent-Class: com.heihu577.MyPremainTest (对应了 MyPremainTest::agentmain 方法)
创建完毕后, 我们使用package
命令进行打包成jar
, 命名为agent.jar
, 但该文件不能够直接运行.
-javaagent:agent.jar=参数
如果想要运行我们刚刚的agent.jar
文件并看到它的效果, 那么我们则需要运行我们main.jar
文件进行理解, 首先我们普通运行我们的main.jar
文件的结果是这样的:
PS C:UsersAdministratorDesktop> java -jar .main.jar
[MyApp] Hello, My name is Heihu577~
[MyApp] 当前Java进程的PID是: 2612@heihubook
那么如果我们想达到一个Spring-AOP
的效果, 即在main.jar::main
方法之前进行执行agent.jar::premain
方法, 则我们应该这样运行:
PS C:UsersAdministratorDesktop> java -javaagent:agent.jar=Hacker! -jar .main.jar
[MyPremainTest::premain] 你传递过来的参数: Hacker!
[MyApp] Hello, My name is Heihu577~
[MyApp] 当前Java进程的PID是: 10520@heihubook
这里主要是在运行main.jar
文件之前增加了-javaagent
参数. 而它的运行逻辑如下:
agentmain 何处运行?
上面我们可以看到premain
的运行结果, 它的运行必须在运行之前进行指明javaagent
参数.
而agentmain
则无需在运行之前进行指明参数, 即在程序运行时
就可以进行执行agent.jar::agentmain
方法, 下面我们看一下具体实现.
具体实现
先将我们的普通main.jar
跑起来, 看一下普通运行的效果:
PS C:UsersAdministratorDesktop> java -jar main.jar
[MyApp] Hello, My name is Heihu577~
[MyApp] 当前Java进程的PID是: 15272@heihubook
(阻塞在这一行)
创建一个新项目, 这次新项目我们无需进行打jar包的一个操作, 但我们需要引入JDK自带的tools.jar
文件, 因为该文件中有VirtualMachine
这个API, 在后面我们需要使用.
首先在pom.xml
文件中进行引入:
<dependencies>
<dependency>
<groupId>com.sun.jdk</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>
随后随意创建一个运行类, 用于attach到我们的main.jar
, 代码如下:
public class MyAgentTest {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
System.out.println("AgentDemo4Test"); // 打印测试标识信息
// 获取当前系统中所有虚拟机实例的描述符列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
// 遍历虚拟机实例描述符列表
for (VirtualMachineDescriptor vmd : list) {
// 检查虚拟机实例的显示名称是否为 "main.jar"
if (vmd.displayName().equals("main.jar")) {
// 附加到指定的虚拟机实例
VirtualMachine vm = VirtualMachine.attach(vmd.id());
// 打印虚拟机实例的 ID
System.out.println(vm.id());
// 打印虚拟机实例的信息
System.out.println(vm);
// 加载指定的 Java Agent
vm.loadAgent("C:\Users\Administrator\Desktop\agent.jar", "Hacker!");
// 参数1: 指明具体的 agent 包路径
// 参数2: 给该 agent 方法传递的参数
// 从目标虚拟机断开连接
vm.detach();
}
}
}
}
编译并运行该程序, 那么main.jar
那边就会执行agent.jar::agentmain
方法, 如下:
PS C:UsersAdministratorDesktop> java -jar main.jar
[MyApp] Hello, My name is Heihu577~
[MyApp] 当前Java进程的PID是: 15272@heihubook
[MyPremainTest::agentmain] 你传递过来的参数: Hacker!
(阻塞在这一行)
可以用如下图进行解释:
Instrumentation
接口简介
刚刚我们成功执行了agentmain
方法, 我们知道的是, agentmain & premain
方法都有第二个参数, 它们是Instrumentation
类型的, 看一下这个接口的定义:
public interface Instrumentation {
/**
* 增加一个Class文件的转换器,转换器用于改变Class二进制流的数据。
* 参数canRetransform设置是否允许重新转换。
* @param transformer 转换器实例
* @param canRetransform 是否允许重新转换
*/
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
/**
* 在类加载之前,重新定义Class文件。
* 如果在类加载之后,需要使用retransformClasses方法重新定义。
* addTransformer方法配置之后,后续的类加载都会被Transformer拦截。
* 对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。
* 类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
* @param transformer 转换器实例
*/
void addTransformer(ClassFileTransformer transformer);
/**
* 删除一个类转换器。
* @param transformer 要删除的转换器实例
* @return 如果成功删除返回true,否则返回false
*/
boolean removeTransformer(ClassFileTransformer transformer);
/**
* 检查是否支持类的重新转换。
* @return 如果支持返回true,否则返回false
*/
boolean isRetransformClassesSupported();
/**
* 在类加载之后,重新定义Class。
* 这个方法是1.6之后加入的,事实上,该方法是更新了一个类。
* @param classes 要重新定义的类数组
* @throws UnmodifiableClassException 如果类无法被修改
*/
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
/**
* 检查是否支持类的重新定义。
* @return 如果支持返回true,否则返回false
*/
boolean isRedefineClassesSupported();
/**
* 重新定义类。
* @param definitions 包含新定义的类数组
* @throws ClassNotFoundException 如果类未找到
* @throws UnmodifiableClassException 如果类无法被修改
*/
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;
/**
* 检查类是否可修改。
* @param theClass 要检查的类
* @return 如果类可修改返回true,否则返回false
*/
boolean isModifiableClass(Class<?> theClass);
/**
* 获取所有已加载的类。
* @return 已加载的类数组
*/
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
/**
* 获取由指定类加载器初始化的类。
* @param loader 指定的类加载器
* @return 由指定类加载器初始化的类数组
*/
@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);
/**
* 获取一个对象的大小。
* @param objectToSize 要获取大小的对象
* @return 对象的大小(以字节为单位)
*/
long getObjectSize(Object objectToSize);
/**
* 将JAR文件添加到引导类加载器的搜索路径中。
* @param jarfile 要添加的JAR文件
*/
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
/**
* 将JAR文件添加到系统类加载器的搜索路径中。
* @param jarfile 要添加的JAR文件
*/
void appendToSystemClassLoaderSearch(JarFile jarfile);
/**
* 检查是否支持本地方法前缀。
* @return 如果支持返回true,否则返回false
*/
boolean isNativeMethodPrefixSupported();
/**
* 为转换器设置本地方法前缀。
* @param transformer 转换器实例
* @param prefix 本地方法前缀
*/
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}
其中我们挑出addTransformer & retransformClasses
进行重点理解.
使用案例
接下来的所有案例, 操作步骤将重新定义agentmain
方法, 并重新打包jar
并attach
执行, 过程就不再描述.
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("[MyPremainTest::agentmain] 你传递过来的参数: " + agentArgs);
Class[] allLoadedClasses = inst.getAllLoadedClasses(); // 得到当前 JVM 加载所有的类
for (Class clazz : allLoadedClasses) {
System.out.println(clazz.getName());
/*
com.heihu577.MyPremainTest
com.heihu577.MyApp
sun.instrument.InstrumentationImpl$1
... 当前 JVM 已加载的类, 太多了, 就不粘贴了.
*/
}
}
随后比较重要的是addTransformer
方法, 而理解该方法我们需要先在premain
方法中进行理解.
ClassFileTransformer 理解
premain 中进行理解
这里我们将通过premain & -javaagent
参数进行理解, 定义premain
方法如下:
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("[MyPremainTest::premain] 你传递过来的参数: " + agentArgs);
inst.addTransformer(new MyClassFileTransformer()); // 对 jvm 准备加载的所有类增加转换器, 当加载到具体类时, 会先走到 MyClassFileTransformer::transform 方法中
}
static class MyClassFileTransformer implements ClassFileTransformer {
/**
* 转换类的字节码。
* @param loader 加载该类的类加载器。如果类是通过引导类加载器加载的,则此参数为null。
* @param className 类的全限定名(包括包名),格式为"com/example/MyClass"。
* @param classBeingRedefined 如果这个方法是响应于retransformClasses调用而被调用的,则此参数表示正在被重新转换的类;
* 如果是类首次加载时被调用的,则此参数为null。
* @param protectionDomain 类的保护域,包含了类的安全策略和权限信息。
* @param classfileBuffer 类文件的字节码数组,即类文件在磁盘上的二进制表示。
* 这个数组在传递给transform方法之前,已经被JVM读取并加载到内存中。
* @return 修改后的类文件的字节码数组。如果不需要修改类文件,可以直接返回传入的classfileBuffer参数。
* 如果返回null,则表示拒绝加载该类(这通常是不被建议的,因为它会导致类加载失败)。
* @throws IllegalClassFormatException 如果转换后的类文件格式不正确,抛出此异常。
*/
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("Transforming class: " + className);
return classfileBuffer; // 返回修改后的字节码,这里直接返回原字节码, 并没有对其进行修改
}
}
编译, 重新打jar包后, 我们指明javaagent参数执行:
PS C:UsersAdministratorDesktop> java -javaagent:agent.jar -jar main.jar
[MyPremainTest::premain] 你传递过来的参数: null
Transforming class: java/lang/invoke/MethodHandleImpl
Transforming class: java/lang/invoke/MethodHandleImpl$1
Transforming class: java/lang/invoke/MethodHandleImpl$2
Transforming class: java/util/function/Function
... more
[MyApp] Hello, My name is Heihu577~
Transforming class: java/util/LinkedHashMap$LinkedHashIterator
Transforming class: java/util/LinkedList$ListItr
Transforming class: java/net/InetAddress$CacheEntry
... more
[MyApp] 当前Java进程的PID是: 23428@heihubook
可以看到的是, 我们在main
方法之前调用premain
方法, 在premain
方法中增加了一个类转换器, 那么接下来JVM加载的类就会调用到MyClassFileTransformer::transform
方法支持其修改字节码等操作.
agentmain 中进行理解
从刚刚的案例中, 成功理解了ClassFileTransformer
的使用, 但是是在premain
中进行理解的, 依赖于-javaagent
参数指明.
那么如果在agentmain
中进行使用呢?这里我们需要配合retransformClasses
方法的调用, 给出案例:
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
System.out.println("[MyPremainTest::agentmain] 你传递过来的参数: " + agentArgs);
inst.addTransformer(new MyClassFileTransformer(), true); // 参数2: 是否支持类的转换
Class[] allLoadedClasses = inst.getAllLoadedClasses(); // 得到当前 JVM 加载的所有类
ArrayList<Class> transformClasses = new ArrayList<>(); // 准备待转换的类
for (Class clazz : allLoadedClasses) {
String className = clazz.getName();
if (className.equals("com.heihu577.MyApp")) {
System.out.println("ADD " + className);
transformClasses.add(clazz); // 将 com.heihu577.MyApp 这个类加入
}
}
inst.retransformClasses(transformClasses.toArray(new Class[0])); // 进行转换为Class[]
}
static class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// 注意:这里打印的 className 可能是替换过的,与 clazz.getName() 不完全匹配
System.out.println("Transforming class: " + className);
// 如果需要修改类,请在这里处理 classfileBuffer
return classfileBuffer; // 目前返回原始字节码, 如果返回 null 则不做一丁点修改
}
}
生成agent.jar
后, 运行我们的main.jar
, 随后进行attach
的结果如下:
PS C:UsersAdministratorDesktop> java -jar main.jar
[MyApp] Hello, My name is Heihu577~
[MyApp] 当前Java进程的PID是: 2456@heihubook
[MyPremainTest::agentmain] 你传递过来的参数: Hacker!
ADD com.heihu577.MyApp
Transforming class: com/heihu577/MyApp
可以看到的是, 目前是已经调用到MyClassFileTransformer::transform
方法了, 我们可以在该方法中使用javassist
进行修改类的字节码!
javassist
Javassist(JAVA programming ASSISTant)是一个开源的分析、编辑和创建Java字节码的类库。允许在运行时定义新类和修改类文件,提供了创建、修改类和方法的能力。它的使用方法如下:
那么先引入javassist
包如下:
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.30.2-GA</version>
</dependency>
</dependencies>
入门案例 - 创建类并实例化
我们新创建一个项目进行简单的使用:
public class Main {
public static void main(String[] args) throws Exception {
ClassPool classPool = new ClassPool(true);
classPool.insertClassPath(new LoaderClassPath(Main.class.getClassLoader())); // 添加搜索路径, 当前是 AppClassLoader
CtClass ctClass = classPool.makeClass("com.heihu577.Cat"); // 创建一个 com.heihu577.Cat 类
/*
* class com.heihu577.Cat {}
* */
ctClass.addInterface(classPool.get(Animal.class.getName())); // 让其实现 Animal 接口
/*
* class com.heihu577.Cat implements com.heihu577.Main$Animal {}
* */
CtMethod eatMethod = new CtMethod(CtClass.voidType, "eat", new CtClass[]{classPool.get(String.class.getName())}, ctClass);
/**
* 参数1: 指明方法返回类型
* 参数2: 指明方法名称
* 参数3: 方法参数类型
* 参数4: 在哪个类下创建方法
*/
/*
* class com.heihu577.Cat implements com.heihu577.Main$Animal {
* void eat(String);
* }
* */
String src = "{System.out.println("Eating... " + $1);}"; // javassist 中的 $1 参数, 代表第一个参数
eatMethod.setBody(src); // 设置方法体
/*
* class com.heihu577.Cat implements com.heihu577.Main$Animal {
* void eat(String){
* System.out.println("Eating... " + $1);
* }
* }
* */
ctClass.addMethod(eatMethod); // 在该类上添加一个方法
Class clazz = ctClass.toClass(); // 将写好的类, 转换为 Class 对象
Animal animal = (Animal) clazz.newInstance(); // 创建实例, 这里使用接口接收, 调用方法时, 不需要通过反射调用
animal.eat("rice"); // 调用 eat 方法
}
public interface Animal {
void eat(String food);
}
}
有一种在JavaScript
中创建DOM
节点操作的感觉, 而它的运行结果为:
class com.heihu577.Cat
Eating... rice
特殊符号
在上面的案例中, src
变量中存在$1
, 这是遵循了javassist
的语法. 而它的特殊符号语法如下:
符号 | 含义 |
---|---|
$0 , $1 , $2 , ... |
$0 = this; $1 = args[1] ..... |
$args |
方法参数数组.它的类型为 Object[] |
$$ |
所有实参。例如, m($$) 等价于 m($1,$2, ...) |
$cflow( ...) |
cflow 变量 |
$r |
返回结果的类型,用于强制类型转换 |
$w |
包装器类型,用于强制类型转换 |
$_ |
返回值 |
实际案例
修改方法体
在上面我们只是简单的通过javassist
进行创建一个类, 并且在其中定义了我们自定义的一个方法, 那么如果我们想要修改某个类下的某个方法体应该如何操作呢?
public class MainTest2 {
public static class MyStringUtils {
public String returnA() {
return "A";
}
}
public static void main(String[] args) throws Exception {
// MyStringUtils myStringUtils = new MyStringUtils();
// System.out.println(myStringUtils.repeatA(10));
ClassPool classPool = new ClassPool(true);
classPool.insertClassPath(new LoaderClassPath(MainTest2.class.getClassLoader()));
CtClass ctClass = classPool.get("com.heihu577.MainTest2$MyStringUtils"); // 得到具体类模型, 这里必须使用字符串, 否则将抛出已加载过异常
CtMethod repeatA = ctClass.getDeclaredMethod("returnA"); // 得到 returnA 方法
repeatA.setBody("{return "Heihu577";}"); // 修改 returnA 方法体
ctClass.toClass(); // 使用 ClassLoader 进行加载
System.out.println(new MyStringUtils().returnA()); // 返回 Heihu577
}
}
通过上述案例, 我们已经成功的将returnA
的方法体改为了return "Heihu577"
.
创建新方法
当然, 除了修改我们也可以在原有类基础上进行添加方法. 实现如下:
public class MainTest2 {
public static class MyStringUtils {
public String returnA() {
return "A";
}
}
public static void main(String[] args) throws Exception {
ClassPool classPool = new ClassPool(true);
classPool.insertClassPath(new LoaderClassPath(MainTest2.class.getClassLoader()));
CtClass ctClass = classPool.get("com.heihu577.MainTest2$MyStringUtils"); // 得到具体类模型, 这里必须使用字符串, 否则将抛出已加载过异常
CtMethod sayHello = new CtMethod(CtClass.voidType, "sayHello", new CtClass[]{}, ctClass); // 返回值 void, 方法名 sayHello, 方法参数无, 在哪个类上
sayHello.setBody("{System.out.println("Hello~World~");}"); // 设置方法体
ctClass.addMethod(sayHello); // 将该方法加入到 CtClass 中
ctClass.toClass(); // 使用 ClassLoader 进行加载
MyStringUtils myStringUtils = new MyStringUtils();
myStringUtils.getClass().getDeclaredMethod("sayHello").invoke(myStringUtils); // 调用刚刚添加的 sayHello 方法, 输出 Hello~World~
}
}
由于编译时, 并无法扫描到sayHello
方法, 所以我们这里只能通过反射进行调用.
修改方法参数类型
public class MainTest2 {
public static class MyStringUtils {
public void tester(String name) {
System.out.println("tester starting...");
System.out.println(name);
}
}
public static void main(String[] args) throws Exception {
ClassPool classPool = new ClassPool(true);
classPool.insertClassPath(new LoaderClassPath(MainTest2.class.getClassLoader()));
CtClass ctClass = classPool.get("com.heihu577.MainTest2$MyStringUtils");
CtMethod testerMethod = ctClass.getDeclaredMethod("tester", new CtClass[]{classPool.get("java.lang.String")});
ClassMap classMap = new ClassMap();
classMap.put("java.lang.String", "java.lang.Object"); // 把 String 类型改成 Object
CtMethod testerNewMethod = CtNewMethod.copy(testerMethod, "tester", ctClass, classMap); // 参数四用于修改参数类型
ctClass.removeMethod(testerMethod); // 删除原有的 tester 方法
ctClass.addMethod(testerNewMethod); // 将 copy 创建出来的 method, 加入到 ctClass 中
ctClass.toClass(); // 将类加载到内存
MyStringUtils myStringUtils = new MyStringUtils();
Method[] declaredMethods = myStringUtils.getClass().getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println(declaredMethod); // 可以看到 public void com.heihu577.MainTest2$MyStringUtils.tester(java.lang.Object)
}
myStringUtils.getClass().getDeclaredMethod("tester", Object.class).invoke(myStringUtils, 123); // 传入 Integer 类型仍然可行
}
}
AOP 实现
通过代理调用手段
在调用方法前输出method start
, 在调用方法完毕后输出method end
. 使用如下案例:
public class MainTest2 {
public static class MyStringUtils {
public void tester(String name) {
System.out.println("Hi: " + name);
}
}
public static void main(String[] args) throws Exception {
ClassPool classPool = new ClassPool(true);
classPool.insertClassPath(new LoaderClassPath(MainTest2.class.getClassLoader()));
CtClass ctClass = classPool.get("com.heihu577.MainTest2$MyStringUtils"); // 得到 MyStringUtils 类
CtMethod tester = ctClass.getDeclaredMethod("tester", new CtClass[]{classPool.get(String.class.getName())}); // 得到 tester 方法
// 创建 tester$agent 方法, 方法体和 tester 方法相同
CtMethod tester$agent = CtNewMethod.copy(tester, tester.getName() + "$agent", ctClass, null);
// 先添加 tester$agent 方法
ctClass.addMethod(tester$agent);
// 定义新的方法体
String src = "{" +
"System.out.println("method start");" +
"this." + tester.getName() + "$agent($$);" + // 调用 tester$agent 方法
"System.out.println("method end");" +
"}";
// 将原来的 tester 方法体, 改为调用 tester$agent 方法的方法体
tester.setBody(src);
// 转换为Java类
Class<?> clazz = ctClass.toClass();
// 创建实例并调用方法
MyStringUtils instance = (MyStringUtils) clazz.getDeclaredConstructor().newInstance();
instance.tester("aaa");
/*
method start
Hi: aaa
method end
*/
}
}
通过插入代码块手段
当然我们也可以通过insertBefore & insertAfter
进行操作类字节码:
public class MainTest2 {
public static class MyStringUtils {
public void tester(String name) {
System.out.println("Hello " + name);
}
}
public static void main(String[] args) throws Exception {
ClassPool classPool = new ClassPool(true);
classPool.insertClassPath(new LoaderClassPath(MainTest2.class.getClassLoader()));
CtClass ctClass = classPool.get("com.heihu577.MainTest2$MyStringUtils");
CtMethod testerMethod = ctClass.getDeclaredMethod("tester", new CtClass[]{classPool.get(String.class.getName())});
testerMethod.insertBefore("System.out.println("Before..." + name);"); // 在方法前增加代码
testerMethod.insertAfter("System.out.println("After...");"); // 在方法后增加代码
ctClass.toClass();
MyStringUtils myStringUtils = new MyStringUtils();
myStringUtils.tester("World");
}
}
但是这种方式的缺陷则是, 假设在insertBefore
中进行定义了a
变量, 而在insertAfter
中是无法获取到a
变量的值的.
Javassist & JavaAgent 联动使用
JavaAgent 可以通过agentmain
方法, 进行劫持到正常的 Java 程序中, 支持将已加载的类进行重新加载, 提供修改字节码操作功能.
Javassist 提供修改字节码功能操作, 下面来看一下这两者联合使用的案例. 本次将重新构建新项目进行说明.
搭建正常服务
这里我们新建一个项目, 创建一个正常服务:
package com.heihu577;
public class Server {
public static void main(String[] args) throws InterruptedException {
while (true) {
sayHello();
Thread.sleep(1000);
}
}
public static void sayHello() {
System.out.println("[Server] Hello ~");
}
}
这个代码很简单, 每过一秒进行输出[Server] Hello ~
, 随后我们配置打包插件:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
</plugins>
</build>
因为是不需任何依赖, 我们只需要使用maven-jar-plugin
就可以了. 随后我们创建META-INF/MANIFEST.MF
文件内容如下:
Main-Class: com.heihu577.Server
配置完毕后, 打成server.jar
包并运行:
PS C:UsersAdministratorDesktop> java -jar server.jar
[Server] Hello ~
[Server] Hello ~
[Server] Hello ~
... (持续输出)
创建 JavaAgent
随后我们再新建一个项目, 用于生成我们javaAgent.jar
包,创建主程序如下:
public class MyAgent {
public static void agentmain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new MyClassFileTransformer(), true);
Class<?>[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class<?> allLoadedClass : allLoadedClasses) {
if (allLoadedClass.getName().equals("com.heihu577.Server")) {
try {
inst.retransformClasses(allLoadedClass); // 重新加载 com.heihu577.Server
} catch (UnmodifiableClassException e) {
throw new RuntimeException(e);
}
}
}
}
static class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
System.out.println("Transforming class: " + className);
ClassPool classPool = new ClassPool(true);
classPool.insertClassPath(new LoaderClassPath(loader));
CtClass ctClass = classPool.get(className.replace("/", "."));
CtMethod tester = ctClass.getDeclaredMethod("sayHello"); // 得到 sayHello 方法
tester.setBody("{System.out.println("[MyAgent] Heihu577!");}");
return ctClass.toBytecode(); // 返回修改后的字节码
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
由于这里我们的transform
方法使用了javassist
库中的内容, 这里打包方式以及扩展引入在pom.xml
中是这样定义的:
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.30.2-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
使用maven-assembly-plugin
可以进行带扩展打包, 随后指明我们的/resources/META-INF/MANIFEST.MF
文件内容如下:
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: com.heihu577.MyAgent
定义完毕之后, 可以进行打包. 打成agent.jar
包.
运行 attach 应用
再新建一个项目, 写入attach
代码块:
public class Test {
public static void main(String[] args) throws AgentLoadException, IOException, AgentInitializationException, AttachNotSupportedException {
System.out.println("AgentDemo4Test"); // 打印测试标识信息
// 获取当前系统中所有虚拟机实例的描述符列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
// 遍历虚拟机实例描述符列表
for (VirtualMachineDescriptor vmd : list) {
// 检查虚拟机实例的显示名称是否为 "main.jar"
if (vmd.displayName().equals("server.jar")) {
// 附加到指定的虚拟机实例
VirtualMachine vm = VirtualMachine.attach(vmd.id());
// 打印虚拟机实例的 ID
System.out.println(vm.id());
// 打印虚拟机实例的信息
System.out.println(vm);
// 加载指定的 Java Agent
vm.loadAgent("C:\Users\Administrator\Desktop\agent.jar", "Hacker!");
// 参数1: 指明具体的 agent 包路径
// 参数2: 给该 agent 方法传递的参数
// 从目标虚拟机断开连接
vm.detach();
}
}
}
}
这里我们需要引入tools.jar
:
<dependencies>
<dependency>
<groupId>com.sun.jdk</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>
随后运行我们的server.jar
:
PS C:UsersAdministratorDesktop> java -jar server.jar
[Server] Hello ~
[Server] Hello ~
运行我们的attach应用, 使用javaAgent && javassist
技术修改正在运行的server.jar
中的Server::sayHello
内容:
Reference
Javassist 视频课: https://www.bilibili.com/video/BV1ov4y1Z7tr
Javassist 使用指南(一):https://www.jianshu.com/p/43424242846b
Javassist 使用指南(二):https://www.jianshu.com/p/b9b3ff0e1bf8
Javassist 使用指南(三):https://www.jianshu.com/p/7803ffcc81c8
Java Agent 从入门到内存马: https://xz.aliyun.com/t/9450
Java Agent 使用指南: https://www.cnblogs.com/rickiyang/p/11368932.html
Java Agent 使用: https://blog.csdn.net/zhangyifang_009/article/details/137936613
Java Agent 通灵之术: https://lsieun.github.io/article/java-agent-summoning-jutsu.html
原文始发于微信公众号(Heihu Share):语言特性 | JavaAgent & Javassist
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论