暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

开发Java Agent过程中涉及ClassLoader的坑

技术随想录 2021-12-06
6723

最近偶然想到以前下载的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文件加载机制

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

public static void premain(String args,Instrumentation inst)

顾名思义,premain
方法在main
方法之前被调用,示例如下所示:

Agent premain调用

运行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重命名依赖

使用maven-shade-plugin
重命名方式解决依赖冲突,具有容易实施,无需修改用户代码的优点,但与此同时具有如下缺点:

  • 大型项目中,各团队引入的依赖项是错综复杂的;
  • 容易漏掉某一依赖项,问题容易在线上运行时暴发;
  • 重命名包,会导致不容易进行Debug。

Agent不推荐使用maven-shade-plugin
解决依赖冲突,那么我们需要新的解决方式,即Custom ClassLoader

2.2 Custom ClassLoader

重新回顾下,Java中类是如何加载的:

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加载依赖的隔离,效果如下所示。

Agent与Application依赖隔离

3 ClassLoader依赖传递

本章节代码,节选自袁伟大佬的agent-demo.

假设对用户的printiInfo
函数进行增强,增强代码如下所示:

Agent Advice代码

增强后的代码如下所示:

Agent Advice增强后用户代码

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

类缺失异常

原因在于ObjectUtils.isEmpty(obj);
代码由Agent提供(Agent的Custom ClassLoader加载了ObjectUtils
依赖),但是此语句被ApplicationClassLoader加载。在ApplicationClassLoader加载的代码中,不包含ObjectUtils
依赖。

Agent提供方无法强制用户代码引入ObjectUtils依赖,因此,我们需要一种方法,可以将Agent的依赖项传递至Application。此处,我们将这种传递中间件称呼为Dispatcher
。回顾一下,Agent ClassLoader与Application ClassLoader的共同祖先是Bootstrap ClassLoader,因此我们需要围绕Bootstrap ClassLoader做文章。

ClassLoader通过Dispatcher传递依赖

此处,我们定义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接口的实例,代码如下所示:

实现了Action接口的Agent执行代码
Agent Advice代码

此处,可以看到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 拍摄的图片


文章转载自技术随想录,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论