Java字节码增强技术Bytebuddy探路篇二(NoClassDefFoundError问题)

图片来自pixabay.com的NickRivers会员

最近为了实现Java应用RPC调用的录制和Mock回放,需要以无侵入方式获取到RPC方法的出入参数和返回响应消息等数据,于是踏上了Java字节码增强技术的道路摸索。技术路径上选择了Java Agent + Bytebuddy框架,但是在应用实践过程中,对tomcat/dubbo/rocketmq进行类切面增强时出现了NoClassDefFoundError的问题,本文针对这个关键问题的出现原因进行分析讨论,提供相应的解决思路。

1. 演示代码工程

为了方便讨论,请先下载演示代码工程,整个工程结构如下,

+ phantom-core 演示基础类。
+ phantom-demo 演示web应用,这是一个简单的sprint boot应用,运行在端口18080。
+ phantom-agent 一个Java agent,里面通过ByteBuddy对指定类以无侵入方式切面增强,在演示工程中主要对Tomcat web的请求进行切面处理。
+ phantom-agent-plugin 一个增强类插件,这个是为了解决NoClassDefFoundError问题而提供的一个插件解决方案,应用于phantom-agent的TransformerV3.tranform()中,项目构建后需要复制构建包到路径/tmp/phantom-agent-plugin.jar上。

下载后,分别对项目进行构建、启动和测试,命令如下,

# 使用mvn工具编译构建
mvn clean package

# 启动演示web应用
java -jar ./phantom-demo/target/phantom-demo.jar

# 测试web请求命令
curl -X POST http://127.0.0.1:18080/api/hello

若上面的web应用启动成功,则测试web请求将会收到“hello,world”的消息响应,这个简单的测试请求已经走过web服务的完整路径。工程中的Transformer将通过java agent方式对tomcat web请求进行切面处理,获取所有http请求的执行前、执行后、执行异常相关情况并打印到日志中,tomcat切点定义为,

# 切点:tomcat核心类StandardHostValve的invoke方法
org.apache.catalina.core.StandardHostValve.invoke(Request request, Response response);

演示工程中Transformer类有三个,

  1. com.phantom.agent.trace.TransformerV1 :用于演示问题的复现和定位
  2. com.phantom.agent.trace.TransformerV2 :用于演示问题的解决思路1
  3. com.phantom.agent.trace.TransformerV3 :用于演示问题的解决思路2

这三个类都各自继承自ByteBuddy的AgentBuilder.Transformer接口类,实现了tranform()方法。

2. 问题复现和定位

我们先复现一下NoClassDefFoundError的问题,通过设置agent启动参数agent.transformer.version=v1,执行TransformerV1版本的类增强变换,启动命令如下,

# 启动命令(加载agent)
java \
-javaagent:./phantom-agent/target/phantom-agent.jar \
-Dagent.transformer.version=v1 \
-jar ./phantom-demo/target/phantom-demo.jar

这个时候对hello接口发起请求,该请求可以成功“hello,world”的响应消息,翻开web应用后台,可以观察如下日志,

[20220808 16:22:37-453][http-nio-18080-exec-2][INFO] [trace]beforeMethod(), method = org.apache.catalina.core.StandardHostValve.invoke
[20220808 16:22:37-453][http-nio-18080-exec-2][ERROR] EnhancerProxy failure - beforeMethod, [class org.apache.catalina.core.StandardHostValve].[invoke], msg = java.lang.NoClassDefFoundError: org/apache/catalina/connector/Request
[20220808 16:22:37-455][http-nio-18080-exec-2][INFO] [trace]afterMethod(), method = org.apache.catalina.core.StandardHostValve.invoke

可以看到,增强切面在后台报java.lang.NoClassDefFoundError的错误,即找不到org.apache.catalina.connector.Request类,若通过调试的话,可以看到这个异常是从TomcatEnhancer报出来,增强切面出现异常,找不到相应的Request类。

这个Request类定义在tomcat-embedd-core类包中,这个tomcat核心类包其实已经被web应用正常加载,tomcat web处于正常工作状态,hello接口的请求可以得到hello响应消息证明了这点。另外,ByteBuddy也正常在指定invoke切点织入了切面,如下的两条来自TomcatEnhancer日志证明了这点:

[20220808 16:22:37-453][http-nio-18080-exec-2][INFO] [trace]beforeMethod(), method = org.apache.catalina.core.StandardHostValve.invoke
[20220808 16:22:37-455][http-nio-18080-exec-2][INFO] [trace]afterMethod(), method = org.apache.catalina.core.StandardHostValve.invoke

现在的问题是,为什么在增强切面类TomcatEnhancer执行时会报找不到tomcat核心类包的Request类?

打开应用的远程调试,在执行到TomcatEnhancer时,检查一下是否能够直接通过loadClass找到Request类,

this.getClass().classLoader.loadClass("org.apache.catalina.connector.Request")

没有出意外,确实没有找到这个类。查看了一下TomcatEnhancer的类加载器,是AppClassLoader,这是系统类加载器,负责加载来自Java classpath指定路径下的JAR类包。难道Request类不在AppClassLoader的搜索路径?

于是通过arthas工具查看当前应用的类加载树,

# 查看类加载器树
[arthas@78212]$ classloader -t
+-BootstrapClassLoader
+-sun.misc.Launcher$ExtClassLoader@2503dbd3
  +-com.taobao.arthas.agent.ArthasClassloader@463899fd
  +-sun.misc.Launcher$AppClassLoader@18b4aac2
    +-org.springframework.boot.loader.LaunchedURLClassLoader@6fdbe764
Affect(row-cnt:5) cost in 10 ms.

# 查看类文件信息,通过AppClassLoader,未找到
[arthas@78212]$ classloader -c 18b4aac2 -r org/apache/catalina/connector/Request.class
Affect(row-cnt:0) cost in 0 ms.

# 查看类文件信息,通过LaunchedURLClassLoader,找到
[arthas@78212]$ classloader -c 6fdbe764 -r org/apache/catalina/connector/Request.class
 jar:file:/Users/huangyinhuang/hyh/gitee/simple-demo/demo-bytebuddy/phantom-demo/target/phantom-demo.jar!/BOOT-INF/lib/tomcat-embed-core-9.0.65.jar!/org/apache/catalina/connector/Request.class
Affect(row-cnt:1) cost in 1 ms.

通过arthas的探针查看证实了Request类由LaunchedURLClassLoader加载,确实无法通过AppClassLoader进行加载。

3. 解决思路1

知道了这个问题的原因后,一个首先想到的解决方案就是让LaunchedURLClassLoader来加载TomcatEnhancer类,即延迟TomcatEnhancer类加载到类增强时刻,而不是在agent启动时刻,这样TomcatEnhancer类和Request类都由Spring的LaunchedURLClassLoader进行加载。

我们可以在transform方法中通过传入的LaunchedURLClassLoader来动态创建TomcatEnhancer实例,于是就有了TransformerV2版本的类增强变换方案,打开其transform()方法,可以看到其如下代码实现,

@Override
public DynamicType.Builder transform(DynamicType.Builder builder, TypeDescription typeDescription, ClassLoader loader, JavaModule javaModule) {

        // enhanceClass = "com.phantom.agent.enhancer.impl.TomcatEnhancer"
        // loader = org.springframework.boot.loader.LaunchedURLClassLoader

        EnhancerProxy proxy = new EnhancerProxy();
        try {
            AbstractEnhancer enhancer = (AbstractEnhancer) Class.forName(this.enhanceClass, true, loader).newInstance();
            proxy.setEnhancer(enhancer);
        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
            Logger.error("failed to initialized the proxy %s", e.toString());
        }

        // return ...
    }

通过设置agent启动参数agent.transformer.version=v2,执行上面TransformerV2版本的类增强变换,

# 启动命令(加载agent)
java \
-javaagent:./phantom-agent/target/phantom-agent.jar \
-Dagent.transformer.version=v2 \
-jar ./phantom-demo/target/phantom-demo.jar

发一个hello接口的测试请求,测试请求可以成功收到“hello,world”的响应消息,翻开web应用后台,却仍然收到如下NoClassDefFoundError日志,

[20220808 18:29:44-174][http-nio-18080-exec-2][INFO] [trace]beforeMethod(), method = org.apache.catalina.core.StandardHostValve.invoke
[20220808 18:29:48-173][http-nio-18080-exec-2][ERROR] EnhancerProxy failure - beforeMethod, [class org.apache.catalina.core.StandardHostValve].[invoke], msg = java.lang.NoClassDefFoundError: org/apache/catalina/connector/Request
[20220808 18:29:48-176][http-nio-18080-exec-2][INFO] [trace]afterMethod(), method = org.apache.catalina.core.StandardHostValve.invoke

问题并没有得到解决,继续跟踪调试,发现如下Class.forName方法参数重虽然指定了LaunchedURLClassLoader,但实际上返回TomcatEnhancer实例仍然由AppClassLoader加载,并没有被指定的LaunchedURLClassLoader所加载。

// enhanceClass = "com.phantom.agent.enhancer.impl.TomcatEnhancer"
// loader = org.springframework.boot.loader.LaunchedURLClassLoader
Class.forName(this.enhanceClass, true, loader).newInstance()

见调试图片,

这个时候突然想到类的双亲委派机制,恍然大悟,这里即使指定了LaunchedURLClassLoader来创建,但是由于AppClassLoader是LaunchedURLClassLoader父加载器,基于Java的双亲委派机制,最终还是会被AppClassLoader进行创建。

4. 解决思路2

这个时候,就不能再依赖LaunchedURLClassLoader了,得另谋思路。要改变Java的双亲委派机制,就得自定义类加载器,这也是TransformerV3版本采用的技术方案。

@Override
public DynamicType.Builder transform(DynamicType.Builder builder, TypeDescription typeDescription, ClassLoader loader, JavaModule javaModule) {

    // enhancerClz = "com.phantom.agent.enhancer.impl.TomcatEnhancer"
    // loader = org.springframework.boot.loader.LaunchedURLClassLoader

    EnhancerProxy proxy = new EnhancerProxy();
    try {
        IAspectEnhancer enhancer = EnhancerInstanceLoader.load(enhancerClz, loader);
        proxy.setEnhancer(enhancer);
    } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
        Logger.error("failed to initialized the proxy %s", e.toString());
    }

    // return ...
}

上面实现代码中的EnhancerInstanceLoader就是一个自定义类加载器,其将加载指定在路径"/tmp/phantom-agent-plugin.jar"的Jar包。

通过设置agent启动参数agent.transformer.version=v3,执行上面TransformerV3版本的类增强变换,

# 复制文件到/tmp/phantom-agent-plugin.jar
cp ./phantom-agent-plugin/target/phantom-agent-plugin.jar /tmp/

# 启动命令(加载agent)
java \
-javaagent:./phantom-agent/target/phantom-agent.jar \
-Dagent.transformer.version=v3 \
-jar ./phantom-demo/target/phantom-demo.jar

发一个hello接口的测试请求,测试请求可以成功收到“hello,world”的响应消息,这时打开web应用后台日志,NoClassDefFoundError的错误信息不再出现, hello接口的Span详细请求信息也通过Span顺利地打印出来了。

[20220808 18:54:18-878][http-nio-18080-exec-2][INFO] [trace]beforeMethod(), method = org.apache.catalina.core.StandardHostValve.invoke
[20220808 18:54:18-881][http-nio-18080-exec-2][INFO] [trace]afterMethod(), method = org.apache.catalina.core.StandardHostValve.invoke
[20220808 18:54:23-880][BatchSpanProcessor_WorkerThread-1][INFO] demo 1.0.0 - SpanData{spanContext=ImmutableSpanContext{traceId=d23da1005580022dd42e92a3fc4c0754, spanId=cfdd64665bdb7270, traceFlags=01, traceState=ArrayBasedTraceState{entries=[]}, remote=false, valid=true}, parentSpanContext=ImmutableSpanContext{traceId=00000000000000000000000000000000, spanId=0000000000000000, traceFlags=00, traceState=ArrayBasedTraceState{entries=[]}, remote=false, valid=false}, resource=Resource{schemaUrl=null, attributes={service.name="unknown_service:java", telemetry.sdk.language="java", telemetry.sdk.name="opentelemetry", telemetry.sdk.version="1.12.0"}}, instrumentationLibraryInfo=InstrumentationLibraryInfo{name=demo, version=1.0.0, schemaUrl=null}, name=POST /api/hello, kind=PRODUCER, startEpochNanos=1659956058879000000, endEpochNanos=1659956058881771573, attributes=AttributesMap{data={METHOD=POST, URL=http://127.0.0.1:18080/api/hello, Component=Tomcat, URI=/api/hello, RemoteAddr=127.0.0.1, RemoteHost=127.0.0.1, RemotePort=64352}, capacity=128, totalAddedValues=7}, totalAttributeCount=7, events=[], totalRecordedEvents=0, links=[], totalRecordedLinks=0, status=ImmutableStatusData{statusCode=UNSET, description=}, hasEnded=true}
ta={METHOD=POST, URL=http://127.0.0.1:18080/api/hello, Component=Tomcat, URI=/api/hello, RemoteAddr=127.0.0.1, RemoteHost=127.0.0.1, RemotePort=62654}, capacity=128, totalAddedValues=7}, totalAttributeCount=7, events=[], totalRecordedEvents=0, links=[], totalRecordedLinks=0, status=ImmutableStatusData{statusCode=UNSET, description=}, hasEnded=true}

5. 执行时序图

为了更好地理解相关代码的执行路径,下面提供相关的执行时序图,

Java字节码增强技术Bytebuddy探路篇

图片来自pixabay.com的Alexas_Fotos会员

最近为了实现Java应用RPC调用的录制和Mock回放,需要以无侵入方式获取到RPC方法的出入参数和返回响应消息等数据,于是踏上了Java字节码增强技术的道路摸索,这个非常类似Trace所使用的相关技术,不过需要深入到RPC方法级别,对指定方法进行无侵入方式切面处理。先后对ASM/Javaassist/Bytebuddy等技术进行了调研等,本文是对所做摸索探路工作的总结,若读者有类似Trace场景需求,可以进行借鉴参考。

在众多比较之后最后选择的是Bytebuddy技术。

1. Java字节码简介

Java字节码是众多字节码增强技术的知识基础。Java语言写出的源代码首先需要编译成class文件,即字节码文件,然后被JVM加载并运行,每个class文件具有如下固定的数据格式,

ClassFile {
    u4             magic;           // 魔数,固定为0xCAFEBABE
    u2             minor_version;   // 次版本
    u2             major_version;   // 主版本,常见版本:52对应1.8,51对应1.7,其他依次类推
    u2             constant_pool_count;                     // 常量池个数
    cp_info        constant_pool[constant_pool_count-1];    // 常量池定义
    u2             access_flags;    // 访问标志:ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT等
    u2             this_class;      // 类索引
    u2             super_class;     // 父类索引
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

可以看到,class文件总是一个魔数开头,后面跟着版本号,然后就是常量定义、访问标志、类索引、父类索引、接口个数和索引表、字段个数和索引表、方法个数和索引表、属性个数和索引表。

class文件本质上是一个字节码流,每个字节码所处的位置代表着一定的指令和含义。如何对class文件中定义的指令和字节码进行解读、增强定义、编排,这是字节码增强技术所要完成的事情。

了解Java字节码有助于字节码增强的开发,但并不是实现字节码增强开发的必要条件,最新主流的众多字节码增强工具框架类库都将字节码的编排进行了不同程度封装,在可读性、易编排性、排错性上提供开发便利性,学习曲线和开发难度得到了较好的改善。

2. Java字节码增强支持

对于字节码增强的开发来说,JVMTI是一个在实践中应该被熟悉的工具技术。JVM从1.5版本开始提供JVM Tool Interface,这是JVM对外的、用于Java应用监控和调试的一系列工具接口,是JVM平台调试架构的重要组成部分。

下图是JVM平台调试架构图

The Java™ Platform Debugger Architecture is structured as follows:
           Components                          Debugger Interfaces

                /    |--------------|
               /     |     VM       |
 debuggee ----(      |--------------|  <------- JVM TI - Java VM Tool Interface(Jvm服务端调试接口)
               \     |   back-end   |
                \    |--------------|
                /           |
 comm channel -(            |  <--------------- JDWP - Java Debug Wire Protocol (Java调试通信协议)
                \           |
                     |--------------|
                     | front-end    |
                     |--------------|  <------- JDI - Java Debug Interface (客户端调试接口和调试应用)
                     |      UI      |
                     |--------------|

JVM启动支持加载agent代理,而agent代理本身就是一个JVM TI的客户端,其通过监听事件的方式获取Java应用运行状态,调用JVM TI提供的接口对应用进行控制。

我们可以看下Java agent代理的两个入口函数定义,

// 用于JVM刚启动时调用,其执行时应用类文件还未加载到JVM
public static void premain(String agentArgs, Instrumentation inst);

// 用于JVM启动后,在运行时刻加载
public static void agentmain(String agentArgs, Instrumentation inst);

这两个入口函数定义分别对应于JVM TI专门提供了执行字节码增强(bytecode instrumentation)的两个接口。

  • 加载时刻增强,类字节码文件在JVM加载的时候进行增强,。
  • 动态增强,已经被JVM加载的class字节码文件,当被修改或更新时进行增强,从JDK 1.6开始支持。

这两个接口都是从JDK 1.6开始支持。

我们无需对上面JVM TI提供的两个接口规范了解太多,Java Agent和Java Instrument类包封装好了字节码增强的上述接口通信。我们需要了解的是,上述入口函数传入的第二个参数Instrumentation实例,即Java Instrument类java.lang.instrument.Instrumentation,查看其类定义,可以看到其提供的核心方法只有一个addTransformer,用于添加多个ClassFileTransformer,

// 说明:添加ClassFileTransformer
// 第一个参数:transformer,类转换器
// 第二个参数:canRetransform,经过transformer转换过的类是否允许再次转换
void Instrumentation.addTransformer(ClassFileTransformer transformer, boolean canRetransform)

ClassFileTransformer则提供了tranform()方法,用于对加载的类进行增强重定义,返回新的类字节码流。需要特别注意的是,若不进行任何增强,当前方法返回null即可,若需要增强转换,则需要先拷贝一份classfileBuffer,在拷贝上进行增强转换,然后返回拷贝。

// 说明:对类字节码进行增强,返回新的类字节码定义
// 第一个参数:loader,类加载器
// 第二个参数:className,内部定义的类全路径
// 第三个参数:classBeingRedefined,待重定义/转换的类
// 第四个参数:protectionDomain,保护域
// 第五个参数:classfileBuffer,待重定义/转换的类字节码(不要直接在这个classfileBuffer对象上修改,需拷贝后进行)
// 注:若不进行任何增强,当前方法返回null即可,若需要增强转换,则需要先拷贝一份classfileBuffer,在拷贝上进行增强转换,然后返回拷贝。
byte[] ClassFileTransformer.transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte classfileBuffer)

下面简单的演示样例,通过Java agent打印出JVM加载的类列表,

  1. 下载演示代码,见这里
  2. 编译项目 mvn clean package
  3. 运行命令
java -javaagent:./demo-agent/target/agent-agent.jar -jar ./demo-app/target/demo-app.jar
  1. 可以通过控制台查看所有JVM加载的类列表。
loading agent..
agent has been loaded.
transforming class = java/lang/invoke/MethodHandleImpl
transforming class = java/lang/invoke/MethodHandleImpl$1
transforming class = java/lang/invoke/MethodHandleImpl$2
transforming class = java/util/function/Function
transforming class = java/lang/invoke/MethodHandleImpl$3
transforming class = java/lang/invoke/MethodHandleImpl$4
transforming class = java/lang/ClassValue

3. 演示类

为了方便展示不同字节码增强技术,下面将以Greeting类为例,对sayHello()方法进行加强。

public class Greeting {

    public String sayHello() {
        String hello = "hello,world";
        System.out.println(hello);
        return hello;
    }

}

分别在sayHello函数执行前后添加打印语句,输出如下类似信息。

begin of sayhello().
hello,world
end of sayhello().

为了能够检查增强后的类,建议使用arthas调试工具,

jad  com.pphh.demo.api.Greeting

通过jad命令,对Greeting类进行反编译,以了解字节码增强后的类定义。

4. Java字节码增强类库 - ASM

ASM是一个Java字节码解析和操作框架,整个类包非常小,还不到120KB,但其非常注重对类字节码的操作速度,这种高性能来自于它的设计模式 - 访问者模式,即通
过Reader、Visitor和Writer模式。

ASM是直接操作类字节码数据,因此其读写的是字节码指令,比如,

mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("begin of sayhello().");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

这种指令比较晦涩难懂,在实际操作过程中,会先将期望的类源码文件写好,编译后查看字节码文件,然后复制相关字节码指令。

演示代码
1. 下载演示代码,见这里
2. 编译项目 mvn clean package
3. 运行命令

java -javaagent:./demo-asm/target/agent-asm.jar -jar ./demo-app/target/demo-app.jar
  1. 可以通过控制台查看日志。

5. Java字节码增强类库 - Javassist

Javassist是一个非常早的字节码操作类库,开始于1999年,它能够支持两种编辑方式:源码级别和字节码指令级别,相比于晦涩的字节码级别,源码级别更加人性化,代码编写起来更加易懂。

以上面的ASM字节码指令编辑为例,换成对应的Javassist源码级别编辑方式,如下所示,

CtMethod m = cc.getDeclaredMethod("sayHello");
m.insertBefore("{ System.out.println(\"begin of sayhello()\"); }");

相信大多数程序员更愿意接受源码级别编辑方式,翻译成直接码指令的工作就交给Javassist完成,目前源码级别方式Javassist只支持Java语言语法。

演示代码
1. 下载演示代码,见这里
2. 编译项目 mvn clean package
3. 运行命令

java -javaagent:./demo-javaassist/target/agent-jassist.jar -jar ./demo-app/target/demo-app.jar
  1. 可以通过控制台查看日志。

6. Java字节码增强工具/类库 - ByteBuddy

ByteBuddy是一个基于ASM的字节码增强框架,开始于2014年,相比其它字节码操作类库而言,其诞生的时间则年轻得多,所以ButeBuddy API在设计之初上就吸收了业界其它字节码工具类库的优点,提供了丰富灵活的API接口,可以快速创建新类、继承已有类、动态重构类。

如下是几个简单代码样例如下,

// 创建一个com.pphh.demo.Sample类,继承于Object.class
DynamicType.Unloaded dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .name("com.pphh.demo.Sample")
  .make();

// 创建一个新类,继承于Foo.class
new ByteBuddy().subclass(Foo.class)

// 重构类Foo.class
new ByteBuddy().redefine(Foo.class)

// 重设定Foo.class(保留原先类的方法定义,基于原先定义进行扩展)
new ByteBuddy().rebase(Foo.class)

和Javassist比起来,它不再是一个Java源码的文本填充工具,而是让开发能够以代码开发的方式增强类,能够有编译级别的纠错。在下面的例子中,

    public Object intercept(@This Object obj,
                            @AllArguments Object[] allArguments,
                            @SuperCall Callable zuper,
                            @Origin Method method) throws Throwable {
        System.out.println("begin of sayHello()");
        Object result = zuper.call();
        System.out.println("end of sayHello()");
        return result;
    }

同样地写System.out.println,若不小心写错了,如上方式是可以在编译时刻得到提醒的,这个和Javassist比起来好很多。

演示代码
1. 下载演示代码,见这里
2. 编译项目 mvn clean package
3. 运行命令

java -javaagent:./demo-bytebuddy/target/agent-buddy.jar -jar ./demo-app/target/demo-app.jar
  1. 可以通过控制台查看日志。

7. Java字节码增强工具对比和关系图

7.1 Java字节码增强工具对比

对比 ASM Javassist JDK Proxy Cglib ByteBuddy
起源时间 2002 1999 2000 2011 2014
包大小 130KB
(版本9.3)
788KB
(版本3.28.0-GA)
3.7MB
(版本1.10.19)
增强方式 字节码指令 字节码指令和源码(注:源码文本) 源码 源码 源码
源码编译 NA 不支持 支持 支持 支持
agent支持 支持 支持 不支持,依赖框架 不支持,依赖框架 支持
性能
维护状态 停止升级 停止维护 活跃
优点 超高性能,应用场景广泛 同时支持字节码指令和源码两种增强方式 JDK原生类库支持 零侵入,提供良好的API扩展编程
缺点 字节码指令对应用开发者不友好 场景非常局限,只适用于Java接口 已经不再维护,对于新版JDK17+支持不好,官网建议切换到ByteBuddy
应用场景 小,高性能,广泛用于语言级别 广泛用于框架场景 广泛用于Trace场景

注:相关性能数据来自这里

综合了上述的字节码增强工具对比,比较了开发便利性和需求目标,我们最后选择了ByteBuddy来实现Trace跟踪技术。

7.2 Java字节码增强工具关系图

Java字节码增强工具关系图

需要提一下,JDK Proxy和Cglib也是以代码方式进行类方法的切面增强,但它们都是以框架的方式实现了Java类的动态扩展,主要应用在框架级别的字节码增强,在某种程度上JDK Proxy和Cglib技术对应用是有代码侵入的,这里的侵入不仅仅是框架代码侵入,而且包括增强的类中依赖JDK Proxy和Cglib类。与此相比,ButeBuddy API是以无侵入方式加强类代码,设计理念更优。

8. 参考资料

  1. JVM - The class File Format
  2. The Java Platform Debugger Architecture
  3. JVMTI - Bytecode Instrumentation
  4. java.lang.instrument类定义
  5. ASM
  6. Javassist
  7. Bytebuddy
  8. cglib

一个架构评审文档模板

图片来自pixabay.com的Alexas_Fotos会员

架构评审作为项目研发的一个重要节点,一般发生在项目立项之后、正式的代码开始开发之前,由开发人员从架构的角度思考项目落地方案的合理性、规范性、可行性、可扩展性,使得能够在项目的前期解决架构问题,避免在后期、代码开发完成之后发生大量的返修改造工作,并让测试、运维、平台架构等相关的团队尽早介入项目的推进过程中。

为了让架构评审能够顺利组织运作,好的架构评审模板是关键点。

本文提供一个架构评审模板,本模板从实践中逐步总结而来,供技术团队参考。有几个需要关注的方面是,

  1. 和技术评审文档相比,架构评审文档是以架构师的视角来编写,体现了架构师对项目的整体理解和设计。架构评审文档关注架构设计,技术评审文档关注实现细节。
  2. 作为模板,其一般包含了架构评审所需要的方方面面,在实践过程中,并不是所有部分都需要一一列出,只需列出期望被架构评审的要点即可,无须求全。
  3. 在架构评审会议之前,建议对待评审的架构评审文档先做个快速审查,确认符合评审的要求。好的架构评审文档对架构设计有明确描述,能够帮助大家快速理解,并进入架构要点讨论。

1. 业务/功能简介

简要描述当前功能需求,其目标和意义,建议提供相应的业务主流程图。

2. 架构设计

描述当前项目的组件架构设计,包括物理架构和逻辑架构,用于评审架构是否合理,是否可行,是否符合规范。

2.1 物理架构

绘制应用服务的物理部署架构,以网络拓扑为主线,列出内外网访问的HTTP域名,内部RPC调用主链路。
主要用于查看部署架构是否高可用部署,是否支持水平扩展。
对于有网络流量治理的设计,比如限流和熔断,可以在物理部署架构图中进行说明,描述其网络调用情况和流量治理方案。

2.2 逻辑架构

绘制应用服务的逻辑架构,以组件的上下调用关系为主线,列出所有设计的应用组件和调用链,说明各个逻辑模块的职责功能,及其上下依赖情况。
主要用于查看组件的上下依赖关系是否合理,各个组件是否承载了合理的功能交互。
对核心流程,需要绘制调用时序图。

3. 数据模型和类设计

3.1 数据表ER数据模型设计

描述当前核心数据库表的关系大图,包括关联字段、关联关系(一对一、一对多、多对多)。

3.2 核心类设计

描述当前业务的抽象类、接口类设计,包括其相关继承和组合、实现关系,以及相关类设计模式的应用。

4. 技术难点设计

4.1 高并发场景

对于有大流量访问的项目,需要提供对流量的预估,描述支撑高并发场景的技术方案,包括缓存、异步消息、线程池配置等等,评估并发下的数据线程安全;
对于核心业务链路,还需要考虑相应的治理监控手段,比如限流熔断降级策略。

4.2 数据一致性场景

对于有对数据一致性要求的项目,比如交易、支付等场景,需要考虑相应实现的技术方案,包括数据库事务、消息事务等等。

4.3 数据大表设计

每天的数据记录多少,是否考虑分库分表,归档策略

  • 每天的数据增量多少?
  • 是否需要读写分离?
  • 是否需要分库分表?
  • 是否可以归档?若归档,保留多久的记录?

4.4 慢接口说明

对于单接口运行时间大于10秒的,需要单独列出进行评审。注:这里的10秒为业务入口网关的接口超时时间。

5. 运维设计

5.1 新应用和新域列表

若有新增应用或新域名,列出并说明相关信息,包括AppId、应用框架、功能等。

5.2 运维成本

列举所需的机器硬件配置,硬件成本,软件成本等。

5.3 监控接入

日志监控:是否接入日志和监控平台
告警设置:是否接入告警平台
健康检查接口:是否提供健康检查接口

5.4 可维护性

是否有新引入组件和技术栈?
是否符合发布标准?
是否可以快速水平扩展?
是否接入健康自检?

5.5 安全性

网络:当前的网络调用是否安全?是否需要配置白名单访问?是否遵循产品设计与开发安全红线?
软件:使用的第三方软件框架是否安全?(列出第三方软件、框架、组件,开源协议)

6. 其它

上述文档列出架构评审的基本要点,若有更多方面,可以在本评审模板的基础上进行增减内容。