字节码增强技术ByteBuddy和探针技术-JavaAgent

如何动态修改运行中的字节码,有两个步骤,1:生成新的字节码 2:替换旧的字节码

  • 谁去修改字节码,就是字节码增强框架做的事。
  • 修改后的字节码数据是怎么生效的,主要由Java Agent技术来实现。

1.字节码增强框架

Java 是一种强类型的编程语言,即要求所有变量和对象都有一个确定的类型,如果在赋值操作中出现类型不兼容的情况,就会抛出异常。强类型检查在大多数情况下是可行的,然而在某些特殊场景下,强类型检查则成了巨大的障碍。

我们在做一些通用工具封装的时候,类型检查就成了很大障碍。比如我们编写一个通用的Dao实现数据操作,我们根本不知道用户要调用的方法会传几个参数、每个参数是什么类型、需求变更又会出现什么类型,几乎没法在方法中引用用户方法中定义的任何类型。我们绝大多数通用工具封装都采用了反射机制,通过反射可以知道用户调用的方法或属性,但是Java反射有很多缺陷:

  1. 反射性能很差
  2. 反射能绕开类型安全检查,不安全,比如权限暴力破解

现有的比较流行的java编程语言代码生成库:

Java Proxy

        Java Proxy 是 JDK 自带的一个代理工具,它允许为实现了一系列接口的类生成代理类。Java Proxy 要求目标类必须实现接口是一个非常大限制,例如,在某些场景中,目标类没有实现任何接口且无法修改目标类的代码实现,Java Proxy 就无法对其进行扩展和增强了。

ASM

        ASM 是一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。使用起来不是那么友好,但性能较好。

CGLIB

        CGLIB 诞生于 Java 初期,但不幸的是没有跟上 Java 平台的发展。虽然 CGLIB 本身是一个相当强大的库,但也变得越来越复杂。鉴于此,导致许多用户放弃了 CGLIB 。

Javassist

        Javassist 的使用对 Java 开发者来说是非常友好的,它使用Java 源代码字符串和 Javassist 提供的一些简单 API ,共同拼凑出用户想要的 Java 类,Javassist 自带一个编译器,拼凑好的 Java 类在程序运行时会被编译成为字节码并加载到 JVM 中。Javassist 库简单易用,而且使用 Java 语法构建类与平时写 Java 代码类似,但是 Javassist 编译器在性能上比不了 Javac 编译器,而且在动态组合字符串以实现比较复杂的逻辑时容易出错。

Byte Buddy

        ByteBuddy是一个代码生成和操作库,用于在 Java 应用程序运行时创建和修改 Java 类,而无需编译器的帮助。除了 Java 类库附带的代码生成实用程序外,Byte Buddy 还允许创建任意类,并且不限于实现用于创建运行时代理的接口。此外,Byte Buddy 提供了一种方便的 API,可以使用 Java 代理或在构建过程中手动更改类。无需理解字节码指令,即可使用简单的 API 就能很容易操作字节码,控制类和方法。已支持Java 11,库轻量,仅取决于Java字节代码解析器库ASM的访问者API,它本身不需要任何其他依赖项。比起JDK动态代理、cglib、Javassist,Byte Buddy在性能上具有一定的优势。
官网:https://bytebuddy.net

2.Javaagent技术

通过java agent技术进行类的字节码修改最主要使用的就是Java Instrumentation API。下面将介绍如何使用Java Instrumentation API进行字节码修改。

3.2.1 实现agent启动方法

启动时加载

Java Agent支持目标JVM启动时加载,也支持在目标JVM运行时加载,这两种不同的加载模式会使用不同的入口函数,如果需要在目标JVM启动的同时加载Agent,那么可以选择实现下面的方法:

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

[2] public static void premain(String agentArgs);

运行时加载

JVM将首先寻找[1],如果没有发现[1],再寻找[2]。如果希望在目标JVM运行时加载Agent,则需要实现下面的方法:2023-10-28 00:00:00

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

[2] public static void agentmain(String agentArgs);

这两组方法的第一个参数AgentArgs是随同 “–javaagent”一起传入的程序参数,如果这个字符串代表了多个参数,就需要自己解析这些参数。inst是Instrumentation类型的对象,是JVM自动传入的,我们可以拿这个参数进行类增强等操作。

3.常见应用

目前市面的全链路监控系统基本都是参考Google的Dapper (opens new window)(大规模分布式系统的跟踪系统)来做的。例如;蚂蚁金服分布式链路跟踪组件SOFATracer、Gokit微服务-服务链路追踪 、Pinpoint、Prometheus(普罗米修斯)等等。还有Arthus也使用了上述技术。

4.代码实践

基于JavaAgent的全链路监控三《ByteBuddy操作监控方法字节码》

下面基于byteBuddy实现一个方法耗时统计的agent

1.打包配置

<properties>
    <!-- Build args -->
	<argline>-Xms512m -Xmx512m</argline>
	<skip_maven_deploy>false</skip_maven_deploy>
	<updateReleaseInfo>true</updateReleaseInfo>
	<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
	<maven.test.skip>true</maven.test.skip>
	<!-- 自定义MANIFEST.MF -->
	<maven.configuration.manifestFile>src/main/resources/META-INF/MANIFEST.MF</maven.configuration.manifestFile>
</properties>

<dependencies>
	<dependency>
		<groupId>javassist</groupId>
		<artifactId>javassist</artifactId>
		<version>3.12.1.GA</version>
		<type>jar</type>
	</dependency>
	<dependency>
		<groupId>net.bytebuddy</groupId>
		<artifactId>byte-buddy</artifactId>
		<version>1.8.20</version>
	</dependency>
	<dependency>
		<groupId>net.bytebuddy</groupId>
		<artifactId>byte-buddy-agent</artifactId>
		<version>1.8.20</version>
	</dependency>
</dependencies>

<!-- 将javassist包打包到Agent中 -->
<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-shade-plugin</artifactId>
	<executions>
		<execution>
			<phase>package</phase>
			<goals>
				<goal>shade</goal>
			</goals>
		</execution>
	</executions>
	<configuration>
		<artifactSet>
			<includes>
				<include>javassist:javassist:jar:</include>
				<include>net.bytebuddy:byte-buddy:jar:</include>
                <include>net.bytebuddy:byte-buddy-agent:jar:</include>
			</includes>
		</artifactSet>
	</configuration>
</plugin>      
 

2.配置 src\main\resources\META-INF\MANIFEST.MF

Manifest-Version: 1.0
Premain-Class: com.example.agent.MyAgent
Can-Redefine-Classes: true

3.监控实现

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");
        }
    }

}

4.javaagent方法重写 MyAgent.java

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

    public static void bytebyddyPremain(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.example.agent")) // 指定需要拦截的类
                .transform(transformer)
                .with(listener)
                .installOn(inst);
    }
    //如果代理类没有实现上面的方法,那么 JVM 将尝试调用该方法
    public static void premain(String agentArgs) {
    }
}

5.测试方法

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

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

}

6.运行配置

jvm options

-javaagent:D:\IdeaProjects\code-learn\javaagent\target\javaagent-0.0.1-SNAPSHOT.jar=testargs

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值