在日常的开发实践中,javaagent的应用场景可谓是非常广泛了,无论是在链路监控的APM中,还是在诊断工具的Arthas中,亦或是在处理log4j2漏洞的”疫苗“中,都能看到javaagent的身影,并发挥着重要的作用。
限于篇幅原因,这里就不详细介绍了,有兴趣的同学可以自行了解!
ps:javaagent还可以做很多很多很强大且有趣的事情!

dev、fat、uat、pro这样的多环境,而是 fat 中包含了 fat1、fat2、fat3 等多套环境study、play,它们所部署的fat环境数量是不确定的fat环境部署的服务也不一定是一致的,如study业务中的fat-3中仅部署了appA,没有部署appB。ps:
fat环境共用一个注册中心,每个服务(如appA)数据库也是只有一个,apollo配置亦是如此
一些基于或类似于注册中心的调用可能会出现不可控的情况,示意如下:

当appC去调用appA的时候,可能会调用到3套环境中的其中一个,是不可控。 为了解决这个问题,就需要对各个环境的调用进行隔离。
ps:更详细的可背景以参考之前的文章:一种多业务下多环境的dubbo隔离方案
常用的基于或者类似于注册中心的调用有以下这些:
能实现隔离功能就行了嘛?不一定!除了解决基本的隔离的基本问题外,还期待:
通过配置文件进行配置,支持统一管理,不需要跨越多个平台来配置。
这个属于底线要求了,不能影响正常的业务功能逻辑。
这并不是一个业务需求,并且是不需要上线的,因此不宜提交。
目前存在的运行方式有:
jar in jar形式)非jar in jar形式)ps:若有一种运行环境不支持,或一个服务不配合,都无法达到完整的隔离
先给个环境隔离的示意图:

Topic_internal_A_to_B 是study业务内(appA生产消息,appB消费消息)的主题Topic_external_x 是外部业务作为生产者,study业务需要进行消费的主题i.处理当前业务内部的topic隔离:

appA发送消息时,重命名发送的topic,如Topic_internal_A_to_B_fat1appB消费消息时,订阅相应的topic,如Topic_internal_A_to_B_fat1ii.处理消费外部的topic隔离:

appA向注册中心注册时,group带上环境标识,如fat-1fat-1中不订阅Topic_external_2、Topic_external_3这两个topic总结:
与mq的处理类似,这里就不重复了。
总结:
ps:如果仍有疑问还是可以参考之前的文章:一种多业务下多环境的dubbo隔离方案,处理方案是一样的,不同的是之前是在项目内处理,现在换成javaagnet实现
这边的runner线程指的是springboot中继承了CommandLineRunner来启动的线程,目前的场景是竞争处理一个队列中的任务:

隔离处理方式示意图:

总结:
xxl-job注册示意如下:

当有任务需要调度时,也是会按某种规则从3个appA中选一个来进行执行,某些情况下也是不可控的,解决方法也很简单,覆盖注册的名称即可:

总结:
ps:看起来实现并不难,归结为拦截属性、禁用bean两种操作,真的有这么简单?
那么,怎么样才算是优雅呢?
举例:在javaagent中使用了1.0版本的StringUtils类,而宿主服务中使用了2.0版本的StringUtils类,那么当jvm在执行javaagent里相关逻辑过程中加载了1.0版本的StringUtils类时,就不会再尝试加载2.0版本的StringUtils类(同一个类加载器下),这可能导致宿主服务出现异常。
因为要基于宿主内使用的组件来做一些处理,所以编写和运行时候都需要能访问相关的类,甚至是需要调用宿主应用中的bean。
应用运行的环境是硬性条件,很难为了隔离而强制要求开发小伙伴更换应用的运行方式。
当处理过程中需要进行集合操作等较为复杂的流程时,如果以字符串形式插入一堆复杂的代码,会导致:
所以更稳妥的方法是将相关的处理逻辑封装到方法,在插桩时仅插入这个方法的调用即可。
这里的统一指的是在javaagent中打出的日志应该是一致的,更甚者可能要求跟宿主的日志保持一致。 如果你使用了System.out来进行日志输出,那你大概率会被锤的了。
基于此能够实现一些有趣的东西,参考之前的文章:
说了这么多,你一定很好奇这样的javaagent到底长什么样吧!
结构图:

咋一看,这可一点都不优雅了呀,不急,且听我娓娓道来!
为了封装相关逻辑,我们将javaagent分成两部分:
helper 模块transformer 模块因此在具体操作时,一般只会往字节码中插入方法的调用,如下:

ps:由于运行环境和类加载的不确定
transformer模块不一定能调用helper模块!
回应上文的举例:我们可以使用shade插件的relocation特性,修改javaagent中的StringUtils类的全限定名,如从org.apache.commons.lang3.StringUtils改为 shaded.org.apache.commons.lang3.StringUtils类,这样就不会冲突了。
如结构图所示:对相关的工具类进行了更改包名的操作(javaassist、jsoup、slf4等),都在其原有包名基础上加入了shaded前缀,这样就能确保不会与宿主应用的依赖产生冲突,因此也不会出现类覆盖的情况。
这里说白了就是要求javaagent内在书写、编译、运行时都能访问到宿主应用的类,但是运行时相关的类在宿主应用的依赖中已经有了,因此javaagent中不能重复出现。
因此结构图中可见压jar中并没有宿主应用的类,在maven引入这些依赖时scope使用provided即可。

方向:处理的重点是
helper模块,因为该模块依赖了宿主应用的类。
几点必要的说明:
helper模块中的类是会被宿主应用执行过程中被调用的,而helper模块本身又依赖了宿主应用的类,因此,helper模块与 应用的类 必须是被同一个类加载器加载。AppClassLoader的加载路径中。jar in jar形式启动时,宿主应用会被以jar in jar形式加载,其类加载器是AppClassLoader的子类加载器LanuchedURLClassLoader。基于此,要想兼容运行jar in jar形式启动的服务,需要做到:
helper模块对AppClassLoader不可见,否则会直接被AppClassLoader提前加载(双亲委派)helper模块能被LanuchedURLClassLoader加载。具体措施是:
helper模块放进jar in jar中,这对AppClassLoader不可见。helper模块的jar in jar路径添加到LanuchedURLClassLoader的类加载路径中,使其能够被搜索加载。结果是:
helper模块同时存在于顶层目录和 /BOOT-INF.lib/ 中,简单来说是因为jar in jar形式下,访问的是 /BOOT-INF.lib/ 中的jar包依赖的,而非jar in jar形式下运,访问的是顶层目录中的helper模块。/BOOT-INF.lib/ 与顶层目录中的的helper模块的包名是不一致的,并且在具体插桩的时候包了一层TransformerHelper.unShadeIfNecessary,为的就是控制不用运行环境下访问不同位置的helper模块。参考:
使用日志组件即可,目前使用的是slf4j。
以依赖形式来注入bean的常用方式是增加 /META-INF/spring.factories 配置,因此结构图中可见,helper模块中是有 /META-INF/spring.factories 文件的。
ps:细心的你一定发现在
jar in jar中的是没有shaded开头的,而顶层目录里是有的,这也是为了兼容两个环境做的处理
直接用http请求访问一个统一的apollo配置即可:


项目最终是产生了4个子模块:
helper:封装复杂的操作逻辑,对应了上文的helper模块。transformer:入口、同时也是插桩操作的实现,对应了上文的transformer模块。package:没有代码,仅做打包用,为的是能同时将helper模块解压到顶层目录和放到 /BOOT-INF.lib/中。maven-shade-transformer:合并spring.factories需要用到的plugin配置。该项目结构不是一蹴而就的,而是随着需求丰富逐步增加的:

helper子项目与transformer子项目。package子项目spring.factories时新增了maven-shade-transformer子项目
package依赖了helper与transformer,负责生成最终javaagent的jarhelper依赖了transformer,因为helper中需要访问transformer的配置等maven-shade-transformer只是打包支持用的
transformer,仅打包类,没有特殊处理 
helper,此时会对helper中的一些依赖进行shadow操作,如slf4j 
package阶段:顶层目录,此时helper模块的非jar in jar依赖会在此时生成。helper到/BOOT-INF/lib下,也就是jar in jar的helper模块。github链接(代码中仍有很多可以改善优化的地方,但是我已经迫不及待地分享啦!)
ps:本次的这个项目结构就是之前想法和实现方案的一些升级和完善,有其它想法意见的话欢迎交流呀!
来源:https://juejin.cn/post/7135734853869404167