User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Java   ›   正文
Java

Java—JavaAgent探针

2022-05-16 01:16:25
2439  0 6
参考目录 隐藏
1) Java Agent 实现原理
2) 相关API初探
3) Instrumentation
4) addTransformer
5) redefineClasses
6) retransformClasses
7) ClassFileTransformer
8) Instrumentation接口源码
9) JVM启动前的agent实现
10) JVM启动后的agent实现
11) 入门案例
12) JVM启动前替换实现
13) JVM启动后替换实现
14) java.lang.instrument 解析
15) ClassFileLoadHook
16) Instrument的实现
17) premain 启动时加载
18) agentmain 启动时加载
19) ClassFileLoadHook回调实现
20) sun.instrument 源码分析
21) ClassFileTransFormer 转换器接口
22) TransformerManager
23) 触发转换的动作
24) 总结
25) JDK.attach 解析
26) 模型类
27) AttachPermisson
28) VirtualMachineDescriptor
29) AttachProvider
30) 核心类VirtualMacine
31) loadAgent()
32) loadAgentPath()
33) attach()
34) detach()
35) 监控JVM
36) AgentBuilder 构建
37) 入门例子
38) JVM启动前替换
39) 操作监控方法字节码
40) ThreadLocal链路追踪
41) JVM启动后替换实现
42) AgentBuilder的详解
43) 创建一个agent
44) AgentBuilder 类结构
45) 嵌套的内部类
46) 方法

阅读完需:约 59 分钟

Java Agent 直译为 Java 代理,也常常被称为 Java 探针技术。

Java Agent 是在 JDK1.5 引入的,是一种可以动态修改 Java 字节码的技术。Java 中的类编译后形成字节码被 JVM 执行,在 JVM 在执行这些字节码之前获取这些字节码的信息,并且通过字节码转换器对这些字节码进行修改,以此来完成一些额外的功能。

Java Agent 是一个不能独立运行 jar 包,它通过依附于目标程序的 JVM 进程,进行工作。启动时只需要在目标程序的启动参数中添加-javaagent 参数添加 ClassFileTransformer 字节码转换器,相当于在main方法前加了一个拦截器。

所谓Java Agent,其功能都是基于java.lang.instrument中的类去完成。Instrument提供了允许Java编程语言代理检测JVM上运行的程序的功能,而检测的机制就是修改字节码。Instrument位于rt.jar中,java.lang.instrument包下,使用Instrument可以用来检测或协助运行在 JVM中的程序;甚至对已加载class进行替换修改,这也就是我们常说的热部署、热加载。一句话总结Instrument:检测类的加载行为对其进行干扰(修改替换)

Instrument的实现基于JVMTI(Java Virtual Machine Tool Interface)的,所谓JVMTI就是一套由 Java 虚拟机提供的,为JVM 相关的工具提供的本地编程接口集合。JVMTI基于事件驱动,简单点讲就是在JVM运行层面添加一些钩子可以供开发者去自定义实现相关功能。

Java Agent 主要有以下功能

  • Java Agent 能够在加载 Java 字节码之前拦截并对字节码进行修改;
  • Java Agent 能够在 Jvm 运行期间修改已经加载的字节码;

Java Agent 的应用场景

  • IDE 的调试功能,例如 Eclipse、IntelliJ IDEA ;
  • 热部署功能,例如 JRebel、XRebel、spring-loaded;
  • 各种线上诊断工具,例如 Btrace、Greys,还有阿里的 Arthas;
  • 各种性能分析工具,例如 Visual VM、JConsole 等;
  • 全链路性能检测工具,例如 Skywalking、Pinpoint等;

Java Agent 实现原理

在了解Java Agent的实现原理之前,需要对Java类加载机制有一个较为清晰的认知。一种是在main方法执行之前,通过premain来执行,另一种是程序运行中修改,需通过JVM中的Attach实现,Attach的实现原理是基于JVMTI。

主要是在类加载之前,进行拦截,对字节码修改

下面我们分别介绍一下这些关键术语:

JVMTI 就是JVM Tool Interface,是 JVM 暴露出来给用户扩展使用的接口集合,JVMTI 是基于事件驱动的,JVM每执行一定的逻辑就会触发一些事件的回调接口,通过这些回调接口,用户可以自行扩展

JVMTI是实现 Debugger、Profiler、Monitor、Thread Analyser 等工具的统一基础,在主流 Java 虚拟机中都有实现

JVMTIAgent是一个动态库,利用JVMTI暴露出来的一些接口来干一些我们想做、但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:

  • Agent_OnLoad函数,如果agent是在启动时加载的,通过JVM参数设置
  • Agent_OnAttach函数,如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用Agent_OnAttach函数
  • Agent_OnUnload函数,在agent卸载时调用

javaagent 依赖于instrument的JVMTIAgent(Linux下对应的动态库是libinstrument.so),还有个别名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),专门为Java语言编写的插桩服务提供支持的

instrument 实现了Agent_OnLoad和Agent_OnAttach两方法,也就是说在使用时,agent既可以在启动时加载,也可以在运行时动态加载。其中启动时加载还可以通过类似-javaagent:jar包路径的方式来间接加载instrument agent,运行时动态加载依赖的是JVM的attach机制,通过发送load命令来加载agent

JVM Attach 是指 JVM 提供的一种进程间通信的功能,能让一个进程传命令给另一个进程,并进行一些内部的操作,比如进行线程 dump,那么就需要执行 jstack 进行,然后把 pid 等参数传递给需要 dump 的线程来执行


相关API初探

Instrumentation

java.lang.instrument包下关键的类为:java.lang.instrument.Instrumentation。该接口提供一系列替换转化class定义的方法。接下来看一下该接口的主要方法进行以下说明:

addTransformer

用于注册transformer。除了任何已注册的转换器所依赖的类的定义外,所有未来的类定义都将可以被transformer看到。当类被加载或被重新定义(redefine,可以是下方的redefineClasses触发)时,transformer将被调用。如果canRetransform为true,则表示当它们被retransform时(通过下方的retransformClasses),该transformer也会被调用。addTransformer共有如下两种重载方法:

void addTransformer(ClassFileTransformer transformer,boolean canRetransform)

void addTransformer(ClassFileTransformer transformer)

redefineClasses

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

此方法用于替换不引用现有类文件字节的类定义,就像从源代码重新编译以进行修复并继续调试时所做的那样。该方法对一系列ClassDefinition进行操作,以便允许同时对多个类进行相互依赖的更改(类a的重新定义可能需要类B的重新定义)。假如在redifine时,目标类正在执行中,那么执行中的行为还是按照原来字节码的定义执行,当对该类行为发起新的调用时,将会使用redefine之后的新行为。

注意:此redefine不会触发类的初始化行为

当然redefine时,并不是随心所欲,我们可以重新定义方法体、常量池、属性、但是不可以添加、移除、重命名方法和方法和入参,不能更改方法签名或更改继承。当然,在未来的版本中,这些限制可能不复存在。

在转换之前,不会检查、验证和安装类文件字节,如果结果字节出现错误,此方法将抛出异常。而抛出异常将不会有类被重新定义

retransformClasses

针对JVM已经加载的类进行转换,当类初始加载或重新定义类(redefineClass)时,可以被注册的ClassFileTransformer进行转化;但是针对那些已经加载完毕之后的类不会触发这个transform行为进而导致这些类无法被我们agent进行监听,所以可以通过retransformClasses触发一个事件,而这个事件可以被ClassFileTransformer捕获进而对这些类进行transform。

此方法将针对每一个通过addTransformer注册的且canRetransform是true的,进行调用其transform方法,转换后的类文件字节被安装成为类的新定义,从而拥有新的行为。

redefineClasses是自己提供字节码文件替换掉已存在的class文件,retransformClasses是在已存在的字节码文件上修改后再替换之。

ClassFileTransformer

在我们的agent中,需要提供该接口的实现,以便在JVM定义类之前转换class字节码文件,该接口中就提供了一个方法,此方法的实现可以转换提供的类文件并返回一个新的替换类文件:

byte[] transform(ClassLoader loader,
                 String className,
                 Class<?> classBeingRedefined,
                 ProtectionDomain protectionDomain,
                 byte[] classfileBuffer)
          throws IllegalClassFormatException

Instrumentation接口源码

public interface Instrumentation
{
    //添加ClassFileTransformer
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    //添加ClassFileTransformer
    void addTransformer(ClassFileTransformer transformer);

    //移除ClassFileTransformer
    boolean removeTransformer(ClassFileTransformer transformer);

    //是否可以被重新定义
    boolean isRetransformClassesSupported();

    // 重新转换提供的类集。此功能有助于检测已加载的类。
    // 最初加载类或重新定义类时,可以使用 ClassFileTransformer 转换初始类文件字节。
    // 此函数重新运行转换过程(无论之前是否发生过转换)。
    retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    // 返回当前 JVM 配置是否支持重新定义类。
    // 重新定义已加载的类的能力是 JVM 的可选能力。
    // 仅当代理 JAR 文件中的 Can-Redefine-Classes 清单属性设置为 true(如包规范中所述)并且 JVM 支持此功能时,才支持重新定义。
    boolean isRedefineClassesSupported();

    //重新定义Class文件
    void redefineClasses(ClassDefinition... definitions)
        throws ClassNotFoundException, UnmodifiableClassException;

    //是否可以修改Class文件
    boolean isModifiableClass(Class<?> theClass);

    //获取所有加载的Class
    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();

    //获取指定类加载器已经初始化的类
    @SuppressWarnings("rawtypes")
    Class[] getInitiatedClasses(ClassLoader loader);

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

    //添加指定jar包到启动类加载器检索路径
    void appendToBootstrapClassLoaderSearch(JarFile jarfile);

    //添加指定jar包到系统类加载检索路径
    void appendToSystemClassLoaderSearch(JarFile jarfile);

    //本地方法是否支持前缀
    boolean isNativeMethodPrefixSupported();

    //设置本地方法前缀,一般用于按前缀做匹配操作
    void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}

JVM启动前的agent实现

Instrument是JDK5开始引入,在JDK5中Instrument要求在目标JVM程序运行之前通过命令行参数javaagent来设置代理类,在JVM初始化之前,Instrument启动在JVM中设置回调函数,检测特点类加载情况完成实际增强工作。

-javaagent: jarpath[ =options]

这里jarpath就是我们的agent jar的路径,agent jar必须符合jar文件规范。代理JAR文件的manifest(META-INF/MANIFEST.MF)必须包含属性Premain-Class。此属性的值是代理类的类名。代理类必须实现一个公共静态premain方法,该方法原则上与主应用程序入口点类似。在JVM初始化之后,将按照指定代理的顺序调用每个主方法(premain),然后将调用实际应用程序的主方法(main)。每个premain方法必须按照启动顺序返回。

premain方法可以有如下两种重载方法,如果两者同时存在,则优先调用多参数的方法:

public static void premain(String agentArgs, Instrumentation inst);

public static void premain(String agentArgs);

JVM启动后的agent实现

JDK6开始为Instrument增加很多强大的功能,其中要指出的就是在JDK5中如果想要完成增强处理,必须是在目标JVM程序启动前通过命令行指定Instrument,然后在实际应用中,目标程序可能是已经运行中,针对这种场景下如果要保证 JVM不重启得以完成我们工作,这不是我们想要的,于是JDK6中Instrument提供了在JVM启动之后指定设置java agent达到Instrument的目的。

该实现需要确保以下3点:

  1. agent jar中manifest必须包含属性Agent-Class,其值为agent类名。
  2. agent类中必须包含公有静态方法agentmain
  3. system classload必须支持可以将agent jar添加到system class path。

agent jar将被添加到system class path,这个路径就是SystemClassLoader加载主应用程序的地方,agent class被加载后,JVM将会尝试执行它的agentmain方法,同样的,如果以下两个方法都存在,则优先执行多参数方法:

public static void agentmain(String agentArgs, Instrumentation inst);


public static void agentmain(String agentArgs);

看到这里,结合JVM前启动前agent的实现和JVM启动后agent的实现,可能想问是否可以在一个agent class中同时包含premain、agentmain呢,答案是可以的,只不过在JVM启动前不会执行agentmain,同样的,JVM启动后不会执行premain。

如果我们的agent无法启动(agent class无法被加载、agentmain出异常、agent class没有合法的agentmain方法等),JVM将不会终止!


入门案例

JVM启动前替换实现

我们已经知道通过配置-javaagent:文件.jar后,在java程序启动时候会执行premain方法。接下来我们使用javassist字节码增强的方式,来监控方法程序的执行耗时。

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态”AOP”框架。

加入依赖

        <dependency>
            <groupId>javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.12.1.GA</version>
            <type>jar</type>
        </dependency>

构建项目

MyAgent

public class MyAgent {

    //JVM 首先尝试在代理类上调用以下方法
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("this is my agent:" + agentArgs);
        MyMonitorTransformer monitor = new MyMonitorTransformer();
        inst.addTransformer(monitor);
    }

    //如果代理类没有实现上面的方法,那么 JVM 将尝试调用该方法
    public static void premain(String agentArgs) {
    }

}

MyMonitorTransformer

public class MyMonitorTransformer implements ClassFileTransformer {

    private static final Set<String> classNameSet = new HashSet<>();

    static {
        classNameSet.add("com.enmalvi.javaagent.ApiTest");
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        try {
            String currentClassName = className.replaceAll("/", ".");
            if (!classNameSet.contains(currentClassName)) { // 提升classNameSet中含有的类
                return null;
            }
            System.out.println("transform: [" + currentClassName + "]");

            CtClass ctClass = ClassPool.getDefault().get(currentClassName);
            CtBehavior[] methods = ctClass.getDeclaredBehaviors();
            for (CtBehavior method : methods) {
                enhanceMethod(method);
            }
            return ctClass.toBytecode();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;

    }


    private void enhanceMethod(CtBehavior method) throws Exception {
        if (method.isEmpty()) {
            return;
        }
        String methodName = method.getName();
        if ("main".equalsIgnoreCase(methodName)) {
            return;
        }

        final StringBuilder source = new StringBuilder();
        // 前置增强: 打入时间戳
        // 保留原有的代码处理逻辑
        source.append("{")
                .append("long start = System.nanoTime();\n") //前置增强: 打入时间戳
                .append("$_ = $proceed($$);\n")              //调用原有代码,类似于method();($$)表示所有的参数
                .append("System.out.print(\"method:[")
                .append(methodName).append("]\");").append("\n")
                .append("System.out.println(\" cost:[\" +(System.nanoTime() - start)+ \"ns]\");") // 后置增强,计算输出方法执行耗时
                .append("}");

        ExprEditor editor = new ExprEditor() {
            @Override
            public void edit(MethodCall methodCall) throws CannotCompileException {
                methodCall.replace(source.toString());
            }
        };
        method.instrument(editor);
    }

}

我们需要在Maven中配置,编译打包的插件,这样我们就可以很轻松的借助Maven生成Agent的jar包

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.0</version>
                <configuration>
                    <archive>
                        <!--自动添加META-INF/MANIFEST.MF -->
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <!--
                            Manifest-Version 文件版本

                            Premain-Class
                            包含 premain 方法的类(类的全路径名)main方法运行前代理

                            Agent-Class
                            包含 agentmain 方法的类(类的全路径名)main开始后可以修改类结构

                            Boot-Class-Path
                            设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。(可选)

                            Can-Redefine-Classes true
                            表示能重定义此代理所需的类,默认值为 false(可选)

                            Can-Retransform-Classes true
                            表示能重转换此代理所需的类,默认值为 false (可选)

                            Can-Set-Native-Method-Prefix true
                            表示能设置此代理所需的本机方法前缀,默认值为 false(可选)
                            -->
                            <Menifest-Version>1.0</Menifest-Version>
                            <Premain-Class>com.enmalvi.javaagent.JavaAgnet01.MyAgent</Premain-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

否则我们需要在resources下创建META-INF/MANIFEST.MF文件,文件内容如下,我们可以看出这个与Maven中的配置是一致的,然后通过配置编译器,借助编译器打包成jar包,需指定该文件

Manifest-Version: 1.0
Premain-Class: com.enmalvi.javaagent.JavaAgnet01.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

告示文件MANIFEST.MF参数说明:

  • Manifest-Version文件版本
  • Premain-Class包含 premain 方法的类(类的全路径名)main方法运行前代理,这个属性用来指定JVM启动时的代理agent,它必须包含premain方法,如果这个属性不存在,则JVM将终止。注意是类的全路径
  • Agent-Class包含 agentmain 方法的类(类的全路径名)main开始后可以修改类结构,如果agent实现支持在JVM启动后某个时间启动代理的机制,那么该属性则指定该代理类。如果该属性不存在,代理将不会启动。
  • Boot-Class-Path设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。(可选)该属性可以指定BootStrapClassLoad加载的路径。
  • Can-Redefine-Classes true表示能重定义此代理所需的类,默认值为 false(可选)该属性用来指定该agent是否针对redefineClass产生作用
  • Can-Retransform-Classes true表示能重转换此代理所需的类,默认值为 false (可选)该属性用来指定该agent是否针对retransformClass产生作用
  • Can-Set-Native-Method-Prefix true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)

最后通过Maven生成Agent的jar包,然后修改测试目标程序的启动器,添加JVM参数即可

参数示例

-javaagent:/Users/xujiahui/Me/IDEA/enmalvi/JavaAgent/target/JavaAgent-0.0.1-SNAPSHOT.jar=testargs

测试类

public class ApiTest {
    public static void main(String[] args) {
        ApiTest apiTest = new ApiTest();
        apiTest.echoHi();
    }

    private void echoHi(){
        System.out.println("hi agent");
    }
}

结果

this is my agent:testargs
transform: [com.enmalvi.javaagent.ApiTest]
hi agent
method:[echoHi] cost:[40833ns]

JVM启动后替换实现

最早 JDK 1.5发布 java.lang.instrument 包时,agent 是必须在 JVM 启动时,通过命令行选项附着(attach)上去。但在 JVM 正常运行时,加载 agent 没有意义,只有出现问题,需要诊断才需要附着 agent。JDK 1.6 实现了 attach-on-demand(按需附着),可以使用 Attach API 动态加载 agent。这个 Attach API 在 tools.jar 中。JVM 启动时默认不加载这个 jar 包,需要在 classpath 中额外指定。使用 Attach API 动态加载 agent 的示例代码如下:

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

public class AgentLoader {

    public static void main(String[] args) throws Exception {
        if (args.length < 2) {
            System.err.println("Usage: java -cp .:$JAVA_HOME/lib/tools.jar"
                    + " com.demo.AgentLoader <pid/name> <agent> [options]");
            System.exit(0);
        }

        String jvmPid = args[0];
        String agentJar = args[1];
        String options = args.length > 2 ? args[2] : null;
        for (VirtualMachineDescriptor jvm : VirtualMachine.list()) {
            if (jvm.displayName().contains(args[0])) {
                jvmPid = jvm.id();
                break;
            }
        }

        VirtualMachine jvm = VirtualMachine.attach(jvmPid);
        jvm.loadAgent(agentJar, options);
        jvm.detach();
    }
}

启动时加载 agent,-javaagent 传入的 jar 包需要在 MANIFEST.MF 中包含 Premain-Class 属性,此属性的值是 代理类 的名称,并且这个 代理类 要实现 premain 静态方法。启动后加载 agent 也是类似,通过 Agent-Class 属性指定 代理类,代理类 要实现 agentemain 静态方法。agent 被加载后,JVM 将尝试调用 agentmain 方法。

对于已经定义加载的类,需要使用重定义类(调用 Instrumentation.redefineClass)或重转换类(调用 Instrumentation.retransformClass)。

// 注册提供的转换器。如果 canRetransform 为 true,那么重转换类时也将调用该转换器
void addTransformer(ClassFileTransformer transformer, boolean canRetransform)
// 使用提供的类文件重定义提供的类集。新的类文件字节,通过 ClassDefinition 传入
void redefineClasses(ClassDefinition... definitions)
                     throws ClassNotFoundException, UnmodifiableClassException
// 重转换提供的类集。对于每个添加时 canRetransform 设为 true 的转换器,在这些转换器中调用 transform 方法 
void retransformClasses(Class<?>... classes)
                        throws UnmodifiableClassException

重定义类(redefineClass)从 JDK 1.5 开始支持,而重转换类(retransformClass)是 JDK 1.6 引入。相对来说,重转换类能力更强,当存在多个转换器时,重转换将由 transform 调用链组成,而重定义类无法组成调用链。重定义类能实现的逻辑,重转换类同样能完成,所以保留重定义类方法(Instrumentation.redefineClass)可能只是为了向后兼容。

agent class实现

public class Myattah {
    public static void agentmain(String agentArgs, Instrumentation inst) throws Exception {
        System.out.println("agentmainagentmainagentmainagentmain---agentmain");
        inst.addTransformer(new MyMonitorTransformer2(), true);
        // 关键点
        Class<?> aClass = Class.forName("com.enmalvi.javaagent.JavaAgnet01.MainController");
        if (inst.isModifiableClass(aClass)) {
            inst.retransformClasses(aClass);
        }

    }
}

MyMonitorTransformer2

public class MyMonitorTransformer2 implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        try {
            System.out.println(className+"---------");
            if (className.contains("MainController")){
                final ClassPool classPool = ClassPool.getDefault();
                final CtClass clazz = classPool.get("com.enmalvi.javaagent.JavaAgnet01.MainController");
                CtMethod convertToAbbr = clazz.getDeclaredMethod("test");
                String methodBody = "return \"hello world【version2]\";";
                convertToAbbr.setBody(methodBody);
                // 返回字节码,并且detachCtClass对象
                byte[] byteCode = clazz.toBytecode();
                //detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
                clazz.detach();
                return byteCode;
            }
            // 如果返回null则字节码不会被修改
            return null;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;

    }

}

MainController

@RestController
public class MainController {

    @GetMapping("agent")
    public String test(){
        System.out.println("hello world");
        return "test";
    }
}

这里关键的一点就是在我们的agentmain中手动retransform一下我们需要增强的类。

启动应用程序,并attach

这里我们需要获取目标JVM程序,并且进行attach加载我们的agent

    public static void main(String[] args) throws Exception {
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            //如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
            //然后加载 agent.jar 发送给该虚拟机
            System.out.println(vmd.displayName());
            if (vmd.displayName().equals("com.enmalvi.javaagent.JavaAgentApplication")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent("/Users/xujiahui/Me/IDEA/enmalvi/JavaAgent/target/JavaAgent-0.0.1-SNAPSHOT.jar");
                virtualMachine.detach();
            }
        }
    }

这里有个点很重要,在执行的时候容易报错找不到com.sun.tools.attach的包,这里需要在pom的做修改,强行添加

        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8.0</version>
            <scope>system</scope>
            <systemPath>/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/lib/tools.jar</systemPath>
        </dependency>

代理类的添加

<Menifest-Version>1.0</Menifest-Version>
<Premain-Class>com.enmalvi.javaagent.JavaAgnet05.MyAgent</Premain-Class>
<Agent-Class>com.enmalvi.javaagent.JavaAgnet01.Myattah</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>

关于Agent注意事项

VirtualMachine该类允许我们通过给 attach() 方法传入一个jvm的pid(进程id),远程连接到jvm上 。

我们可以通过 loadAgent() 方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以在 Class 加载前改变 Class 的字节码,也可以在 Class 加载后重新加载。

这里在修改类的时候特别需要注意,如果被修改的类同时被两个方法修改(Premain,Agent)如果修改那么就会出现错误,无法重定义类,不能同时作用于一个类。
(目前存疑,感觉是可以同时作用于一个类的,报错是–(class redefinition failed: attempted to delete a method)类重定义失败:试图删除方法)


java.lang.instrument 解析

instrument对于agent而言太重要了,所以我们先来了解一下instrument

写一个agent.jar可以分为以下几步

  1. 实现 ClassFileTransFormer 类 —-定义class的字节码转化逻辑
  2. 实现premain或者domain方法,调用Instrumentation接口,添加定义好的转换逻辑。
  3. 制作agent.jar,在MAINIFEST.MF中配置agent.jar的入口信息。
  4. 执行agent.jara.
    1. 启动时加载 比如java -javaagent:"proj-premain.jar=hello agent" -cp "target/classes/java/main" com.demo.App
    2. jvm运行时加载,获取目标JVM的对象,执行loadgent动作。
VirtualMachine jvm = VirtualMachine.attach(jvmPid);
jvm.loadAgent(agentJar, options);
jvm.detach();

JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口,这些接口可以供开发者扩展自行的逻辑。

ClassFileLoadHook

ClassFileLoadHook 事件。比如我想监听JVM加载某个类的事件,那么我们就可以实现一个回调函数赋给jvmtiEnv的回调方法集合里的ClassFileLoadHook(Class类加载事件),
那么当JVM进行类加载时就会触发回调函数,我们就可以在JVM加载类的时候做一些扩展操作。
比如上面提到的更改这个类的Class文件信息来增强这个类的方法。

agent函数 (Agent_OnLoad、Agent_OnAttach、Agent_OnUnload)

JVMTI运行时,一个JVMTIAgent对应一个jvmtiEnv或者是多个,JVMTIAgent是一个动态库,利用JVMTI暴露出来的接口来进行扩展。

主要有三个函数:

  • Agent_OnLoad方法:如果agent是在启动时加载的,那么在JVM启动过程中会执行这个agent里的
  • Agent_OnAttach函数(通过-agentlib加载vm参数中)Agent_OnAttach方法:如果agent不是在启动时加载的,而是attach到目标程序上,然后给对应的目标程序发送load命令来加载,则在加载过程中会调用Agent_OnAttach方法
  • Agent_OnUnload方法:在agent卸载时调用,要么主动下发卸载命令,要么jvm关闭是触发。

Instrument的实现

Instrument 就是一种 JVMTIAgent,它实现了Agent_OnLoad和Agent_OnAttach两个方法,也就是在使用时,Instrument既可以在启动时加载,也可以再运行时加动态加载

premain启动时加载就是在启动时添加JVM参数:-javaagent:XXXAgent.jar的方式
agentmain运行时加载是通过JVM的attach机制来实现,通过发送load命令来加载

premain 启动时加载

  1. Instrument agent启动时加载会实现Agent_OnLoad方法,具体实现逻辑如下:创建并初始化JPLISAgent
  2. 监听VMInit事件,在vm初始化完成之后执行下面逻辑
    1. 创建Instrumentation接口的实例,也就是InstrumentationImpl对象
    2. 监听ClassFileLoadHook事件(类加载事件,通过set callback)
    3. 调用InstrumentationImpl类的loadClassAndCallPremain方法,这个方法会调用javaagent的jar包中里的MANIFEST.MF里指定的Premain-Class类的premain方法
  3. 解析MANIFEST.MF里的参数,并根据这些参数来设置JPLISAgent里的内容

agentmain 启动时加载

Instrument agent运行时加载会使用Agent_OnAttach方法,会通过JVM的attach机制来请求目标JVM加载对应的agent,过程如下

  1. 创建并初始化JPLISAgent
  2. 解析javaagent里的MANIFEST.MF里的参数
  3. 创建InstrumentationImpl对象
  4. 监听ClassFileLoadHook事件
  5. 调用InstrumentationImpl类的loadClassAndCallAgentmain方法,这个方法会调用javaagent的jar包中里的MANIFEST.MF里指定的agentmain-Class类的agentmain方法

ClassFileLoadHook回调实现

启动时加载和运行时加载都是监听同一个jvmti事件那就是ClassFileLoadHook,这个是类加载的事件,在读取类文件字节码之后回调用的,这样就可以对字节码进行修改操作。

在类加载时修改类的字节码

在JVM加载类文件时,执行回调,加载Instrument agent,创建Instrumentation接口的实例并且执行premain方法,premain方法中注册自定义的ClassFileTransformer来对字节码文件进行操作,这个就是在加载时进行字节码增强的过程。

修改内存已经存在的类

那么如果java类已经加载完成了,在运行的过程中需要进行字节码增强的时候还可以使用Instrumentation接口的redifineClasses方法,
通过执行该方法,在JVM中相当于是创建了一个VM_RedifineClasses的VM_Operation,此时会stop_the_world,具体的执行过程如下:

  • 挨个遍历要批量重定义的 jvmtiClassDefinition
  • 然后读取新的字节码,如果有关注 ClassFileLoadHook 事件的,还会走对应的 transform 来对新的字节码再做修改字节码解析好,创建一个 klassOop
  • 对象对比新老类,并要求如下:
    • 父类是同一个实现的接口数也要相同,并且是相同的接口类
    • 访问符必须一致
    • 字段数和字段名要一致
    • 新增的方法必须是 private static/final 的
    • 可以删除修改方法
    • 对新类做字节码校验
    • 合并新老类的常量池
    • 如果老类上有断点,那都清除掉
    • 对老类做 JIT 去优化
    • 对新老方法匹配的方法的 jmethodId 做更新,将老的 jmethodId 更新到新的 method 上
    • 新类的常量池的 holer 指向老的类
    • 将新类和老类的一些属性做交换,比如常量池,methods,内部类
    • 初始化新的 vtable 和 itable
    • 交换 annotation 的 method、field、paramenter
    • 遍历所有当前类的子类,修改他们的 vtable 及 itable

sun.instrument 源码分析

这个是在premain和agentmain中如何使用instrument。注意三个方法

  • addTransformer(new Transformer, true) –jdk5
  • retransformClass(Target.class) –JDK6
  • redifineClasses(new ClassDefintion) — jdk5

这个三个方法就是用来执行转化字节码的

ClassFileTransFormer 转换器接口

default byte[] transform(ClassLoader loader,String className,Class<> classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer)

参数解析

  1. ClassLoader : 被加载类的loader
  2. className: 正在加载类的名称
  3. classBeingRedefined: 目标类为A,需要被重定义,被重定义的类记为A1,那么classBeingRedefined: 就代表着A1
  4. classFileBuffer当前类的定义

TransformerManager

instrument内拥有一个TransformerManager,包含了TransFormer数组。也就是说存在多个TransFormer时,TransFormer会形成一个链。上一个transformer对字节的修改,被传递到下一个transformer。

jdk6支持对本地方法的修改,所以会对分为4类,4类按顺序执行

  • 不支持retransform的 TransFormer
  • 不支持retransform 的 TransFormerNative
  • 支持retransform的 TransFormer
  • 支持retransform TransFormerNative

触发转换的动作

addTransformer(ClassFileTransformer transformer, boolean canRetransform);

这个函数的作用是,添加到transfomer数组中去。每当有一个类加载时会触发ClassFileLoadHook的回调,
这个回调会执行transfomer链,得到被加工过的字节码—保留在classFileBuffer类,最后被加载进方法区。

redefineClasses(ClassDefinition… definitions)

接受多个类的定义。再经过所transfomer数组(通常不添加)的加工,最后被加载进方法区。
这个方法是来源于jdk5
官方文档用来替换类内不存在任何引用(refrence)的类。往往修改一个类A也会导致相关联的B也要被修改。所以这里参数可以传入多个类。如果被redefine的方法,在栈stack中有了,此时再发生redefine。那么结果是,stack中方法继续执行老的定义,新的调用将会采用redefine后的方法。

retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

这个类在JDK6引入,和redefine类似但更强大。
可以对于已经加载的类(包括被redined的类)进行修改。
当类第一次加载时或者被redefined时,类的字节码byte[] ,会经过transfomer数组的加工。这个函数就是用来重放这个过程,不管内存中的类是否被装换过。

retransformClasses遵从以下步骤:

  1. 总是从类最初的定义开始(没有修改过的,原始的)
  2. 执行所有canRetransform 为true的transfomer,跳过canRetransform 为false的transfomer。
  3. 加载进内存,和redefine一样,存在栈里面的方法改变,对新方法的调用用新定义。

注意

  • 如果所有的transfomer为flase,那么他从最原始的类的定义出发,跳过所有transfomer,得到结果还是最原始的类。
  • 如果内存类已经被修改了,那么会发生原始的类定义替换被修改过的类

总结

只要使用了addTransformer,那么每有类加载的时候,就会触发transfomer链加工。redefine接受一组类的定义,然后触发transfomer的加工,替换进内存里。retransformClasses和redefineClasses类似,从最原始的类定义,开始重放transfomer链接的加工(只重放所有被标记为canRetransform为true的transfomer),替换进内存。


JDK.attach 解析

从JDK6开始引入,除了Solaris平台的Sun JVM支持远程的Attach,在其他平台都只允许Attach到本地的JVM上。

用Java语言编写的工具使用此API附加到目标虚拟机(VM),并将其tool agent 加载到目标VM中。

什么是attach机制?

说简单点就是jvm提供一种jvm进程间通信的能力,能让一个进程传命令给另外一个进程,并让它执行内部的一些操作,比如说我们为了让另外一个jvm进程把线程dump出来,那么我们跑了一个jstack的进程,然后传了个pid的参数,告诉它要哪个进程进行线程dump,既然是两个进程,那肯定涉及到进程间通信,以及传输协议的定义,比如要执行什么操作,传了什么参数等

涉及进程间通信。启动Attach Listener进程和VM进程通信。交换数据的方式使用的是SOCKET。

功能

  • 内存dump
  • 线程dump
  • 类信息统计(比如加载的类及大小以及实例个数等)
  • 动态加载agent
  • 动态设置vm flag(但是并不是所有的flag都可以设置的,因为有些flag是在jvm启动过程中使用的,是一次性的)
  • 打印vm flag
  • 获取系统属性等

代码结构

  • VirtualMachine是核心,通过操作它,来进行attach等
  • AttachPermission来负责权限控制功能
  • VirtualMachineDescriptor vm的模型,一个数据对象,包含pid或者vmname信息
  • AttachProvider各个JVM平台,自己负责对接实现。利用SPI给VirtualMachine的接口提供实现,截图中给出了OPENJDK14中默认对Hotspot的实现

模型类

AttachPermisson

用来做权限控制

权限目标名称 权限允许什么 允许此权限的风险
attachVirtualMachine 能够附加到另一个Java虚拟机并将代理加载到该VM。 这使攻击者可以控制目标VM,这可能会导致目标VM行为异常。
createAttachProvider 能够创建AttachProvider实例。 这使攻击者可以创建AttachProvider,该AttachProvider可以潜在地用于附加到其他Java虚拟机。

程序员通常不会直接创建AttachPermission对象。而是由安全策略代码基于读取安全策略文件来创建它们。

jvm在启动时可以设置一些权限,比如为了安全考虑禁止attach的行为。这个类就是在attach时被调用来检测是否能够attach。

VirtualMachineDescriptor

JVM信息的Bean。jvm有很多种,用来保存具体jvm的信息。

VirtualMachineDescriptor是用于描述Java虚拟机的容器类。它封装了标识目标虚拟机的标识符,以及AttachProvider在尝试附加到虚拟机时应使用的对它的引用。该标识符取决于实现,但是通常是每个Java虚拟机都在其自己的操作系统进程中运行的进程标识符(或pid)环境。

VirtualMachineDescriptor也有一个displayName。显示名称通常是工具可能显示给用户的人类可读字符串。例如,显示系统上运行的Java虚拟机列表的工具可能会使用显示名称而不是标识符。阿VirtualMachineDescriptor可以在没有被创建的显示名称。在这种情况下,标识符用作显示名称。

VirtualMachineDescriptor实例通常是通过调用该VirtualMachine.list() 方法来创建的。这将返回描述符的完整列表,以描述所有已安装的Java虚拟机attach providers。

AttachProvider

用来attach到jvm的接口。真正被使用的是各个厂家提供的jvm实现,是该类的子类。

核心类VirtualMacine

  • loadAgent
    加载 JPLTS agent,这个agent会加载agent.jar包 并执行 agentMain或者premain方法
  • attach(attach)
    依附到目标jvm
  • detach 离开

在此示例中,我们附加到由进程标识符标识的Java虚拟机2177。然后,使用提供的参数在目标进程中启动JMX管理代理。最后,客户端从目标VM分离。

VirtualMachine可安全地供多个并发线程使用。

一个Jvm的实现

loadAgent()

加载制作好的agent.jar包,并执行premain domain方法。

加载 JPLTS agent,这个agent会加载agent.jar包 并执行 agentMain或者premain方法

源码就不贴了,这里整个类的执行过程。可以总结为和jvm建立连接后,向jvm发送命令,传输agent.jar的path参数,并触发其中的premain或者domain方法

loadAgentPath()

用来加载 native方法库,即用他语言写的库。可以看出 load cmd 会有字段来标识是不是本地方法

attach()

这个方法的实现最终是在provider中实现,最终的结果是返回一个VirtualMachine对象。可以和jvm通信,让jvm执行指令。

流程

  • 检查权限
  • 测试是否可以attach
  • 成功attach
  • 以上检查都没问题,返回new VirtualMachineImpl(this, vmid);

不同平台实现不同

detach()

hotspot的实现比较直接,直接将fd文件设置为null

监控JVM

如果对JMXBean不陌生的就可以知道,这些bean是用来获取JVM运行信息:堆内存,线程状态,gc次数等。VirtualMacine提供这样的函数,向jvm发送指令,暴露端口。通过这个端口就可以获取JVM运行信息。

startManagementAgent(Properties agentProperties)

startLocalManagementAgent()
仅暴露本地端口,最终是从本地fd读取jvm信息。


AgentBuilder 构建

前面的代码几乎都是利用javassist来实现的,使用javassist虽说能实现我们的目的但是不够便利,下面我们尝试用Byte Buddy,便捷地创建 Java Agent

关于 Byte Buddy 的使用之前整理过基本用法

Java—Byte Buddy字节码编程

入门例子

JVM启动前替换

操作监控方法字节码

MyAgent

public class MyAgent {

    //JVM 首先尝试在代理类上调用以下方法
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("this is my agent:" + agentArgs);
        
        AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
            return builder
                    .method(ElementMatchers.any()) // 拦截任意方法
                    .intercept(MethodDelegation.to(MethodCostTime.class)); // 委托
        };

        AgentBuilder.Listener listener = new AgentBuilder.Listener() {
            @Override
            public void onDiscovery(String s, ClassLoader classLoader, JavaModule javaModule, boolean b) {

            }

            @Override
            public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule, boolean b, DynamicType dynamicType) {

            }

            @Override
            public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule, boolean b) {

            }

            @Override
            public void onError(String s, ClassLoader classLoader, JavaModule javaModule, boolean b, Throwable throwable) {

            }

            @Override
            public void onComplete(String s, ClassLoader classLoader, JavaModule javaModule, boolean b) {

            }

        };

        new AgentBuilder
                .Default()
                .type(ElementMatchers.nameStartsWith("com.enmalvi.javaagent.ApiTest2")) // 指定需要拦截的类
                .transform(transformer)
                .with(listener)
                .installOn(inst);
    }


    //如果代理类没有实现上面的方法,那么 JVM 将尝试调用该方法
    public static void premain(String agentArgs) {
    }

}

MethodCostTime

public class MethodCostTime {

    @RuntimeType
    public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
        long start = System.currentTimeMillis();
        try {
            // 原有函数执行
            return callable.call();
        } finally {
            System.out.println(method + " 方法耗时: " + (System.currentTimeMillis() - start) + "ms");
        }
    }

}

测试方法

public class ApiTest2 {

    public static void main(String[] args) throws InterruptedException {
        ApiTest2 apiTest = new ApiTest2();
        apiTest.echoHi2();
    }

    private void echoHi2() throws InterruptedException {
        System.out.println("hi agent");
        Thread.sleep((long) (Math.random() * 500));
    }

}

pom修改

<Menifest-Version>1.0</Menifest-Version>
<Premain-Class>com.enmalvi.javaagent.JavaAgnet02.MyAgent</Premain-Class>
<Agent-Class>com.enmalvi.javaagent.JavaAgnet01.Myattah</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>

添加启动包

-javaagent:/Users/xujiahui/Me/IDEA/enmalvi/JavaAgent/target/JavaAgent-0.0.1-SNAPSHOT.jar

结果

this is my agent:null
hi agent
private void com.enmalvi.javaagent.ApiTest2.echoHi2() throws java.lang.InterruptedException 方法耗时: 493ms
public static void com.enmalvi.javaagent.ApiTest2.main(java.lang.String[]) throws java.lang.InterruptedException 方法耗时: 493ms

ThreadLocal链路追踪

当业务程序代码在线上运行时,实例A、实例B、实例C,他们直接可能从上到下依次调用,为了能很好的监控程序的调用链路,我们需要对调用链路进行追踪监控。实例的外部可能是通过RPC、HTTP、SOCKET、WEBSERVICE等方式进行调用,内部是方法逻辑依次执行。外部例如http可以通过在头部写入追踪ID进行监控,内部使用threadlocal进行保存上下文关系。

ThreadLocal变量特殊的地方在于:对变量值的任何操作实际都是对这个变量在线程中的一份copy进行操作,不会影响另外一个线程中同一个ThreadLocal变量的值。

TrackContext

public class TrackContext {

    private static final ThreadLocal<String> trackLocal = new ThreadLocal<String>();

    public static void clear(){
        trackLocal.remove();
    }

    public static String getLinkId(){
        return trackLocal.get();
    }

    public static void setLinkId(String linkId){
        trackLocal.set(linkId);
    }

}

TrackManager

/**
 * 追踪管控
 */
public class TrackManager {

    private static final ThreadLocal<Stack<String>> track = new ThreadLocal<Stack<String>>();

    private static String createSpan() {
        Stack<String> stack = track.get();
        if (stack == null) {
            stack = new Stack<>();
            track.set(stack);
        }
        String linkId;
        if (stack.isEmpty()) {
            linkId = TrackContext.getLinkId();
            if (linkId == null) {
                linkId = "nvl";
                TrackContext.setLinkId(linkId);
            }
        } else {
            linkId = stack.peek();
            TrackContext.setLinkId(linkId);
        }
        return linkId;
    }

    public static String createEntrySpan() {
        String span = createSpan();
        Stack<String> stack = track.get();
        stack.push(span);
        return span;
    }


    public static String getExitSpan() {
        Stack<String> stack = track.get();
        if (stack == null || stack.isEmpty()) {
            TrackContext.clear();
            return null;
        }
        return stack.pop();
    }

    public static String getCurrentSpan() {
        Stack<String> stack = track.get();
        if (stack == null || stack.isEmpty()) {
            return null;
        }
        return stack.peek();
    }


}

MyAdvice

public class MyAdvice {

    @Advice.OnMethodEnter()
    public static void enter(@Advice.Origin("#t") String className, @Advice.Origin("#m") String methodName) {
        String linkId = TrackManager.getCurrentSpan();
        if (null == linkId) {
            linkId = UUID.randomUUID().toString();
            TrackContext.setLinkId(linkId);
        }
        String entrySpan = TrackManager.createEntrySpan();
        System.out.println("链路追踪:" + entrySpan + " " + className + "." + methodName);

    }

    @Advice.OnMethodExit()
    public static void exit(@Advice.Origin("#t") String className, @Advice.Origin("#m") String methodName) {
        TrackManager.getExitSpan();
    }

}

@Advice.OnMethodEnter

被标注此注解的方法会被织入到被bytebudy增强的方法前调用

  • Advice.Argument
    • 用来获取 被执行对象的field或者参数
  • Advice.This
    • 是当前advice对象的载体,标注之后,方法内部可以获取目前advice对象。

@Advice.OnMethodExit

被标注此注解的方法会被织入到被bytebudy增强的方法后调用

  • Advice.Argument
    • 用来获取 被执行对象的field或者参数
  • Advice.This
    • 是当前advice对象的载体,标注之后,方法内部可以获取目前advice对象。
  • Advice.Return
    • 给一个参数标识Advice.Return 后可以接收返回结果。
  • Advice.Thrown
    • 给一个参数标识Advice.Thrown 后可以接收抛出的异常。如果方法引发异常,则该方法将提前终止。如果由Advice.OnMethodEnter注释的方法引发异常,则不会调用由Advice.OnMethodExit方法注释的方法。如果检测的方法引发异常,则仅当Advice.OnMethodExit.onThrowable()属性设置为true(默认值)时,才调用由Advice.OnMethodExit注释的方法。

补充内容

这里Advice开头的都是net.bytebuddy.asm包下的,比如@Advice.Origin("#t")注解,而@Origin()注解是net.bytebuddy.implementation.bind.annotation包下的,是不一样的。

关于Advice类是这么描述的

建议包装器复制要在匹配方法之前和/或之后执行的蓝图方法的代码。为此,类的static方法由Advice.OnMethodEnter和/或Advice.OnMethodExit并提供给此类的实例。

也就是说这个是专门用来做前置后置处理的。

使用时也是用不一样的方式来调用

builder.visit(Advice.to(plugin.adviceClass()).on(point.buildMethodsMatcher()));

其实visit也是特殊的方法与Advice是匹配的

得出的结论是Advice是字节码的生成,方法的前置或后置调用,但是原本的intercept是方法的拦截调用生成。

反正就是如果是在方法的前置或者后置那就使用Advice相关的内容,如果是方法的运行时拦截处理,或者是代码生成都是可以用intercept相关的内容。

这两个方法是可以一起使用的,比如

AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
                    builder=builder.method(ElementMatchers.any()).intercept(MethodDelegation.to(MethodCostTime.class));
                    builder = builder.visit(Advice.to(plugin.adviceClass()).on(point.buildMethodsMatcher()));
                    return builder;
                };

后面回归正题,上面只是补充


MyAgent

public class MyAgent {

    //JVM 首先尝试在代理类上调用以下方法
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("基于javaagent链路追踪");

        AgentBuilder agentBuilder = new AgentBuilder.Default();


        AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
            builder = builder.visit(
                    Advice.to(MyAdvice.class)
                            .on(ElementMatchers.isMethod()
                                    .and(ElementMatchers.any()).and(ElementMatchers.not(ElementMatchers.nameStartsWith("main")))));
            return builder;
        };

        agentBuilder = agentBuilder.type(ElementMatchers.nameStartsWith("com.enmalvi.javaagent.ApiTest04")).transform(transformer);

        //监听
        AgentBuilder.Listener listener = new AgentBuilder.Listener() {
            @Override
            public void onDiscovery(String s, ClassLoader classLoader, JavaModule javaModule, boolean b) {

            }

            @Override
            public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule, boolean b, DynamicType dynamicType) {
                System.out.println("onTransformation:" + typeDescription);
            }

            @Override
            public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule, boolean b) {

            }

            @Override
            public void onError(String s, ClassLoader classLoader, JavaModule javaModule, boolean b, Throwable throwable) {

            }

            @Override
            public void onComplete(String s, ClassLoader classLoader, JavaModule javaModule, boolean b) {

            }

        };

        agentBuilder.with(listener).installOn(inst);

    }

    //如果代理类没有实现上面的方法,那么 JVM 将尝试调用该方法
    public static void premain(String agentArgs) {
    }

}

测试方法

public class ApiTest04 {

    public static void main(String[] args) {

        //线程一
        new Thread(() -> new ApiTest04().http_lt1()).start();

        //线程二
        new Thread(() -> {
            new ApiTest04().http_lt1();
        }).start();
    }


    public void http_lt1() {
        System.out.println("测试结果:hi1");
        http_lt2();
    }

    public void http_lt2() {
        System.out.println("测试结果:hi2");
        http_lt3();
    }

    public void http_lt3() {
        System.out.println("测试结果:hi3");
    }


}

依旧需要修改pom添加启动包

结果

基于javaagent链路追踪
onTransformation:class com.enmalvi.javaagent.ApiTest04
链路追踪:f7dce089-4591-4924-8130-bfe6781d582d com.enmalvi.javaagent.ApiTest04.http_lt1
测试结果:hi1
链路追踪:5b336087-dca9-4eb3-841a-806c19fbf1e9 com.enmalvi.javaagent.ApiTest04.http_lt1
测试结果:hi1
链路追踪:f7dce089-4591-4924-8130-bfe6781d582d com.enmalvi.javaagent.ApiTest04.http_lt2
测试结果:hi2
链路追踪:5b336087-dca9-4eb3-841a-806c19fbf1e9 com.enmalvi.javaagent.ApiTest04.http_lt2
测试结果:hi2
链路追踪:5b336087-dca9-4eb3-841a-806c19fbf1e9 com.enmalvi.javaagent.ApiTest04.http_lt3
链路追踪:f7dce089-4591-4924-8130-bfe6781d582d com.enmalvi.javaagent.ApiTest04.http_lt3
测试结果:hi3
测试结果:hi3

JVM启动后替换实现

Byte Buddy 对 Attach API 作了封装,屏蔽了对 tools.jar 的加载,可以直接使用 ByteBuddyAgent类:

public static void main(String[] args) {
        if (args.length < 2) {
            System.err.println("Usage: java com.demo.AgentLoader <pid/name> <agent> [options]");
            System.exit(0);
        }

        String jvmPid = args[0];
        String agentJar = args[1];
        String options = args.length > 2 ? args[2] : null;
        for (VirtualMachineDescriptor jvm : VirtualMachine.list()) {
            if (jvm.displayName().contains(args[0])) {
                jvmPid = jvm.id();
                break;
            }
        }
        ByteBuddyAgent.attach(new File(agentJar), jvmPid, options);
    }

按照上面的内容还可以写成

    public static void main(String[] args) {
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            //如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
            //然后加载 agent.jar 发送给该虚拟机
            System.out.println(vmd.displayName());
            if (vmd.displayName().equals("com.enmalvi.javaagent.JavaAgentApplication")) {
                ByteBuddyAgent.attach(new File("/Users/xujiahui/Me/IDEA/enmalvi/JavaAgent/target/JavaAgent-0.0.1-SNAPSHOT.jar"), "2963");
            }
        }
    }

其中 com.enmalvi.javaagent.JavaAgentApplication 这个是 SpringBoot 启动后的虚拟机的名字,可以通过它来判断是否要替换该JVM的类,和直接使用JVM的PID是一样的。

Byte Buddy 实现 agentmain:

public static void agentmain(String agentArgs, Instrumentation inst) {
    new AgentBuilder.Default()
            .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
            .disableClassFormatChanges()
            .type(ElementMatchers.named("com.demo.App"))
            .transform(new AgentBuilder.Transformer() {
                @Override
                public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
                                                        TypeDescription typeDescription,
                                                        ClassLoader classLoader,
                                                        JavaModule module) {
                    return builder.method(ElementMatchers.named("getGreeting"))
                            .intercept(FixedValue.value(agentArgs));
                }
            }).installOn(inst);
}

实现attach关键还是这个两个

.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.disableClassFormatChanges()

Byte Buddy 实现启动后动态加载 agent,官方提供了 Advice API 。

Advice API 实现原理上是,在被拦截方法内部的开始和结尾添加代码,如下右图所示。这样只更改了方法体,不更改方法签名,也没添加额外的方法,符合重定义类(redefineClass)和重转换类(retransformClass)的限制。


AgentBuilder的详解

创建一个agent

定义transformer,每有一个类加载时,触发transformer逻辑,对类进行匹配和修改。

bytebuddy提供了Agentbuilder,封装了一系列API来新建一个 agent。

示例

假设我们定义了一个注解ToString,我们匹配所有标注@ToString的类,修改toString方法,让其返回"transformed"。

class ToStringAgent {
  public static void premain(String arguments, Instrumentation instrumentation) {
    new AgentBuilder.Default()
        .type(isAnnotatedWith(ToString.class))
        .transform(new AgentBuilder.Transformer() {
      @Override
      public DynamicType.Builder transform(DynamicType.Builder builder,
                                              TypeDescription typeDescription,
                                              ClassLoader classloader) {
        return builder.method(named("toString"))
                      .intercept(FixedValue.value("transformed"));
      }
    }).installOn(instrumentation);
  }
}

  • type 接受 一个ElementMatcher 匹配 ToString注解
  • transform 接受AgentBuilder.Transformer 来描述修改逻辑
// 这里匹配类里面的`toString`方法,使用`intercept`修改返回值
 public DynamicType.Builder transform(DynamicType.Builder builder,
                                              TypeDescription typeDescription,
                                              ClassLoader classloader) {
        return builder.method(named("toString"))
                      .intercept(FixedValue.value("transformed"));
      }

  • installOn ,将修改,应用到instrumentation中。

AgentBuilder 类结构

ByteBuddy API 文档

嵌套的内部类

order Modifier and Type Interface Description
0 static interface AgentBuilder.CircularityLock 一个循环锁,阻止ClassFileLocator被提前使用。发生在,一个 transformation逻辑可能会导致另外的类被加载。如果不避免这样的循环依赖,就会抛出ClassCircularityError错误,导致类加载失败
1 static interface AgentBuilder.ClassFileBufferStrategy 关于class定义的字节缓存buffer如何被使用的策略
2 static class AgentBuilder.Default AgentBuilder的默认实现。默认情况下,byte buddy 无视任何被bootstrap loader加载的类和合成类。
1. Self-injection 和 rebase 模式开启
2. 为了避免class的格式发生改变,AgentBuilder.disableClassFormatChanges()
3. 所有的类型解析都是 PoolStrategy.Default#FAST,忽略所有的debug信息
3 static interface AgentBuilder.DescriptionStrategy 策略,描述当转化和定义一个类时,如何解决TypeDescription定义,这个解决指的是寻找+处理
4 static interface AgentBuilder.FallbackStrategy 失败策略,允许万一失败时,可以再次进行transformation或者redefine/retransformation。如果这样做了,可能就是会使用typepool,而不是一个已经加载的type description–相当于某个class的备份。
如果class loader不能找到所有的依赖的类,会抛出异常。使用typepool,由于时懒加载,所以会规避异常,直到被使用
5 static interface AgentBuilder.Identified 用来描述AgentBuilder处理哪几种mathcer,这个就是提供给mather一个标识,便于筛选
6 static interface AgentBuilder.Ignored 允许声明,忽略那些具体的方法
7 static interface AgentBuilder.InitializationStrategy 初始化策略,决定LoadedTypeInitializer是如何加载辅助类。agentbuilder不能重用TypeResolutionStrategy策略,是因为Javaagents不能获取到一个被transformation过,已经加载的类。
所以所以不同的初始化加载策略更有效
8 static interface AgentBuilder.InjectionStrategy 将辅助类注入classloader的策略
9 static interface AgentBuilder.InstallationListener 监听器,安装和重置class file transformer事件会唤醒这个监听器
10 static class AgentBuilder.LambdaInstrumentationStrategy lambda风格的语法特性开启
11 static interface AgentBuilder.Listener 一个an instrumentation 运行时会触发一堆事件,这个监听器是用来接受这些事件的
12 static interface AgentBuilder.LocationStrategy 修改一个类时,描述如何去创建ClassFileLocator的策略
13 static interface AgentBuilder.Matchable<T extends AgentBuilder.Matchable> 继承了mathcer的一个抽象实现,链式的结构
14 static interface AgentBuilder.PoolStrategy 描述AgentBuilder如何寻找和加载类TypeDescription的策略
15 static interface AgentBuilder.RawMatcher 一个matcher,用来匹配类并决定AgentBuilder.Transformer在ClassFileTransformer运行期间是否被使用
16 static interface AgentBuilder.RedefinitionListenable 允许在redefine的过程中注册一堆监听器
17 static class AgentBuilder.RedefinitionStrategy redefinition策略,描述agent如何控制已经被agent 加载到内存里面的类
18 static interface AgentBuilder.Transformer 应用DynamicType(定义的类修改),将DynamicType,对接到ClassFileTransformer
19 static interface AgentBuilder.TransformerDecorator AgentBuilder.Transformer的装饰器
20 static interface AgentBuilder.TypeStrategy 描述创建一个要被修改的类型,如何创建的方式

方法

相当于API的翻译,会有简单解释。

  • 加载过 往往意味值loaded,就是指classloader已经加载过目标类。bytebuddy通常就是感知类的加载,并且返回一个加工过的类。如此完成字节码修改的作用。
order return type method 描述
0 AgentBuilder with(ByteBuddy byteBuddy) ByteBuddy是用来创建DynamicType的。这里就是接受并生成一个AgentBuilder
1 AgentBuilder with(Listener listener) 注册监听器,创建agent的时候,会唤醒这个Listener。注意的是可以注册多个,如果早已经注册,也会重复唤醒,不会只唤醒一个
2 AgentBuilder with(CircularityLock circularityLock) 一个循环锁,被用于被执行的代码会加载新的类时,会获取这个锁。
当锁被获取时,任何classfiletransformer都不能transforming 任何类,默认,所有被创建的agent都使用一个CircularityLock,避免造成一个ClassCircularityError。
就是避免被执行的代码加载新类,同时其他agent也在转化这个类,造成一个循环依赖的一个锁
3 AgentBuilder with(PoolStrategy poolStrategy) 加载的类时的策略
4 AgentBuilder with(LocationStrategy locationStrategy) 寻找类的位置,利用 class name 加载类二进制数据的策略
5 AgentBuilder with(TypeStrategy typeStrategy) type 应该如何被transformed,eg, rebased或者redefined
6 AgentBuilder with(InitializationStrategy initializationStrategy) 生成一个类时的初始化策略,一个初始化策略是在类被加载后,启动时生效。初始化行为,必须在transformation行为之后,因为java agent 是在加载一个类之前生效。
默认,被注入到对象的初始化逻辑,需要查询一个全局对象来找到所有需要被注入到目标类型的对象
7 RedefinitionListenable.WithoutBatchStrategy with(RedefinitionStrategy redefinitionStrategy) 明确早已经被修改过且加被加载过类的优先级,目的是来安转transformer会用到。注意定一个redefinition strategy会重置之前的一些列的策略。
重要的是,绝大多数JVM不支持被加载过的类,类结构被重新修改,因此默认打开AgentBuilder#disableClassFormatChanges()
8 AgentBuilder with(LambdaInstrumentationStrategy lambdaInstrumentationStrategy) Lambda表达式特性,忽虑掉
9 AgentBuilder with(DescriptionStrategy descriptionStrategy)) 定义被加载类的,解决(resolving)策略。
10 AgentBuilder with(FallbackStrategy fallbackStrategy) 失败策略,比如转换失败时,允许重试一次
11 AgentBuilder with(ClassFileBufferStrategy classFileBufferStrategy) 类定义的buffer如何被使用
12 AgentBuilder with(InstallationListener installationListener) agent安转时,会有安装事件,这个时间会唤醒这个监听器。这个监听器,只有在classfiletransformer安装时会被触发。
classfiletransformer的安装就是agent builder通过创建的ResettableClassFileTransformer执行installation methods 和uninstalled
13 AgentBuilder with(InjectionStrategy injectionStrategy) 辅助类注入到classloader的策略
14 AgentBuilder enableNativeMethodPrefix(String prefix) 给被修改的方法,添加本地方法前缀。注意这个前缀也能给非本地方法用。当Instrumentation安装agent是,也能用这个前缀来注册
15 AgentBuilder disableNativeMethodPrefix() 关闭一个本地方法的前缀
16 AgentBuilder disableClassFormatChanges() 禁止所有对class file 的改变,当时用这个策略时,就不存在使用rebase的可能—被修改的方法会完全被替代,而不是被重名。
可去搜索rebase和redefine的区别。还有就是在生成类型是,加载初始化动作。
这个行为和设置InitializationStrategy.NoOp和TypeStrategy.Default#REDEFINE_FROZEN是等价的。也等同于配置ByteBuddy为Implementation.Context.Disabled。
使用这个策略是让bute buddy 穿件一个 冻结的 instrumented类型和排除调所有的配置行为
17 AgentBuilder assureReadEdgeTo(Instrumentation instrumentation, Class<?>… type) module ,jdk9的概念,不用管
18 AgentBuilder assureReadEdgeTo(Instrumentation instrumentation, JavaModule… module)
19 AgentBuilder assureReadEdgeTo(Instrumentation instrumentation, Collection<? extends JavaModule> modules)
20 AgentBuilder assureReadEdgeFromAndTo(Instrumentation instrumentation, Class<?>… type)
21 AgentBuilder assureReadEdgeFromAndTo(Instrumentation instrumentation, JavaModule… module)
22 AgentBuilder assureReadEdgeFromAndTo(Instrumentation instrumentation, Collection<? extends JavaModule> modules) 都是moudle权限控制相关的
23 Identified.Narrowabler type(ElementMatcher<? super TypeDescription> typeMatcher) 每一个类被加载时会触发一个事件,被agent感知到。这个方法就是用来配一个被加载的类型,目的是应用AgentBuilder.Transformer的修改,在这个类型被加载之前。
如果有几个matcher都命中一个类型,那么最后一个生效。
如果这个matcher是链式的,即还有下一个matcher,那么matcher执行的顺序就是注册的顺序,后面matcher对应的transformations会覆盖前面的。
如果不想被覆盖可以注册为terminal最后一个Identified.Extendable#asTerminalTransformation(),这样就不有有下一个matcher被使用。
注意: AgentBuilder#ignore(ElementMatcher)会在mather之前被应用,来排除忽略的类。
24 Identified.Narrowabler type(ElementMatcher<? super TypeDescription> typeMatcher, ElementMatcher<? super ClassLoader> classLoaderMatcher) 唯一的不同是添加了classLoaderMatcher参数,这个classLoader被用来加载目标类的loader
25 Identified.Narrowabler Identified.Narrowable type(ElementMatcher typeMatcher,ElementMatcher classLoaderMatcher,ElementMatcher moduleMatcher); moduleMatcher jdk9的语法,只是多了层判断
26 Identified.Narrowabler type(RawMatcher matcher); 文档只说用来决定Transformer是否被使用
27 Ignored ignore(ElementMatcher<? super TypeDescription> typeMatcher); 无视方法的matcher
28 Ignored ignore(ElementMatcher<? super TypeDescription> typeMatcher, ElementMatcher<? super ClassLoader> classLoaderMatcher)
29 Ignored ignore(RawMatcher rawMatcher)
30 ClassFileTransformer makeRaw() ClassFileTransformer是jdk instrument包的类。这个方法是创建一个原生的这样的类。当时用原生的ClassFileTransformer,InstallationListener回调就不会生效,而且RedefinitionStrategy的策略也不会应用到当前加载的类中
31 ResettableClassFileTransformer installOn(Instrumentation instrumentation) 单纯使用instrument时,往往是在premain或者agentmain函数里,执行instrumentation.addTransformer(ClassFileTransformer),installOn 就是将创建一个ResettableClassFileTransformer添加进instrumentation。
如果retransformation是打开的,那么retransformed时让ClassFileTransformer再次生效。意思是ClassFileTransformer是链式的,每个ClassFileTransformer被添加的时候有个canRetransformed属性。
retransformed,就是修改已经加载内存里面的类,执行时会再次触发ClassFileTransformer,这时只会触发canRetransformed为true的ClassFileTransformer
32 ResettableClassFileTransformer ResettableClassFileTransformer installOn(Instrumentation instrumentation, TransformerDecorator transformerDecorator); installOn时接受额外的Transformer
33 ResettableClassFileTransformer installOnByteBuddyAgent() ByteBuddyAgent是bytebutty对jdk.attach和agentmain的一个封装。根据目标jvm实例的pid,在运行中attach上去,发出加载agent.jar包命令
34 ResettableClassFileTransformer installOnByteBuddyAgent(TransformerDecorator transformerDecorator);

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

6 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 593474浏览

随机文章
Java—并发编程(八)线程池– (2) 线程池的原理
3年前
Spring Boot 的配置文件 application.properties+多环境切换
5年前
Redis—StringRedisTemplate和RedisTemplate
5年前
PostgreSQL—如何查找TOP SQL
1年前
SpringSecurity—OAuth 2(一)
5年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 593474 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付