最近偶然想到以前下载的Byte Buddy资料很久没看了,又联想到Easeagent项目是基于Byte Buddy进行开发。想到那就学习一下吧。
以下是Byte Buddy的官网简介,懂得都懂,我就不做进一步解释了。
Byte Buddy is a code generation and manipulation library for creating and modifying Java classes during the runtime of a Java application and without the help of a compiler.
在学习的过程中,看到了袁伟大佬
(https://github.com/akwei)的分享材料How To Write a JavaAgent
(https://www.bilibili.com/video/BV1eh41167Lx?spm_id_from=333.999.0.0)。其中,有几段填坑经历,我觉得很有意思,在此分享一下。
1 Java Agent加载
Java文件的加载机制如下所示:

可以看到,我们的代码在转换成机器码之前,需要执行转换,加载和校验。Java Agent即是在此过程中,对代码进行拦截,进行定制化的操作(有点AOP的意思)。
Byte Buddy提供premain
方法,函数签名如下所示:
public static void premain(String args,Instrumentation inst)
顾名思义,premain
方法在main
方法之前被调用,示例如下所示:

运行HelloWorld
程序时,在HelloWorld.class
被JVM加载之前,发现有premain
方法对其进行拦截。根据premain
中定义的Agent规则,对HelloWorld.class执行转换(Transformed
)操作,转换后的helloWorld.class
文件被ClassLoader加载,功能增强完成。
更多解析,可参考字节码编程,Byte-buddy篇一《基于Byte Buddy语法创建的第一个HelloWorld》系列文章(我觉得写的还是不错的)。
2 依赖冲突
2.1 常规解法
不是每个功能都会如同上文中的HelloWorld
功能增强一样简单。实际开发环境中,Agent存在很多依赖,这些依赖可能会与应用程序的依赖存在冲突。如果冲突的依赖项只是API保持兼容性,那么将低版本的依赖从pom.xml
中exclusions
掉,如下所示:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
如果两个版本的API不一致,无法简单的通过exclusions
解决问题,我们就需要使用maven-shade-plugin
解决问题。maven-shade-plugin
通过重命名依赖项的方式,解决依赖冲突问题,具体如下所示:

使用maven-shade-plugin
重命名方式解决依赖冲突,具有容易实施,无需修改用户代码的优点,但与此同时具有如下缺点:
大型项目中,各团队引入的依赖项是错综复杂的; 容易漏掉某一依赖项,问题容易在线上运行时暴发; 重命名包,会导致不容易进行Debug。
Agent不推荐使用maven-shade-plugin
解决依赖冲突,那么我们需要新的解决方式,即Custom ClassLoader
。
2.2 Custom ClassLoader
重新回顾下,Java中类是如何加载的:

Java类加载机制,常用称呼是双亲委派机制(这称呼真的很奇怪,极易让初学者误解),其流程如下所示:
(1)Application ClassLoader将类加载委托至Extension ClassLoader处理,如果Extension ClassLoader未加载则由自己尝试加载;
(2)Extension ClassLoader将类加载委托至Bootstrap ClassLoader处理,如果Bootstrap ClassLoader未加载则由自己尝试加载;
(3)Bootstrap ClassLoader加载类。
Easeagent的Custom ClassLoader,采用的方式是将Custom ClassLoader的Parent设置为Bootstrap ClassLoader(实现继承了Spring Boot的Custom ClassLoader)。通过自定义ClasLoader加载Agent所需的依赖,实现了Agent依赖与用户Application加载依赖的隔离,效果如下所示。

3 ClassLoader依赖传递
本章节代码,节选自袁伟大佬的agent-demo.
假设对用户的printiInfo
函数进行增强,增强代码如下所示:

增强后的代码如下所示:

此时,会爆出类缺失异常:

原因在于ObjectUtils.isEmpty(obj);
代码由Agent提供(Agent的Custom ClassLoader加载了ObjectUtils
依赖),但是此语句被ApplicationClassLoader加载。在ApplicationClassLoader加载的代码中,不包含ObjectUtils
依赖。
Agent提供方无法强制用户代码引入ObjectUtils依赖,因此,我们需要一种方法,可以将Agent的依赖项传递至Application。此处,我们将这种传递中间件称呼为Dispatcher
。回顾一下,Agent ClassLoader与Application ClassLoader的共同祖先是Bootstrap ClassLoader,因此我们需要围绕Bootstrap ClassLoader做文章。

此处,我们定义Dispatcher
与Action
实例,由Byte Buddy提供的API注册至Bootstrap ClassLoader中。
Action是接口,Action的实现类存储了需要被增强的代码。
// Byte Buddy提供的将类注册至Bootstrap ClassLoader API
ClassInjector.UsingInstrumentation
.of(temp, ClassInjector.UsingInstrumentation.Target.BOOTSTRAP, instrumentation)
.inject(Collections.singletonMap(describe.resolve(),
classFileLocator.locate(className).resolve()));
Dispatcher定义为Map,存储实现了Action接口的实例,代码如下所示:


此处,可以看到Advice代码,实质上就是从Dispatcher中根据指定的名称,查询对应的Action实现。获取到对应的Action后,执行需要增强的代码。增强后的用户代码如下所示:

由上图可知,用户代码的执行逻辑变更为,Application ClassLoader尝试加载Dispatcher(委托Bootstrap ClassLoader加载),获取到对应的Action实例对象。因为Action实例对象被Bootstrap ClassLoader加载,连带的ObjectUtils依赖丢失问题解决了,代码正常执行。
总结
Agent对用户代码无侵入的特性,将促使其在APM等领域不断发展壮大。
不过,Agent的开发难度依然比较陡峭,这固然有Byte Buddy文档质量不怎么高的原因,更重要的是对开发人员的综合素质要求比较高。
此外Agent的开发工作量将是比较繁重的工作,这其中有一部分原因是Java的版本迭代速度比较快,每个版本都需要进行适配工作(Easeagent现阶段仅支持Java 8);JDK厂商比较多,不同厂商提供的JDK可能存在细微的差异,这也加大了开发工作量。
对这块感兴趣的同学,可以给Easeagent提交PR。
感谢袁伟大佬在学习过程中提供的帮助。
感谢来自于 Pexels 上的 Quang Nguyen Vinh 拍摄的图片




