利用Java Agent进行代码植入
Java Agent 又叫做 Java 探针,是在 JDK1.5 引入的一种可以动态修改 Java 字节码的技术。可以把javaagent理解成一种代码注入的方式。但是这种注入比起spring的aop更加的优美。
Java agent的使用方式有两种:
实现premain方法,在JVM启动前加载。
实现agentmain方法,在JVM启动后加载。
premain和agentmain函数声明如下,方法名相同情况下,拥有Instrumentation inst参数的方法优先级更高:
public static void agentmain(String agentArgs, Instrumentation inst) { ... } public static void agentmain(String agentArgs) { ... } public static void premain(String agentArgs, Instrumentation inst) { ... } public static void premain(String agentArgs) { ... }JVM 会优先加载带 Instrumentation 签名的方法,加载成功忽略第二种;如果第一种没有,则加载第二种方法。
第一个参数String agentArgs就是Java agent的参数。
Inst 是一个 java.lang.instrument.Instrumentation 的实例,可以用来类定义的转换和操作等等。
premain方式JVM启动时 会先执行 premain 方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,就可以结合第三方的字节码编译工具,比如ASM,javassist,cglib等等来改写实现类。
使用实例: 1)创建应用程序Task.jar先创建一个Task.jar用于模拟在实际场景中的应用程序,Task.java:
public class Task { public static void main (String[] args) { System.out.println("task mian run"); } }把Task打成jar包:
此jar包可以单独执行:java -jar Task.jar
2)创建premain方式的Agent新建一个Agent01.jar,用于在task之前执行:
import java.lang.instrument.Instrumentation; public class Agent01 { public static void premain(String agentArgs, Instrumentation inst){ System.out.println("premain run----"); } }此时项目如果打成jar包,缺少入口main文件,所以需要自己定义一个MANIFEST.MF文件,用于指明premain的入口在哪里:
在src/main/resources/目录下创建META-INF/MANIFEST.MF:
Manifest-Version: 1.0 Premain-Class: com.test.Agent01注意:最后一行是空行,不能省略。以下是MANIFEST.MF的其他选项
Premain-Class: 包含 premain 方法的类(类的全路径名) Agent-Class: 包含 agentmain 方法的类(类的全路径名) Boot-Class-Path: 设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。(可选) Can-Redefine-Classes: true表示能重定义此代理所需的类,默认值为 false(可选) Can-Retransform-Classes: true 表示能重转换此代理所需的类,默认值为 false (可选) Can-Set-Native-Method-Prefix: true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)同样的打成jar包:
回顾下我们之前单独运行task.jar时候,控制台前后并没有打印其他信息
现在我们来使用premain进行注入: java -javaagent:Agent01.jar -jar Task.jar
可以看到premain比task先运行,通过启动时候指定参数javaagent来达到注入的效果
以下是先知社区师傅的流程图:
这种方法存在一定的局限性——只能在启动时使用-javaagent参数指定。在实际环境中,目标的JVM通常都是已经启动的状态,无法预先加载premain。相比之下,agentmain更加实用。
agentmain方式同样使用一个案例来说明使用方式
使用实例: 1)创建应用程序Task.jar