使用 ChatGPT 学习 Javassist

Q:Javassist 是什么?

A:

Javassist是一个开源的Java字节码操作库,它允许开发人员在运行时编辑和创建Java类。使用Javassist,开发人员可以通过修改现有类的字节码来动态地改变它们的行为,也可以使用Javassist的API来创建全新的Java类。Javassist提供了一个易于使用的API,使开发人员能够在不需要了解复杂的字节码结构的情况下轻松地创建和编辑Java类。Javassist还提供了一些高级功能,如动态代理、AOP(面向切面编程)和Java字节码的转换。由于它的灵活性和易用性,Javassist被广泛用于Java应用程序的动态编程和增强。

Q:Javassist 入门示例?

A:

import javassist.*;

public class JavassistExample {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("MyClass");

        // 添加属性
        CtField field = new CtField(CtClass.intType, "myField", cc);
        cc.addField(field);

        // 添加方法
        CtMethod method = CtNewMethod.make("public void setMyField(int value) { this.myField = value; }", cc);
        cc.addMethod(method);

        // 输出类的字节码
        cc.writeFile("output");
    }
}

这个示例创建了一个名为"MyClass"的新类,添加了一个名为"myField"的int类型属性和一个名为"setMyField"的公共void类型方法,该方法设置myField属性的值。最后,将该类的字节码写入名为"output"的文件夹中。

通过运行该示例,将在output文件夹中生成一个MyClass.class文件,该文件包含上述定义的类的字节码。这个示例只是Javassist的入门示例,Javassist还提供了许多其他功能,如修改现有类、动态代理等。

Q:需要添加什么 maven 依赖?

A:

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

最新版本是 3.29.1-GA

添加完 maven 依赖后运行,确实在 output 文件夹下生成了 MyClass.class
CleanShot2023-02-1816.57.14@2x

Q:如何修改现有类?

A:

使用Javassist修改现有类可以通过以下步骤完成:

  1. 获取类池(ClassPool)对象。
    ClassPool pool = ClassPool.getDefault();
    
  2. 使用类池获取要修改的类的CtClass对象。
    CtClass cc = pool.get("com.example.MyClass");
    
  3. 修改类的字节码,比如添加新的方法或属性,修改现有方法或属性等。下面是一个添加新方法的示例:
    CtMethod newMethod = CtNewMethod.make(
    "public void newMethod() { System.out.println(\"This is a new method!\"); }", cc);
    cc.addMethod(newMethod);
    
  4. 将修改后的字节码写入class文件或者加载到当前JVM中。
    cc.writeFile();
    
    或者
    cc.toClass();
    
    writeFile方法会将修改后的字节码写入到磁盘上,toClass方法会将字节码加载到当前JVM中。

下面是一个完整的示例,它会将现有类"com.example.MyClass"中的方法"oldMethod"修改为输出一条新的信息:

import javassist.*;

public class JavassistExample {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get("com.example.MyClass");

        CtMethod oldMethod = cc.getDeclaredMethod("oldMethod");
        oldMethod.setBody("System.out.println(\"This is a modified old method!\");");

        cc.writeFile();
    }
}

注意,为了执行这个示例,你需要将"com.example.MyClass"这个类的字节码文件放在类路径下。


执行前,我的 TestClass类,里面 test 方法,输出1
CleanShot2023-02-1817.07.50@2x

执行后
CleanShot2023-02-1817.08.29@2x

执行后的这个是新生成的文件,并不是覆盖掉原来的文件。

Q:cc.toClass() 和 cc.writeFile() 的区别

A:

toClass() 方法会将 Javassist 修改后的类直接转换为 java.lang.Class 对象,并将该对象加载到 JVM 中,因此你可以在代码中直接使用修改后的类。如果你调用了 toClass() 方法,Javassist 会尝试在 JVM 中加载修改后的类,如果加载成功,则返回一个 java.lang.Class 对象,否则会抛出一个异常。

writeFile() 方法会将 Javassist 修改后的类写入到磁盘上的一个 .class 文件中。这个文件可以用来替换原来的类文件,或者在以后的运行中使用,以避免每次运行时重新生成修改后的类。注意,writeFile() 方法只是将类保存到磁盘上,并没有将其加载到 JVM 中,因此你不能直接在代码中使用修改后的类,除非你手动加载它。

通常来说,toClass() 方法比 writeFile() 更方便,因为它可以直接在 JVM 中加载修改后的类,而不需要手动加载它。但是,如果你需要在多个应用程序之间共享修改后的类,或者想要在以后的运行中使用这个类,那么 writeFile() 方法可能更有用。

需要注意的是,toClass()writeFile() 方法可以结合使用,以便在将修改后的类加载到 JVM 中之前,先将它们保存到磁盘上。这样做可以避免每次运行时重新生成修改后的类。

Q:使用Javassist修改现有类,在其中每个方法执行前后输出当前系统时间,以此来计算方法执行的时间

A:

回答的步骤跟上一个问题的步骤是一样的,不过示例代码不同,而且有错误,最终修改完成后的示例:

import javassist.*;


public class JavassistExample {
    public static void main(String[] args) throws Exception {
        modifyClass();
    }

    public static void modifyClass() throws Exception {
        ClassPool cp = ClassPool.getDefault();
        CtClass cc = cp.get("com.example.MyClass");
        System.out.println("modifyClass " + cc.getName());
        CtMethod[] methods = cc.getDeclaredMethods();
        for (CtMethod method : methods) {
            String methodName = "modifyClass " + cc.getName() + "#" + method.getName();
            System.out.println(methodName);
            method.addLocalVariable("startNanos", CtClass.longType);
            method.insertBefore("{ startNanos = System.nanoTime(); }");
            method.insertAfter("System.out.println(System.nanoTime() - startNanos);");
        }
        // 重新生成字节码并返回
        cc.writeFile();
        cc.toClass();
    }
}

运行生成的代码

public class MyApp {
    public static void main(String[] args) throws Exception {
        // 在 Javassist 中修改 MyClass
        JavassistExample.modifyClass();

        // 已经加载到 JVM 内存中,这里使用的是修改后的类
        MyClass cc = new MyClass();
        cc.method1();
    }
}

修改后的字节码


public class MyClass {
    public MyClass() {
    }

    public void method1() {
        long startNanos = System.nanoTime();
        System.out.println("This is method 1");

        try {
            Thread.sleep(1000L);
        } catch (InterruptedException var6) {
            var6.printStackTrace();
        }

        Object var5 = null;
        System.out.println(System.nanoTime() - startNanos);
    }

    public void method2(String name, Integer age) {
        long startNanos = System.nanoTime();
        System.out.println("This is method 2");
        System.out.println(name);
        System.out.println(age);

        try {
            Thread.sleep(2000L);
        } catch (InterruptedException var8) {
            var8.printStackTrace();
        }

        Object var7 = null;
        System.out.println(System.nanoTime() - startNanos);
    }
}

Q:javaagent 的使用原理

A:

Java Agent 是一种基于 JVM 的动态代理技术,它可以在应用程序启动时通过参数的方式将一个 Jar 包加载到 JVM 中,从而实现对 Java 应用程序的动态监控和控制。Java Agent 的原理是通过 Java Instrumentation API,它提供了对 Java 应用程序字节码的操作能力,可以在 Java 应用程序启动时通过 Agent 将 Java 类进行动态修改,从而实现对应用程序的监控和控制。

在使用 Java Agent 时,需要编写一个代理程序,并将其打包成 Jar 文件。在代理程序中,需要实现 premain 方法,该方法会在应用程序启动时被自动调用。在 premain 方法中,可以通过 Instrumentation API 获取应用程序的类定义,并对其进行动态修改,从而实现对应用程序的增强和监控。

具体而言,Java Agent 可以在 premain 方法中使用 Instrumentation API 的 addTransformer() 方法注册一个 ClassFileTransformer 对象。ClassFileTransformer 对象可以在应用程序加载类文件时对类字节码进行转换。在转换过程中,可以使用字节码操作库,比如 Javassist,ASM 或 ByteBuddy 等,来修改类字节码。通过这种方式,Java Agent 可以实现对应用程序类的增强和监控。

在 Java Agent 中,还可以通过 Instrumentation API 提供的其他方法获取和修改类加载器、管理运行时代理、获取类定义信息等。这些功能可以用于实现更加复杂的增强和监控操作。

总之,Java Agent 是一种非常强大的工具,可以用于实现各种应用程序的增强和监控,比如性能监控、安全审计、调试等。它通过动态修改应用程序的字节码,可以实现非侵入式的监控和增强,而且对应用程序的性能影响非常小。


ClassFileTransformer 是 Java 中的一个接口,它是用于修改或增强字节码的核心接口之一。在 Java 应用程序启动时,通过 -javaagent 参数加载的 Java 代理会自动注册一个 ClassFileTransformer 实现,用于对 JVM 中加载的类进行字节码修改或增强操作。

ClassFileTransformer 接口只有一个方法 transform,该方法接收三个参数:原始类名、类加载器、原始类字节码。该方法的返回值是一个字节数组,表示经过修改后的新的字节码。ClassFileTransformer 可以在类加载过程中的各个阶段进行字节码的修改,例如在类加载之前,类加载时,类被重新转换时等。


Javassist

Javassist是一个开源的分析、编辑和创建Java字节码的类库;

相比ASM,Javassist提供了更加简单便捷的API,使用Javassist我们可以像写Java代码一样直接插入Java代码片段,让我们不再需要关注Java底层的字节码的和栈操作,仅需要学会如何使用Javassist的API即可实现字节码编辑。

学习Javassist可以阅读官方的入门教程:Getting Started with Javassist

API

描述
ClassPool ClassPool是一个存储CtClass的容器,如果调用get方法会搜索并创建一个表示该类的CtClass对象
CtClass CtClass表示的是从ClassPool获取的类对象,可对该类就行读写编辑等操作
CtMethod 可读写的类方法对象
CtConstructor 可读写的类构造方法对象
CtField 可读写的类成员变量对象

标识符

表达式 描述
$0, $1, $2, ... this和方法参数
$args Object[]类型的参数数组
$$ 所有的参数,如m($$)等价于m($1,$2,...)
$cflow(...) cflow变量
$r 返回类型,用于类型转换
$w 包装类型,用于类型转换
$_ 方法返回值
$sig 方法签名,返回java.lang.Class[]数组类型
$type 返回值类型,java.lang.Class类型
$class 当前类,java.lang.Class类型

Agent

JDK1.5开始,Java新增了Instrumentation(Java Agent API)和JVMTI(JVM Tool Interface)功能,允许JVM在加载某个class文件之前对其字节码进行修改,同时也支持对已加载的class(类字节码)进行重新加载(Retransform)。

利用Java Agent这一特性衍生出了APM(Application Performance Management,应用性能管理)、RASP(Runtime application self-protection,运行时应用自我保护)、IAST(Interactive Application Security Testing,交互式应用程序安全测试)等相关产品,它们都无一例外的使用了Instrumentation/JVMTI的API来实现动态修改Java类字节码并插入监控或检测代码。

Java Agent有两种运行模式:

  1. 启动Java程序时添加-javaagent(Instrumentation API实现方式)或-agentpath/-agentlib(JVMTI的实现方式)参数,如java -javaagent:/data/XXX.jar LingXeTest。
  2. JDK1.6新增了attach(附加方式)方式,可以对运行中的Java进程附加Agent。
    这两种运行方式的最大区别在于第一种方式只能在程序启动时指定Agent文件,而attach方式可以在Java程序运行后根据进程ID动态注入Agent到JVM。

Java Agent和普通的Java类并没有任何区别,普通的Java程序中规定了main方法为程序入口,而Java Agent则将premain(Agent模式)和agentmain(Attach模式)作为了Agent程序的入口,两者所接受的参数是完全一致的,如下:

public static void premain(String args, Instrumentation inst) {}
public static void agentmain(String args, Instrumentation inst) {}

Java Agent还限制了我们必须以jar包的形式运行或加载,我们必须将编写好的Agent程序打包成一个jar文件。

使用命令:

java -javaagent:/path/to/agent.jar -jar yourapp.jar

除此之外,Java Agent还强制要求了所有的jar文件中必须包含/META-INF/MANIFEST.MF文件,且该文件中必须定义好Premain-Class(Agent模式)或Agent-Class:(Agent模式)配置,可以通过 maven 插件实现:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.2.0</version>
    <configuration>
        <archive>
            <addMavenDescriptor>true</addMavenDescriptor>
            <index>true</index>
            <manifest>
                <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
                <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
            </manifest>
            <manifestEntries>
                <Premain-Class>com.example.PreMain</Premain-Class>
                <Specification-Version>${project.version}</Specification-Version>
                <Implementation-Version>${project.version}</Implementation-Version>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

将监控信息输出到日志文件

上面的例子中已经实现了,在方法运行前后记录时间戳来监控方法执行时间。
下一步计划,将执行时间记录下来。
在下一步,记录到时序数据库中,加载到 Prometheus

2023/02/12 17:42 下午 posted in  JVM