一、引言

一般如果需要做增强类的架构工具会使用SpringBoot提供的切面,但是这逃不开两个问题:1、使用方需要加注解代码;2、版本更新导致的发布。

所以java还提供了字节码层面的增强方案,对使用的系统是无感的。

二、字节码增强选型

1、Java Agent简介

Java Agent 是一种 Java 技术,它允许开发者在应用程序运行时修改或增强 Java 字节码。字节码增强是指通过修改字节码来改变现有类的行为或添加新的功能。
Java Agent 通过使用 Java Instrumentation API 来实现字节码增强。它允许开发者在类加载过程中拦截和修改字节码,从而实现对类的增强。通过 Java Agent,开发者可以在不修改源代码的情况下,对已有的类进行功能扩展、性能优化、调试等操作。
字节码增强可以用于各种用途,例如:
1. AOP(面向切面编程):通过在方法前后插入额外的代码,实现日志记录、性能监控、事务管理等功能。
2. 动态代理:通过修改字节码,在运行时生成代理对象,实现对目标对象的拦截和增强。
3. 字节码注入:在类加载过程中修改字节码,实现对类的功能扩展或修复。
4. 代码热替换:在应用程序运行时,动态修改已加载类的字节码,实现代码的热部署和更新。
在实际应用中,通常会使用一些开源的字节码增强框架,如 ASM、Byte Buddy、Javassist 等,来简化字节码操作的复杂性。

2、选型比较

1、ASM(ObjectWeb ASM)

ASM是一个低级别的字节码操作库,提供了直接访问和修改字节码的功能。
它提供了灵活的API,可以对字节码进行细粒度的操作,但使用起来相对较复杂。
ASM的性能很高,因为它直接操作字节码,没有额外的开销。
ASM通常用于底层的字节码操作,如编写字节码插桩工具、静态分析工具等。

2. Byte Buddy:

Byte Buddy是一个高级别的字节码操作库,提供了更简单和易于使用的API。
它使用了更高级的抽象,可以通过编写Java代码来定义和修改字节码。
Byte Buddy支持动态生成代理对象、修改现有类的行为等常见的字节码增强操作。
Byte Buddy的性能较好,并且具有较好的易用性和灵活性。

3. Javassist:

Javassist是一个中级别的字节码操作库,提供了方便的API来修改字节码。
它使用类似于Java源代码的语法,可以通过编写类似于Java代码的字符串来定义和修改字节码。
Javassist支持动态生成代理对象、修改现有类的行为等常见的字节码增强操作。
Javassist的性能较好,并且具有较好的易用性和灵活性。

总体而言,ASM提供了最底层的字节码操作能力,但使用起来较为复杂;Byte Buddy和Javassist则提供了更高级别、更易用的API,适合大多数常见的字节码增强需求。

作者组内选用的是Byte Buddy

三、实现

这里我们需要实现捕捉一个指定路径的类方法,把这个方法的参数、返回值、异常按照一定格式记录下来

1、pom

net.bytebuddybyte-buddy1.14.9net.bytebuddybyte-buddy-agent1.14.9javax.servletjavax.servlet-apiprovided3.1.0

2、脚本

本质上就是把这个agent打包,然后塞到服务tomcat的仓库里面去

现在都是塞在服务的docker镜像里面,通过CI/CD打包,发布的时候镜像和包都会一起解压,部署在容器里面,本质上还是生成一个linux的进程。

JAVA_OPTS='"$JAVA_OPTS -javaagent:/

3、agent

比如需要步骤类的路径是com.ct.InvoiceClass,方法是execute

创建AgentBuilder对象,并使用ElementMatchers来匹配类名以`com.ct.InvoiceClass`开头的类

使用`transform`方法来定义对匹配的类进行转换的逻辑

使用Advice.to方法将`InvoiceClassInterceptor`类中的方法应用到`execute`方法上。

import java.lang.instrument.Instrumentation;import java.lang.instrument.ClassFileTransformer;import java.security.ProtectionDomain;import net.bytebuddy.agent.builder.AgentBuilder;import net.bytebuddy.asm.Advice;import net.bytebuddy.matcher.ElementMatchers;public class InvoiceClassAgent {public static void premain(String agentArgs, Instrumentation inst) {new AgentBuilder.Default().type(ElementMatchers.nameStartsWith("com.ct.InvoiceClass")).transform((builder, typeDescription, classLoader, module) ->builder.visit(Advice.to(InvoiceClassInterceptor.class).on(ElementMatchers.named("execute")))).installOn(inst);}public static class InvoiceClassInterceptor {@Advice.OnMethodEnterpublic static void enter(@Advice.Argument(0) Object arg) {//进入方法之前的处理}// @Advice.OnMethodExit表示在目标方法执行后执行// @Advice.Return注解来获取目标方法的返回值// Object[] allArguments就是拿到所有的入参@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)public static void OnMethodExit(@Advice.AllArguments(readOnly = false, typing = Assigner.Typing.DYNAMIC) Object[] allArguments,@Advice.Thrown Throwable throwable, @Advice.Return Object returnValue) {// 进入方法之后的处理}}}

4、过滤

在使用的过程中还要减少扫描,一方面是性能优化,很多基础库扫描没有意义,另外一方面是有些情况下不想进行捕捉,比如日志对象过大

那就需要基于ElementMatcher.Junction.AbstractBase进行过滤,通过实际路径名称进行匹配,匹配上了返回true代表需要过滤

需要在创建AgentBuilder的时候添加agentBuilder.ignore(new IgnoredTypesMatcher());

public class IgnoredTypesMatcher extends ElementMatcher.Junction.AbstractBase {private static final String[] IGNORED_STARTS_WITH_NAME =new String[] {"net.bytebuddy.", "sun.reflect."};private static final String[] IGNORED_CONTAINS_NAME = new String[] {"javassist.", ".asm.", ".reflectasm."};@Overridepublic boolean matches(TypeDescription target) {// isSynthetic用于判断目标类型是否是合成类型// 合成类型是由编译器生成的、在源代码中不存在的类型,例如匿名类、内部类、Lambda表达式等。if (target.isSynthetic()) {return true;}String name = target.getActualName();for (String ignored : IGNORED_STARTS_WITH_NAME) {if (name.startsWith(ignored)) {return true;}}for (String ignored : IGNORED_CONTAINS_NAME) {if (name.contains(ignored)) {return true;}}return false;}}

5、打包

agent最终都是要打包的,然后不断被系统镜像拉取对应路径下的包,如果是本地测试的话就要在idea里面设置-javaagent:/***-agent-1.0.0-SNAPSHOT.jar

四、总结

在使用agent之前,组内也尝试使用父pom,但是由于升级的时候需要改版本发布,几十个系统每次都让组内痛不欲生,最后做了agent,通过CI/CD进行部署,之后有升级只需要重建就行了。

有使用的同学欢迎评论区交流!