字节码增强技术之 Java Agent 入门

createh52个月前 (02-01)技术教程13

前言

分布式链路追踪中为了获取服务之间调用链信息,采集器通常需要在方法的前后做埋点。在 Java 生态中,常见的埋点方式有两种:

  1. 依赖 SDK 手动埋点;
  2. 利用 Java Agent 技术来做无侵入埋点。

我们所熟知的分布式监控系统,是 Zipkin 开始的,最经典的是搞懂 X-B3 Ttrace 协议,使用 Brave SDK,手动埋点生成 Trace。但是 SDK 埋点的方式,对业务代码存在侵入性,当升级埋点时,必须要做代码的变更。

那么如何和业务逻辑解绑呢?

Java 还提供了另外一种方式:依赖 Java Agent 技术,修改目标方法的字节码,做到无侵入的埋点。这种利用 Java Agent 的方式的采集器,也叫做探针。在应用程序启动时使用 -javaagent 参数 ,或者运行时使用 attach(pid) 方式,就可以将探针包注入目标应用程序,完成埋点的植入。对业务代码无侵入的方式,可以做到无感的热升级。用户不需要理解深层的原理,就可以使用完整的监控服务

Java Agent 简介

Java Agent 是 Java 1.5 版本之后引?的特性,其主要作?是在 class 被加载之前对其拦截,已插?我们的监听字节码。使用 Java 的Instrumentation 接口(java.lang.instrument)来编写 Agent。

基本的思路是在 JVM 启动的时候添加一个代理(Java Agent),每个代理是一个 Jar 包,其 MANIFEST.MF 文件里指定了代理类,这个代理类包含一个 premain 方法。JVM 在类加载时候会先执行代理类的 premain 方法,再执行 Java 程序本身的 main 方法,这就是 premain 名字的来源。在 premain 方法中可以对加载前的 class 文件进行修改。

这种机制可以认为是虚拟机级别的 AOP,无需对原有应用做任何修改,就可以实现类的动态修改和增强。

从 JDK 1.6 开始支持更加强大的动态 Instrument,在JVM 启动后通过 Attach(pid) 远程加载。

注意:

无论是通过 Native 的方式还是通过 Java Instrumentation 接口的方式来编写 Agent,它们的工作都是借助 JVMTI 来进行完成。JVMTI 是一套 Native 接口,在 Java 1.5 之前,要实现一个 Agent 只能通过编写Native 代码来实现。

Java Instrumentation 核心方法

Instrumentation 是 java.lang.instrument 包下的一个接口,这个接口的方法提供了注册类文件转换器、获取所有已加载的类等功能,允许我们在对已加载和未加载的类进行修改,实现 AOP、性能监控等功能。

常用方法:

/**
 * 为 Instrumentation 注册一个类文件转换器,可以修改读取类文件字节码
 */
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

/**
 * 对JVM已经加载的类重新触发类加载
 */
void retransformClasses(Class... classes) throws UnmodifiableClassException;

/**
 * 获取当前 JVM 加载的所有类对象
 */
Class[] getAllLoadedClasses()

它的 addTransformer 给 Instrumentation 注册一个 transformer,transformer 是 ClassFileTransformer 接口的实例,这个接口就只有一个 transform 方法,调用 addTransformer 设置 transformer 以后,后续 JVM 加载所有类之前都会被这个 transform 方法拦截,这个方法接收原类文件的字节数组,返回转换过的字节数组,在这个方法中可以做任意的类文件改写。

public class MyClassTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classBytes) throws IllegalClassFormatException {
        // 在这里读取、转换类文件
        return classBytes;
    }
}

Java Agent 核心流程

Java Agent 装载时序图(premain):

Class 装载时序图:

Java Agent 所使用的 Instrumentation 依赖 JVMTI 实现,当然也可以绕过 Instrumentation 直接使用 JVMTI 实现 Agent。JVMTI 与 JDI 组成了 Java 平台调试体系(JPDA)的主要能力。

Java Agent 使?

Java Agent 其实就是?个特殊的 Jar 包,它并不能单独启动的,而必须依附于一个 JVM 进程,可以看作是 JVM 的一个寄生插件,使用 Instrumentation 的 API 用来读取和改写当前 JVM 的类文,通过 -javaagent:xxx.jar 引??标应?。

那这个Jar 和 普通的 Jar 有什么区别么?

Agent 需要打包成一个jar包,在 Maininfe.MF 属性中指定“Premain-Class”或者“Agent-Class”,且需根据需求定义 Can-Redefine-Classes 和 Can-Retransform-Classes。

Java Agent Jar 包 MANIFEST.MF 配置参数:

Manifest-Version: 1.0
#动态 agent 类 
Agent-Class: com.zuozewei.javaagent01.Agent 
#静态 agent 类
Premain-Class: com.zuozewei.javaagent01.Agent
是否允许重复装载
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_112

demo 预演

1、创建 POM 项目 Java Agent,项目结构如下:

2、修改 pom 文件:



    
        parent
        com.zuozewei
        1.0-SNAPSHOT
    
    4.0.0
    jar

    javaagent01

    
        agent

        
            
                org.apache.maven.plugins
                maven-jar-plugin
                2.3.1
                
                    
                        src/main/resources/META-INF/MANIFEST.MF
                    
                
            

            
                maven-assembly-plugin
                
                    ${basedir}
                    
                        true
                        
                            true
                        
                        
                            com.zuozewei.javaagent01.Agent
                        
                    
                    
                        jar-with-dependencies
                    
                
            
            
                org.apache.maven.plugins
                maven-compiler-plugin
                
                    1.7
                    1.7
                
            
        
    

3、创建 AgentMain 类,实现控制台打印,addTransformer 给 Instrumentation 注册一个 transformer。

package com.zuozewei.javaagent01;

import java.lang.instrument.Instrumentation;

public class Agent {

//    public static void premain(String agentArgs) {
//        System.out.println("我是一个萌萌哒的 Java Agent");
//        try {
//            Thread.sleep(2000L);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
//    }

    public static void premain(String agentArgs, Instrumentation instrumentation)  {

        instrumentation.addTransformer(new ClassFileTransformerDemo());

        System.out.println("7DGroup Java Agent");
    }

}

4、创建 ClassFileTransformerDemo 类,拦截并打印所有类名。

package com.zuozewei.javaagent01;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class ClassFileTransformerDemo implements ClassFileTransformer {

    public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)  {
        System.out.println("className: " + className);
        if (!className.equalsIgnoreCase("com/zuozewei/Dog")) {
            return null;
        }
        return getBytesFromFile("/Users/zuozewei/IdeaProjects/javaagent/example01/target/classes/com/zuozewei/Dog.class");
    }

    public static byte[] getBytesFromFile(String fileName) {
        File file = new File(fileName);
        try (InputStream is = new FileInputStream(file)) {
            // precondition

            long length = file.length();
            byte[] bytes = new byte[(int) length];

            // Read in the bytes
            int offset = 0;
            int numRead = 0;
            while (offset = 0) {
                offset += numRead;
            }

            if (offset < bytes.length) {
                throw new IOException("Could not completely read file "
                        + file.getName());
            }
            is.close();
            return bytes;
        } catch (Exception e) {
            System.out.println("error occurs in _ClassTransformer!"
                    + e.getClass().getName());
            return null;
        }
    }

}

5、定义需要修改的项目 example01

6、实现需要修改的类的。

main:

package com.zuozewei;

public class Main {

    public static void main(String[] args) {
        System.out.println("7DGroup");

        System.out.println(new Dog().hello());
//        System.out.println(new Cat().hello());
    }

}

Dog:

package com.zuozewei;

public class Dog {

    public int hello() {
        return 0;
    }

}

7、运行 example01 的 main 方法:

8、打包 javaagent 项目生成 jar 文件,并将 java 文件同 example01 项目的 jar 放在同一个目录下如上图(放在同一个目录为了方便执行)

执行如下命令:

java -jar -javaagent:agent.jar  example.jar

实现了我们的功能,执行结果如下:

总结

本文详细介绍 Java Agent 启动加载实现字节码增强关键技术的实现细节,字节码增强技术为测试人员进行性能监控提供了一种新的思路。目前众多开源监控产品已经提供了丰富的 Java 探针库,作为监控服务的提供者,进一步降低了开发成本,不过开发门槛比较高,对测试人员来说有很大的一部分的学习成本。

源码地址:

https://github.com/zuozewei/blog-example/tree/master/Performance-testing/04-full-link/javaagent

作者:zuozewei
链接:
https://juejin.cn/post/7032905730390753316

相关文章

如何开发一个轻量且高效的SDK(怎么开发sdk)

背景相信大家都使用过Maven,我们平时要使用公共的API,只需要导入Maven依赖即可。然后就可以直接调用里面的方法了,非常的方便。最近的项目有个需求,需要将业务系统与公司内部的OA系统做交互,比如...

一篇文章,手把手教你运行百度离线人脸识别SDK-Java版

本篇文章为简单的讲解百度离线人脸识别SDK-Java版本的工程运行;01注册百度AI开放平台登录http://ai.baidu.com进入百度AI开放平台,进行注册;02找到人脸识别模块,下载离线SD...

Easysearch Java SDK 2.0.x 使用指南(二)

在 上一篇文章 中,我们介绍了 Easysearch Java SDK 2.0.x 的基本使用和批量操作。本文将深入探讨索引管理相关的功能,包括索引的创建、删除、开关、刷新、滚动等操作,以及新版 SD...

微信开发最新Java SDK来了:WxJava3.9.0

引言【关注极客欣,每天看资讯】哈喽,各位小伙伴们,大家好!很高兴又跟大家见面了,今天是周一,继续为大家分享科技资讯。前言今天我们来说一说微信开发,随着微信生态圈的逐步建立和发展,如今,微信的活跃用户体...