一、javaagent

1、什么是javaagent

javaagent 是java1.5之后引入的特性,其主要作用是在class 被加载之前对其拦截,以插入我们的监听字节码。

Java Agent 是 Java 提供的一种强大机制,它允许开发者在不修改原有应用程序代码的情况下,对类进行修改和增强。

Java Agent 在很多场景下都非常有用,如性能监控、代码注入、AOP 等。

2、使用示例

(1)创建javaagent类

package com.test;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

public class JavaAgentTest {
 
    /**
     * javaagent入口类,方法名为premain
     * 接收两个参数:agentArgs 是 Agent 的参数,inst 是 Instrumentation 对象,用于注册类文件转换器。
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        // 添加类文件转换器
        inst.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                    ProtectionDomain protectionDomain, byte[] classfileBuffer)
                    throws IllegalClassFormatException {
                // 这里可以对类字节码进行修改
                System.out.println("Transforming class: " + className);
                return classfileBuffer;
            }
        });
    }
}

(2)创建 MANIFEST.MF 文件

打包后的 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(可选)

MANIFEST.MF可以自己创建(在 resources 目录下新建目录:META-INF,在该目录下新建文件:MANIFREST.MF,接下来将该工程打成jar包,我在打包的时候发现打完包之后的 MANIFREST.MF文件被默认配置替换掉了。所以我是手动将上面我的配置文件替换到jar包中的文件,这里你需要注意。)。注意,MANIFEST.MF最后有一个换行符

也可以用maven插件打包。注意最后一行需要有空行。

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.1.0</version>
            <configuration>
                <archive>
                    <!--自动添加META-INF/MANIFEST.MF -->
                    <manifest>
                        <addClasspath>true</addClasspath>
                    </manifest>
                    <manifestEntries>
                        <Premain-Class>com.test.JavaAgentTest</Premain-Class>
                        <Agent-Class>com.test.JavaAgentTest</Agent-Class>
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                    </manifestEntries>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

(3)使用javaagent包

java启动 参数详解:

-agentlib:<libname>[=<选项>] 加载本机代理库 <libname>, 例如 -agentlib:hprof
	另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<选项>]
	按完整路径名加载本机代理库
-javaagent:<jarpath>[=<选项>]
	加载 Java 编程语言代理, 请参阅 java.lang.instrument

一个java程序中-javaagent参数的个数是没有限制的,所以可以添加任意多个javaagent。所有的java agent会按照你定义的顺序执行,例如:
java -javaagent:agent1.jar -javaagent:agent2.jar -jar MyProgram.jar
程序执行的顺序将会是:
MyAgent1.premain -> MyAgent2.premain -> MyProgram.main
也就是说,正常的jar包会在agent加载之后再执行main方法

# 启动我们的javaagent测试包,发现每加载一个类,都会调用一次我们的日志打印
java -javaagent:mavenjavatest-1.0.jar -jar SpringbootDemo.jar

3、源码分析

(1)Instrumentation 类

public interface Instrumentation {
    
    //增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    //在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);

    //删除一个类转换器
    boolean removeTransformer(ClassFileTransformer transformer);

    boolean isRetransformClassesSupported();

    //在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    boolean isRedefineClassesSupported();

    
    void redefineClasses(ClassDefinition... definitions)
        throws  ClassNotFoundException, UnmodifiableClassException;

    boolean isModifiableClass(Class<?> theClass);

    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();

  
    @SuppressWarnings("rawtypes")
    Class[] getInitiatedClasses(ClassLoader loader);

    //获取一个对象的大小
    long getObjectSize(Object objectToSize);


   
    void appendToBootstrapClassLoaderSearch(JarFile jarfile);

    
    void appendToSystemClassLoaderSearch(JarFile jarfile);

    
    boolean isNativeMethodPrefixSupported();

    
    void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}

4、Instrumentation的局限性

大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,或者笼统说就是类重定义(Class Redefine)的功能,但是有以下的局限性:
1、premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
2、类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses()方法,此方法有以下限制:
2.1 新类和老类的父类必须相同;
2.2 新类和老类实现的接口数也要相同,并且是相同的接口;
2.3 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致;
2.4 新类和老类新增或删除的方法必须是private static/final修饰的;
2.5 可以修改方法体。

除了上面的方式,如果想要重新定义一个类,可以考虑基于类加载器隔离的方式:创建一个新的自定义类加载器去通过新的字节码去定义一个全新的类,不过也存在只能通过反射调用该全新类的局限性。

二、javassist

1、简介

Javassist 使 Java 字节码操作变得简单。它是一个用于在 Java 中编辑字节码的类库。
它使 Java 程序可以在运行时定义新类,并在 JVM 加载它时修改类文件。与其他类似的字节码编辑器不同,Javassist 提供了两个级别的 API:源级别和字节代码级别。如果用户使用源代码级 API,则他们可以在不了解 Java 字节码规范的情况下编辑类文件。整个 API 仅使用 Java 语言的词汇表进行设计。您甚至可以以源文本的形式指定插入的字节码。Javassist 可以即时对其进行编译。另一方面,字节码级别的 API 允许用户像其他编辑器一样直接编辑类文件。

首先要引入jar包。

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.30.2-GA</version>
</dependency>

英文文档:http://www.javassist.org/tutorial/tutorial.html

2、javassist:创建一个新类

package com.test;

import javassist.*;

public class CreatePerson {

    /**
     * 创建一个Person 对象
     */
    public static void createPseson() throws Exception {
        /**
         * 1、创建ClassPool,这里使用了默认的ClassPool
         *
         * 看源码可以看出,getDefault相当于以下两行:
         *  ClassPool classPool = new ClassPool();
         *  classPool.appendSystemPath();
         */
        ClassPool pool = ClassPool.getDefault();

        /**
         * 2. 创建一个空类
         *
         * 如果是想获取一个现有的类,可以使用get方法,只能传字符串类名:
         * CtClass cc = pool.get("com.test.Person");
         */
        CtClass cc = pool.makeClass("com.test.Person");


        // 3. 新增一个字段 private String name;
        // 字段名为name
        CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
        // 访问级别是 private
        param.setModifiers(Modifier.PRIVATE);
        // 初始值是 "zhangsan"
        cc.addField(param, CtField.Initializer.constant("zhangsan"));

        // 4. 生成 getter、setter 方法
        cc.addMethod(CtNewMethod.setter("setName", param));
        cc.addMethod(CtNewMethod.getter("getName", param));

        // 5. 添加无参的构造函数
        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
        cons.setBody("{name = \"lisi\";}");
        cc.addConstructor(cons);

        // 6. 添加有参的构造函数,参数为String类型
        cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
        // $0=this / $1,$2,$3... 代表方法参数
        cons.setBody("{$0.name = $1;}");
        cc.addConstructor(cons);

        /**
         * 7、创建一个名为printName方法,无参数,无返回值,输出name值
         *
         * 也可以这样做:
         *         CtMethod satHelloMethod = CtNewMethod.make(CtClass.voidType, // void返回值
         *                 "sayHello", // 方法名
         *                 new CtClass[]{classPool.get(String.class.getName())}, // 方法参数
         *                 new CtClass[0], // 异常类型
         *                 "{System.out.println(\"hello:\"+$1);}", // 方法体内容,$1表示第一个参数
         *                 class1 // 指定类
         *                 );
         */
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctMethod.setBody("{System.out.println(name);}");
        cc.addMethod(ctMethod);

        // 也可以添加接口
        // cc.addInterface(pool.get("com.test.PersonInterface"));

        //8、这里会将这个创建的类对象编译为.class文件
        cc.writeFile("E:\\javacodes\\mavenjavatest\\src\\main\\java");

        /**
         * 9、生成Class
         */
        Class personClazz = pool.toClass(cc);

        Object o = personClazz.newInstance();
        // 反射执行目标方法
        System.out.println(personClazz.getDeclaredMethod("getName").invoke(o));


    }

    public static void main(String[] args) {
        try {
            createPseson();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

生成的Person文件:
在这里插入图片描述

3、javassist:调用生成的类对象

1)通过反射的方式调用

/**
 * 通过放射方式调用
 */
private static void call4reflect(CtClass cc) throws Exception {
    // 这里不写入文件,直接实例化
    Object person = cc.toClass().newInstance();
    // 设置值
    Method setName = person.getClass().getMethod("setName", String.class);
    setName.invoke(person, "zhangsan");
    // 输出值
    Method execute = person.getClass().getMethod("printName");
    execute.invoke(person);
}

2)通过读取 .class 文件的方式调用

/**
 * 通过读取 .class 文件的方式调用
 */
private static void call4classFile() throws Exception {
    ClassPool pool = ClassPool.getDefault();
    // 设置类路径
    pool.appendClassPath("/javassist_demo/javassist_java_demo/src/main/java/");
    CtClass ctClass = pool.get("com.test.javassist.Person");
    Object person = ctClass.toClass().newInstance();
    // 设置值
    Method setName = person.getClass().getMethod("setName", String.class);
    setName.invoke(person, "zhangsan");
    // 输出值
    Method execute = person.getClass().getMethod("printName");
    execute.invoke(person);
}

3)通过接口的方式

// 定义一个接口
public interface PersonI {
    void setName(String name);

    String getName();

    void printName();
}

/**
 * 通过接口的方式调用
 */
private static void call4classInterface() throws Exception {
    ClassPool pool = ClassPool.getDefault();
    pool.appendClassPath("/javassist_demo/javassist_java_demo/src/main/java/");

    // 获取接口
    CtClass codeClassI = pool.get("com.test.javassist.PersonI");
    // 获取上面生成的类
    CtClass ctClass = pool.get("com.test.javassist.Person");
    // 使代码生成的类,实现 PersonI 接口
    ctClass.setInterfaces(new CtClass[]{codeClassI});

    // 以下通过接口直接调用 强转
    PersonI person = (PersonI) ctClass.toClass().newInstance();
    person.setName("zhangsan");
    person.printName();
}


4、javassist:修改一个现有的类

public class PersonService {
    public void getPerson(){
        System.out.println("get Person");
    }

    public void personFly(){
        System.out.println("oh my god,I can fly");
    }
}
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;

import java.lang.reflect.Method;

/**
 * 修改 .class 文件
 */
public class TestUpdateClass {

    public static void update() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get("com.test.javassist.PersonService");

        CtMethod personFly = cc.getDeclaredMethod("personFly");
        personFly.insertBefore("System.out.println(\"起飞之前准备降落伞\");");
        personFly.insertAfter("System.out.println(\"成功落地。。。。\");");

        // 新增一个方法
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "joinFriend", new CtClass[]{}, cc);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctMethod.setBody("{System.out.println(\"i want to be your friend\");}");
        cc.addMethod(ctMethod);

        Object person = cc.toClass().newInstance();
        // 调用 personFly 方法
        Method personFlyMethod = person.getClass().getMethod("personFly");
        personFlyMethod.invoke(person);
        // 调用 joinFriend 方法
        Method execute = person.getClass().getMethod("joinFriend");
        execute.invoke(person);
    }

    public static void main(String[] args) {
        try {
            update();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

5、javassist官方文档

英文文档:http://www.javassist.org/tutorial/tutorial.html
参考翻译:https://www.cnblogs.com/scy251147/p/11100961.html

6、Javassist中的一些特殊参数

(1)$0,$1,$2,…
0代表this$1代表第一个参数,$2代表第二个参数
静态方法是没有0参数的!

// 如果有:
public void test(int a,int b,int c){}
// 那么如果想引用a和b和c的话,需要这样:
ctMethod.setBody("return $1 + $2 + $3;");

(2)$args
$args变量表示所有参数的数组,它是一个Object类型的数组(new Object[]{…}),如果参数中有原始类型的参数,会被转换成对应的包装类型。比如原始数据类型为int,则会被转换成java.lang.Integer,然后存储在args中。

// 我们测试代码如下:
CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello1", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("System.out.println($args);");

// 编译后的结果如下:
public void hello1(int var1, double var2) {
    System.out.println(new Object[]{new Integer(var1), new Double(var2)});
}

(3)$$
所有方法参数,例如:m($$)相当于m($1,$2,…)

// 例如原java代码
CtMethod hello2 = new CtMethod(CtClass.voidType, "hello2", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
hello2.setModifiers(Modifier.PUBLIC);
hello2.setBody("this.value = $1 + $2;");
ctClass.addMethod(hello2);

//添加一个hello1的方法
CtMethod hello1 = new CtMethod(CtClass.voidType, "hello1", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
hello1.setModifiers(Modifier.PUBLIC);
hello1.setBody("this.value = $1 + $2;");
hello1.insertAfter("hello2($$);");

// 编译后的结果
public void hello2(int var1, double var2) {
    this.value = (int)((double)var1 + var2);
}

public void hello1(int var1, double var2) {
    this.value = (int)((double)var1 + var2);
    Object var5 = null;
    // 可以看到我们hello1调用hello2时,需要传递全部参数。可以写成$$
    this.hello2(var1, var2);
}

(4)$r
表示结果类型,如果有返回值,必须使用该方式强转

Object result = ... ;
$_ = ($r)result;

7、踩坑:insertAfter无法使用insertBefore的参数

我们有一个测试类:

public class PersonService {
    public int getPerson(String name){
        System.out.println("get Person:" + name);
        return name.length();
    }
}

如果想要判断调用getPerson方法需要的时间,或许我们会考虑这样做:

public static void main(String[] args) {
    PersonService personService = new PersonService();
    long begin = System.nanoTime();
    int i = personService.getPerson("zhangsan");
    long end = System.nanoTime();
    System.out.println("cost time:" + (end - begin));
}

有的小伙伴会想到,在方法中使用insertAfter和insertBefore实现以下方式就行了啊:

public int getPerson(String name){
    long begin = System.nanoTime();
    
    System.out.println("get Person:" + name);

    long end = System.nanoTime();
    System.out.println("cost time:" + (end - begin));
    return name.length();
}

但是,实际上生成的代码类似这样子,是拿不到begin的参数的:

public int getPerson(String name){
    {
        long begin = System.nanoTime();
    }

    System.out.println("get Person:" + name);

    {
        long end = System.nanoTime();
        // 无法拿到begin参数
        System.out.println("cost time:" + (end - begin));
    }
    return name.length();
}

我们就考虑实现以下代码:

public int getPerson(String name){
	long begin = System.nanoTime();
    int i = getPerson$assist(name);
    long end = System.nanoTime();
    System.out.println("cost time:" + (end - begin));
    return i;
}

public int getPerson$assist(String name){
    System.out.println("get Person:" + name);
    return name.length();
}

具体实现如下:


import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

public class PersonServiceTest {

    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();

        CtClass ctClass = pool.get("com.test.PersonService");

        // 获取到指定方法
        CtMethod getPersonMethod = ctClass.getDeclaredMethod("getPerson");
        // 复制方法
        // 源方法,目标方法名,目标类
        CtMethod copyMethod = CtNewMethod.copy(getPersonMethod, getPersonMethod.getName() + "$assist", ctClass, null);
        ctClass.addMethod(copyMethod);

        // 重写源方法
        getPersonMethod.setBody("{" +
                        "long begin = System.nanoTime();\n" +
                        "int result = " + getPersonMethod.getName() + "$assist($$);\n" +
                        "long end = System.nanoTime();\n" +
                        "System.out.println(\"cost time:\" + (end - begin));\n" +
                        "return result;\n" +

                        "}"
                );

        // 写入class文件
        ctClass.writeFile("E:\\javacodes\\mavenjavatest\\src\\main\\java");

        // 生成新的class,注意,之前的PersonService必须要不能被加载
        pool.toClass(ctClass);

        PersonService personService = new PersonService();
        personService.getPerson("张三");

    }
}

生成的class:
在这里插入图片描述

三、实战:基于javaagent和javassist

1、统计Springboot项目启动的时长

(1)pom文件


    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.30.2-GA</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <!--自动添加META-INF/MANIFEST.MF -->
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                        	<!--agent入口类-->
                            <Premain-Class>com.test.JavaAgentTest</Premain-Class>
                            <Agent-Class>com.test.JavaAgentTest</Agent-Class>
                            <Can-Redefine-Classes>false</Can-Redefine-Classes>
                            <Can-Retransform-Classes>false</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
            <!--将引用的依赖打包到jar中-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.4.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <!--主启动类,这里没啥用-->
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.example.SpringBootStartupMonitorAgent</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

(2)javaagent代码

package com.test;

import javassist.*;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

public class JavaAgentTest {
 
    /**
     * javaagent入口类,方法名为premain
     * 接收两个参数:agentArgs 是 Agent 的参数,inst 是 Instrumentation 对象,用于注册类文件转换器。
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        // 添加类文件转换器
        inst.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                    ProtectionDomain protectionDomain, byte[] classfileBuffer)
                    throws IllegalClassFormatException {
                // 这里可以对类字节码进行修改,我们springboot的主启动类
                String targetClassName = "com/demo/springbootdemo/SpringbootDemoApplication";
                if (targetClassName.replace('.', '/').equals(className)) {
                    System.out.println("className:" + className);
                    try {
                        ClassPool pool = ClassPool.getDefault();
                        // 添加系统类加载器和当前线程上下文类加载器
                        pool.appendClassPath(new LoaderClassPath(ClassLoader.getSystemClassLoader()));
                        pool.insertClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
                        // 使用二进制字节码创建CtClass
                        CtClass ctClass = pool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));
                        // 获取到指定方法
                        CtMethod getPersonMethod = ctClass.getDeclaredMethod("main");
                        // 复制方法
                        // 源方法,目标方法名,目标类
                        CtMethod copyMethod = CtNewMethod.copy(getPersonMethod, getPersonMethod.getName() + "$assist", ctClass, null);
                        ctClass.addMethod(copyMethod);

                        // 重写源方法
                        getPersonMethod.setBody("{" +
                                "long begin = System.nanoTime();\n" +
                                getPersonMethod.getName() + "$assist($$);\n" +
                                "long end = System.nanoTime();\n" +
                                "System.out.println(\"cost time:\" + (end - begin));\n" +
                                "}"
                        );

                        System.out.println("modify success !!!");
                        // 生成新的class 字节码并返回
                        return ctClass.toBytecode();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                return classfileBuffer;
            }
        });
    }
}

(3)测试

打包之后,执行:

java -javaagent:mavenjavatest-1.0.jar -jar SpringbootDemo-0.0.1-SNAPSHOT.jar

发现统计出执行时间了
在这里插入图片描述

2、Springboot项目运行中动态修改方法

注意,该方式只能实现方法的修改,要想修改整个类涉及bean的生命周期问题,还需要更复杂的编码

(1)pom



    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.30.2-GA</version>
        </dependency>
        <!--java8以后需要手动加入-->
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <!--自动添加META-INF/MANIFEST.MF -->
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Agentmain-Class>com.test.JavaAgentRuntimeTest</Agentmain-Class>
                            <Agent-Class>com.test.JavaAgentRuntimeTest</Agent-Class>
                            <Can-Redefine-Classes>false</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
            <!--将引用的依赖打包到jar中-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.4.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <!--agent 主启动类-->
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.test.JavaAgentRuntimeTest</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>


(2)javaagent代码

package com.test;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

public class JavaAgentRuntimeTest {
 
    /**
     * javaagent入口类,方法名为agentmain,可以在运行中执行代码,替换类
     * 接收两个参数:agentArgs 是 Agent 的参数,inst 是 Instrumentation 对象,用于注册类文件转换器。
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                    ProtectionDomain protectionDomain, byte[] classfileBuffer)
                    throws IllegalClassFormatException {
                // 替换为你要修改的类的全限定名
                String targetClassName = "com/demo/springbootdemo/TestController";
                if (targetClassName.replace('.', '/').equals(className)) {
                    try {
                        ClassPool pool = ClassPool.getDefault();
                        // 添加系统类加载器和当前线程上下文类加载器
                        pool.appendClassPath(new LoaderClassPath(ClassLoader.getSystemClassLoader()));
                        pool.insertClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
                        CtClass cc = pool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));
                        // 替换为你要修改的方法名
                        CtMethod method = cc.getDeclaredMethod("index");
                        // 修改方法体
                        method.setBody("{ System.out.println(\"Modified method body\"); return \"Modify success\"; }");
                        return cc.toBytecode();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                return classfileBuffer;
            }
        }, true);

        try {
            // 重新转换已加载的类
            inst.retransformClasses(Class.forName("com.demo.springbootdemo.TestController"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


}

(3)打包

使用maven打包成agent的jar包。

(4)测试

1、启动springboot
2、jps查看springboot的进程号
3、执行以下代码

import com.sun.tools.attach.VirtualMachine;

public class JavaAgentRuntimeTestMain {

    public static void main(String[] args) {
        try {
            // 获取目标 JVM 的进程 ID
            String pid = "84600";
            VirtualMachine vm = VirtualMachine.attach(pid);
            // 加载 Agent JAR 文件
            vm.loadAgent("E:\\javacodes\\mavenjavatest\\target\\mavenjavatest-1.0.jar");
            vm.detach();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4、观察我们的controller结果

在这里插入图片描述

参考资料

https://www.cnblogs.com/rickiyang/p/11336268.html
https://juejin.cn/post/7078681608206680094

官方文档:http://www.javassist.org/tutorial/tutorial.html

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐