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 上的程序,甚至能够替换和修改某些类的定义

如果还没入门或者想要更多知识,可以查阅

我为什么会研究这个,刚开始是基于这样一个需求:有个远古项目是专门做直播APP的活动,每个活动都有对应的生命周期,加上没有做成模块化(我也在考虑怎么搞),久了之后大部分的活动都已经下线,只有极少的活动还在运营,就想着有什么办法可以检测到哪些代码是还会执行的,以便迁移。想过定时jstack或者Spring AOP,发现都不适合,幸好之前了解过这方面的知识,就觉得可以派上用场了。认真看了之后,发现平时遇到的几个痛点,也可以用Java Instrument解决(原本的需求后面再研究了...囧):

  1. 线上定位问题,想要知道某个变量执行时的值(IDEA远程Debug?):临时加日志记录变量的值
  2. 性能优化,需要知道线上执行每一段代码的耗时:临时加日志记录代码执行耗时
  3. 协助Tester去测试不可以造数据的场景(比如特定日期特定时间的逻辑):临时写死某个变量的值

以上都涉及一个相同的需求:临时修改方法体。这时候,我们就可以使用Instrumentation的redefineClasses

java工具包修改 java程序修改_java

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、打包

要求:

  1. 运行时需要具体平台(Windows、Linux、Mac等)的tools.jar。所以要么把依赖放入jar包,要么执行java -jar时添加类库路径。
  2. 生成对应的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包

java工具包修改 java程序修改_Java Instrument_02

java工具包修改 java程序修改_Java Instrument_03

4、使用

1)写一个目标程序并运行

java工具包修改 java程序修改_Java Agent_04

java工具包修改 java程序修改_动态修改Class定义_05

java工具包修改 java程序修改_java工具包修改_06

2)使用jps命令查看Java进程pid:12780

java工具包修改 java程序修改_Java Agent_07

3)修改Task类,并重新编译,将得到的字节码文件改名为Task-1.class

java工具包修改 java程序修改_Java Agent_08

java工具包修改 java程序修改_动态修改Class定义_09

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

控制台输出

java工具包修改 java程序修改_动态修改Class定义_10

目标程序控制台输出

java工具包修改 java程序修改_Java Agent_11

这不用多说了吧?鼓掌!撒花!