javaAgent动态字节码增强技术示例介绍~~

丁兴德
2023-12-01

注:为了方便大家阅读上一篇文章(解决pom配置编译javaAgent缺少依赖jar包的问题),特意转发另一个博主的文档,此文章非原创!!!

前言

介绍java agent之前也要介绍另一个概念JVMTIJVMTI是JDK提供的一套用于开发JVM监控, 问题定位与性能调优工具的通用编程接口(API)。通过JVM TI,我们可以开发各式各样的JVMTI Agent。这个Agent的表现形式是一个以C/C++语言编写的动态共享库。javaagent可以帮助我们快速使用JVMTI的功能,又不需要重写编写C/C++的底层库。

java agent是依赖java底层提供的一个叫instrumentJVMTI Agent。这个agent又叫JPLISAgent(Java Programming Language Instrumentation Services Agent)

简单来说,java agent是一个JVM的“插件”。在java运行命令中 java agent是一个参数,用来指定agent。

用法:

1)可以在加载class文件之前进行拦截并把字节码做修改。
2)可以在运行期对已加载类的字节码做变更,但是这种情况下会有很多的限制。
3)获取所有已经加载过的类
4)获取所有已经初始化过的类(执行过 clinit 方法,是上面的一个子集)
5)获取某个对象的大小
6)将某个jar加入到bootstrap classpath里作为高优先级被bootstrapClassloader 加载
7)将某个jar加入到classpath里供AppClassloard去加载
8)设置某些native方法的前缀,主要在查找native方法的时候做规则匹配

总的来说可以让JVM按照我们的预期逻辑去执行。

最主要的也是使用最广的功能就是对字节码的修改。通过对字节码的修改我们就可以实现对JAVA底层源码的重写,也正好可以满足我之前的需求。我们还可以做:

1)完全非侵入式的进行代码埋点,进行系统监控
2)修改JAVA底层源码,进行JVM自定义
3)实现AOP动态代理

实践

接下可以创建一个全新的Maven项目,编写Javaagent的案例。

项目需要添加两个maven依赖。

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.51</version>
</dependency>
<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.23.2-GA</version>
    <optional>true</optional>
</dependency>

定义代理类 定义我们的第一代理类FirstAgent。这个代理类只做一个事情,在Instrumentation添加了一个我们自定义的Transformer

public class FirstAgent {
    public static void premain(String agentArgs, Instrumentation inst){
        System.out.println("FirstAgent is Start.");
        inst.addTransformer(new FirstTransformer());
    }
}

实现ClassFileTransformer接口 接下来我们自定义一个ClassFileTransformer接口的实现 在这个类里,我们对原有的类做了两点修改:

增加了一个sex属性

重写toString方法
在修改字节码时使用了javassist这个字节码工具。Javaassist就是一个用来处理Java字节码的类库,有机会再详细介绍。

User

public class User {
    public String name;

    public User(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }
}
FirstTransformer

public class FirstTransformer implements ClassFileTransformer {
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        //只修改自定义的User类
        if(className.equals("User")){
            try {
                ClassPool classPool = ClassPool.getDefault();
                classPool.appendClassPath(new LoaderClassPath(loader));
                CtClass clazz = classPool.makeClass(new ByteArrayInputStream(classfileBuffer), false);
                //定义一个String类型的sex属性
                CtField param = new CtField(classPool.get("java.lang.String"), "sex", clazz);
                //设置属性为private
                param.setModifiers(Modifier.PRIVATE);
                //将属性加到类中,并设置属性的默认值为male
                clazz.addField(param, CtField.Initializer.constant("male"));
                //为刚才的sex属性添加GET SET 方法
                clazz.addMethod(CtNewMethod.setter("setSex", param));
                clazz.addMethod(CtNewMethod.getter("getSex", param));
                //重写toString方法,将sex属性加入返回结果中。
                CtMethod method = clazz.getDeclaredMethod("toString");
                method.setBody("return \"User{\" +\n" +
                        "                \"name='\" + name + '\\',' +\n" +
                        "                \"sex='\" + sex + '\\'' +\n" +
                        "                '}';");
                return clazz.toBytecode();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

打包

FirstAgent加上FirstTransformer就完成了第一个javaagent的全部代码编写部分。

接下来进行打包,生成第一个javaagent 打包可以直接使用maven的打包工具,用mvn install打包即可。

<build>
    <plugins>
        <plugin>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.1.2</version>
            <configuration>
                <archive>
                    <manifestEntries>
                        <Premain-Class>FirstAgent</Premain-Class>
                        <Boot-Class-Path>firstAgent.jar</Boot-Class-Path>
                        <Can-Redefine-Classes>false</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        <Can-Set-Native-Method-Prefix>false</Can-Set-Native-Method-Prefix>
                    </manifestEntries>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

这个里面有几个重要的配置:

Premain-Class :包含 premain 方法的类(类的全路径名)我们这里就是FirstAgent类
Boot-Class-Path :设置打包jar的文件名
Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes :true表示能重转换此代理所需的类,默认值为 false (可选)
Can-Set-Native-Method-Prefix:true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)

测试javaagent编写一个测试方法

public static void main(String[] args) {
    User user = new User("zane");
    System.out.println(JSON.toJSON(user));
    System.out.println(user.toString());
}

在没有使用我们编写的javaagent时,输出如下:

{"name":"zane"}
User{name='zane'}

在运用时添加参数 -javaagent:/Users/zane/javaagent/target/javaagent-1.0.jar,使用我们的代理类,注意路径修改成你本地的jar包地址。

javaagent的参数可以直接查java的帮助文档:

-javaagent:<jarpath>[=<选项>] 加载 Java 编程语言代理, 请参阅 java.lang.instrument
再运行输出如下:

FirstAgent is Start.
{"sex":"male","name":"zane"}
User{name='zane,sex='male'}

可以看到代理已经生效, user对象新增了一个sex的属性,并重写了toString方法。

 类似资料: