专栏名称: 架构师
架构师云集,三高架构(高可用、高性能、高稳定)、大数据、机器学习、Java架构、系统架构、大规模分布式架构、人工智能等的架构讨论交流,以及结合互联网技术的架构调整,大规模架构实战分享。欢迎有想法、乐于分享的架构师交流学习。
目录
相关文章推荐
中国网络空间安全协会  ·  关于邀请参加“网络产品安全能力提升计划(第二 ... ·  10 小时前  
中国网络空间安全协会  ·  关于邀请参加“网络产品安全能力提升计划(第二 ... ·  10 小时前  
盘口逻辑拆解  ·  蹲几个机会 ·  昨天  
盘口逻辑拆解  ·  蹲几个机会 ·  昨天  
幸福东台  ·  官宣!减免优惠 ·  3 天前  
51好读  ›  专栏  ›  架构师

Java agent原理浅析与编码实战

架构师  · 公众号  · 科技自媒体  · 2024-12-02 22:28

主要观点总结

本文介绍了Java Agent的实现原理和使用方法,包括premain agent和attach agent两种类型。文章详细阐述了如何使用javassist库修改字节码文件,并给出了premain agent和attach agent的示例代码。此外,还介绍了如何使用maven-assembly-plugin插件打包agent,并指出了一些注意事项。

关键观点总结

关键观点1: Java Agent的概念和作用


关键观点2: premain agent的原理和使用方法


关键观点3: attach agent的原理和使用方法


关键观点4: 使用maven-assembly-plugin插件打包agent




正文

架构师(JiaGouX)
我们都是架构师!
架构未来,你来不来?



在工作中有使用 sandbox 修改类中的方法体、返回等,现通过 demo 编码学习,了解其底层实现的原理。

agent 在 Java 生态中使用广泛,例如 arthas https://arthas.aliyun.com/doc/ 、trace 记录等框架等,都是通过 agent 实现。

Java 中的 agent 分为两类:

  1. premain agent: 在 Jvm 启动前,将 agent 随 jvm 一起启动生效。
  2. attach agent :在 Jvm 启动后,通过将 agent attach 到指定 pid 的 jvm 上生效。

不论是修改代码逻辑、修改函数返回,本质上来说都修改是加载类的字节码文件,而这两种 agent 分类均是通过 jvm 提供的扩展点在类加载前增加逻辑来修改字节码,达到修改实际生效的字节码文件的效果。

区别在于:

  • premain agent 由于是在 jvm 启动之前生效的,所以可以在类的初次加载前完成扩展逻辑,在类的初次加载时对类的字节码文件进行修改。

  • 而 attach agent 生效时,类都已经加载完毕,所以此时需要触发想要修改的类 重新进行加载,进而修改其字节码文件。


Premain agent


premain,在 main 函数之前启动的意思。

通常是通过 jvm 启动参数指定 agent。

例如:

-javaagent:/Users/xxx/IdeaProjects/AgentDemo/target/AgentDemo-1.0-SNAPSHOT-jar-with-dependencies.jar

执行入口

我们知道,当我们将 Java 代码打包成 jar 包时,需要指定入口 main 方法,main 方法即为整体的触发点。

Premain agent 也类似,需要指定一个 premain agent 的入口方法,不论是通过何种打包方式,最后在 jar 包的 MANIFEST.INF 文件中,通过:

Premain-Class: org.example.PreMainAgent

来指定该 premain agent 的入口类,同时默认调用以下两个方法:

//优先级更高
public static void premain(String agentArgs, Instrumentation inst)
    
public static void premain(String agentArgs)

也就是说,当工程中存在一个这样的类,打包成存在 premain 入口的 jar 包。

即可通过测试的 java 程序中加上对应 jvm 启动参数,实现 premain agent 随 jvm 启动。

package org.example;

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

import javassist.*;

public class PreMainAgent {

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("PreMainAgent premain enter.args=" + agentArgs);
    }
}

如何修改字节码文件

当进入触发入口的之后,通过入参中的 Instrumentation 来对字节码进行修改。

Instrumentation 为 jvm 提供的 允许开发者在 Java 程序运行时检查和修改应用程序的行为的工具类,在 agent 中主要是使用其对类的字节码文件进行修改。

void addTransformer(ClassFileTransformer transformer, boolean




    
 canRetransform);

Instrumentation 中存在 Transformer 的概念,transformer 是在类加载前可以对类进行修改、检查的扩展点。

当我们调用 inst.addTransformer(new DemoTransFormer(), true); 添加了一个 transformer 后,该 transformer 就会在类加载前生效。

例如下面这个 transformer,即可打印出所有加载类的名字。

static class DemoTransFormer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println("PreMainAgent start transform. className=" + className);
        return null;
    }
}

而其返回的是一个 byte[] 数组,即类加载后的字节码文件,也就是,如果想要对指定的类进行字节码文件修改,只需要在这修改,然后将修改后的字节码文件返回即可。

下面为示例,该 transformer org/example/simple/Hello 进行 transform,在这个类的 sayHello 方法的最后,插入了一个打印 hello world 的语句。然后将修改后的字节码文件返回。举例使用的是修改字节码文件使用的 Javaassist 库。

static class DemoTransFormer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println("PreMainAgent start transform. className=" + className);
        if ("org/example/simple/Hello".equals(className)) {
            try {
                // 从ClassPool获得CtClass对象
                final ClassPool classPool = ClassPool.getDefault();
                final CtClass clazz = classPool.get("org.example.simple.Hello");
                CtMethod sayHello = clazz.getDeclaredMethod("sayHello");
                String methodBody = "System.out.println("hello world!");";
                sayHello.insertAfter(methodBody);

                // 返回字节码,并且detachCtClass对象
                byte[] byteCode = clazz.toBytecode();
                //detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
                clazz.detach();
                return byteCode;
            } catch (Throwable ex) {
                System.out.println(ex.getClass().getCanonicalName());
                ex.printStackTrace();
            }
        }
        return null;
    }
}

最后完整的 premain agent 类如下:

package org.example;

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

import javassist.*;

public class PreMainAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("PreMainAgent premain enter.args=" + agentArgs);
        inst.addTransformer(new DemoTransFormer(), true);
    }

    static class DemoTransFormer implements ClassFileTransformer {
        @Override
        public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            if (className.startsWith("java") || className.startsWith("sun")
                    || className.startsWith("com/intellij") || className.startsWith("jdk")) {
                return null;
            }
            System.out.println("PreMainAgent start transform. className=" + className);
            if ("org/example/simple/Hello".equals(className)) {
                try {
                    // 从ClassPool获得CtClass对象
                    final ClassPool classPool = ClassPool.getDefault();
                    final CtClass clazz = classPool.get("org.example.simple.Hello");
                    CtMethod sayHello = clazz.getDeclaredMethod("sayHello");
                    String methodBody = "System.out.println("hello world!");";
                    sayHello.insertAfter(methodBody);

                    // 返回字节码,并且detachCtClass对象
                    byte[] byteCode = clazz.toBytecode();
                    //detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
                    clazz.detach();
                    return byteCode;
                } catch (Throwable ex) {
                    System.out.println(ex.getClass().getCanonicalName());
                    ex.printStackTrace();
                }
            }
            return null;
        }
    }
}

测试使用的 java 代码如下:

package org.example;

import org.example.simple.Hello;

/**
 * Hello world!
 */

public class App 
{
    public static void main( String[] args ) throws InterruptedException {
        System.out.println( "entry main." );
        while (true) {
            Hello.sayHello();
            Thread.sleep(1000);
        }
    }
}
package org.example.simple;

public class Hello {

    public static void sayHello(){
        System.out.println("hello");
    }
}

启动输出如下:

PreMainAgent premain enter.args=null
//先加载main方法所在的类
PreMainAgent start transform. className=org/example/App
//执行main方法
entry main.
//加载Hello类 触发transform
PreMainAgent start transform. className=org/example/simple/Hello
//transform后,增加打印hello world的语句,并循环执行
hello
hello world!
hello
hello world!
hello
hello world!

premain agent 的流程图大致如下:


Attach agent


attach agent 是在 jvm 已经启动之后,再附着到指定的 java 程序中,他需要解决和 premain agent 相同的两个问题:

  1. 执行入口
  2. 如何修改字节码文件

执行入口

premain agent 随着用户本身的 jvm 启动,因此从用户的视角来看的话,只启动 一个 有特殊 jvm 参数的程序。

而 attach agent 不同,本身已经有一个 java 程序了,然后需要再启动一个 java 程序,后启动的 java 程序需要连接到之前的 java 程序中,将 agent 的 jar 包发送过去并 load,从而完成 attach。

整个过程的流程图如下:

其中 attach 的入口与 premain 不同,为

//优先级更高
public static void agentmain (String agentArgs, Instrumentation inst)

public static void agentmain (String agentArgs)
package org.example;

import javassist.NotFoundException;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AttachAgent {

    public static void agentmain(String agentArgs, Instrumentation instrumentation) throws NotFoundException, ClassNotFoundException, UnmodifiableClassException, InterruptedException {
        System.out.println("entry AttachAgent main.");
    }
}

同样需要在 jar 包的 MANIFEST.INF 文件中,通过:

Agent-Class: org.example.AttachAgent

指定 attach 的 jar 包的 agent 入口即可。

连接并load attach agent的代码

package org.example;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;
import java.util.Scanner;

public class AttachAgentMain {

    public static






请到「今天看啥」查看全文