Provides services that allow Java programming language agents to instrument programs running on the JVM. The mechanism for instrumentation is modification of the byte-codes of methods.【用于允许Java编程语言代理检测运行在JVM上的程序提供服务。检测的机制是修改方法的字节码。】
这是java.lang.instrument包的描述。使用 Instrumentation,使得开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。
如果还没入门或者想要更多知识,可以查阅
- 官方文档:https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html
- 优秀博客:https://www.jianshu.com/p/b72f66da679f
我为什么会研究这个,刚开始是基于这样一个需求:有个远古项目是专门做直播APP的活动,每个活动都有对应的生命周期,加上没有做成模块化(我也在考虑怎么搞),久了之后大部分的活动都已经下线,只有极少的活动还在运营,就想着有什么办法可以检测到哪些代码是还会执行的,以便迁移。想过定时jstack或者Spring AOP,发现都不适合,幸好之前了解过这方面的知识,就觉得可以派上用场了。认真看了之后,发现平时遇到的几个痛点,也可以用Java Instrument解决(原本的需求后面再研究了...囧):
- 线上定位问题,想要知道某个变量执行时的值(IDEA远程Debug?):临时加日志记录变量的值
- 性能优化,需要知道线上执行每一段代码的耗时:临时加日志记录代码执行耗时
- 协助Tester去测试不可以造数据的场景(比如特定日期特定时间的逻辑):临时写死某个变量的值
以上都涉及一个相同的需求:临时修改方法体。这时候,我们就可以使用Instrumentation的redefineClasses。
1、开发Agent-Class
package cn.zhh;
import java.io.IOException;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* Agent-Class
*/
public class AgentMain {
/**
* 运行中代理入口
*
* @param agentArgs 自定义参数
* @param inst 增强类
* @throws Exception 异常
*/
public static void agentmain(String agentArgs, Instrumentation inst) throws Exception {
// 自定义参数英文逗号分隔:[0]-class文件绝对路径,[1]-class全名
String[] args = agentArgs.split(",");
// 重新定义Class
inst.redefineClasses(new ClassDefinition(Class.forName(args[1]), readClassBytes(args[0])));
}
/**
* 读取文件内容
*
* @param classPath 文件路径
* @return 文件字节数据
* @throws IOException 文件读取异常
*/
private static byte[] readClassBytes(String classPath) throws IOException {
return Files.readAllBytes(Paths.get(classPath));
}
}
2、开发可执行jar包的主函数
可执行jar包可以和代理jar包分开不同项目,放在一起更加方便。需要依赖tools.jar编译(JDK提供,把{JDK根目录}/lib/tools.jar引入即可),因为使用接口编程,所以不需要区分平台的JDK。
package cn.zhh;
import com.sun.tools.attach.VirtualMachine;
import java.util.Arrays;
import java.util.Objects;
/**
* mainClass
*/
public class Main {
/**
* 可执行jar包主函数
*
* @param args 自定义函数
* @throws Exception 异常
*/
public static void main(String[] args) throws Exception {
if (Objects.isNull(args) || args.length != 4) {
throw new RuntimeException("参数数量不正确,需要4个:第一个agent包绝对路径,第二个Java进程PID,第三个class文件绝对路径,第四个class全名");
}
System.out.println("Main run, args are:");
Arrays.stream(args).forEach(System.out::println);
VirtualMachine virtualMachine = VirtualMachine.attach(args[1]);
try {
virtualMachine.loadAgent(args[0], args[2] + "," + args[3]);
} finally {
virtualMachine.detach();
}
}
}
3、打包
要求:
- 运行时需要具体平台(Windows、Linux、Mac等)的tools.jar。所以要么把依赖放入jar包,要么执行java -jar时添加类库路径。
- 生成对应的MANIFEST.MF清单。
因此,推荐使用Maven
1)添加tools.jar依赖
将jar包install到本地仓库(不要使用Library依赖或者systemPath依赖):mvn install:install-file -DgroupId=com.sun -DartifactId=tools -Dversion=1.8 -Dpackaging=jar -Dfile=D:\Java\jdk1.8.0_141\lib\tools.jar
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
</dependency>
2)添加assembly插件,并配置清单
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>cn.zhh.Main</mainClass>
</manifest>
<manifestEntries>
<Agent-Class>
cn.zhh.AgentMain
</Agent-Class>
<Can-Redefine-Classes>
true
</Can-Redefine-Classes>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
3)运行assembly命令得到包含依赖的可执行jar包
4、使用
1)写一个目标程序并运行
2)使用jps命令查看Java进程pid:12780
3)修改Task类,并重新编译,将得到的字节码文件改名为Task-1.class
4)终极操作,运行jar包,见证奇迹的时候
java -jar agent-1.0-jar-with-dependencies.jar D:\IdeaProjects\java-agent\agent\target\agent-1.0-jar-with-dependencies.jar 12780 D:\IdeaProjects\java-agent\target\target\classes\cn\zhh\Task-1.class cn.zhh.Task
控制台输出
目标程序控制台输出
这不用多说了吧?鼓掌!撒花!