• 搞了一个更完善的javaagent项目结构


    一、啥是javaagent?

    在日常的开发实践中,javaagent的应用场景可谓是非常广泛了,无论是在链路监控的APM中,还是在诊断工具的Arthas中,亦或是在处理log4j2漏洞的”疫苗“中,都能看到javaagent的身影,并发挥着重要的作用。

    限于篇幅原因,这里就不详细介绍了,有兴趣的同学可以自行了解!

    ps:javaagent还可以做很多很多很强大且有趣的事情!

    二、遇到了啥问题需要用javaagnent来解决呢?

    pre.多服务、多环境的部署的现状

    • 多环境不是指devfatuatpro这样的多环境,而是 fat 中包含了 fat1fat2fat3 等多套环境
    • 不同的业务中,如studyplay,它们所部署的fat环境数量是不确定的
    • 同一个业务中,每套fat环境部署的服务也不一定是一致的,如study业务中的fat-3中仅部署了appA,没有部署appB

    ps:fat环境共用一个注册中心,每个服务(如appA)数据库也是只有一个,apollo配置亦是如此

    1.如此部署服务会导致什么问题?

    一些基于或类似于注册中心的调用可能会出现不可控的情况,示意如下:

    appC去调用appA的时候,可能会调用到3套环境中的其中一个,是不可控。 为了解决这个问题,就需要对各个环境的调用进行隔离。

    ps:更详细的可背景以参考之前的文章:一种多业务下多环境的dubbo隔离方案

    2.需要隔离的“调用”有哪些?

    常用的基于或者类似于注册中心的调用有以下这些:

    • mq
    • dubbo
    • xxl-job
    • runner线程
    • 其它自研的框架调用

    3.能“抓老鼠”就是好猫?

    能实现隔离功能就行了嘛?不一定!除了解决基本的隔离的基本问题外,还期待:

    a.可配置

    通过配置文件进行配置,支持统一管理,不需要跨越多个平台来配置。

    b.不影响宿主应用的正常功能使用

    这个属于底线要求了,不能影响正常的业务功能逻辑。

    c.不侵入代码提交

    这并不是一个业务需求,并且是不需要上线的,因此不宜提交。

    d.兼容多种运行场景

    目前存在的运行方式有:

    • 使用springboot的打包插件,打包成一个fatjar来启动(下称jar in jar形式)
    • 指定class文件启动,不打包成jar包(下称 非jar in jar形式)

    ps:若有一种运行环境不支持,或一个服务不配合,都无法达到完整的隔离

    三、javaagent是怎么实现环境隔离的?

    1.首先来分析下能不能实现隔离

    a.mq隔离

    先给个环境隔离的示意图:

    • Topic_internal_A_to_Bstudy业务内(appA生产消息,appB消费消息)的主题
    • Topic_external_x 是外部业务作为生产者,study业务需要进行消费的主题

    i.处理当前业务内部的topic隔离:

    • appA发送消息时,重命名发送的topic,如Topic_internal_A_to_B_fat1
    • appB消费消息时,订阅相应的topic,如Topic_internal_A_to_B_fat1

    ii.处理消费外部的topic隔离:

    • appA向注册中心注册时,group带上环境标识,如fat-1
    • 将不期待消费的topic进行禁用,不订阅,从而避免重复消费,如fat-1中不订阅Topic_external_2Topic_external_3这两个topic

    总结:

    • 需要拦截修改发送消息的topic
    • 需要拦截修改subscribe的topic
    • 需要拦截修改subscribe的group
    • 需要禁用某些topic的订阅

    b.dubbo隔离

    与mq的处理类似,这里就不重复了。

    总结:

    • 需要拦截修改指定provider-api的group
    • 需要拦截修改指定consumer-api的group
    • 需要禁用provider注册

    ps:如果仍有疑问还是可以参考之前的文章:一种多业务下多环境的dubbo隔离方案,处理方案是一样的,不同的是之前是在项目内处理,现在换成javaagnet实现

    c.runner线程控制

    这边的runner线程指的是springboot中继承了CommandLineRunner来启动的线程,目前的场景是竞争处理一个队列中的任务:

    隔离处理方式示意图:

    总结:

    • 禁用部分服务的runner线程,不给启动

    d.xxl-job隔离

    xxl-job注册示意如下:

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

    总结:

    • 需要修改某些环境服务注册使用的appName

    ps:看起来实现并不难,归结为拦截属性、禁用bean两种操作,真的有这么简单?

    2.还要优雅地实现!!

    那么,怎么样才算是优雅呢?

    a.不能与宿主应用的类产生冲突

    举例:在javaagent中使用了1.0版本的StringUtils类,而宿主服务中使用了2.0版本的StringUtils类,那么当jvm在执行javaagent里相关逻辑过程中加载了1.0版本的StringUtils类时,就不会再尝试加载2.0版本的StringUtils类(同一个类加载器下),这可能导致宿主服务出现异常。

    b.能使用宿主应用的类

    因为要基于宿主内使用的组件来做一些处理,所以编写和运行时候都需要能访问相关的类,甚至是需要调用宿主应用中的bean。

    c.兼容两种运行方式

    应用运行的环境是硬性条件,很难为了隔离而强制要求开发小伙伴更换应用的运行方式。

    d.复杂逻辑的封装再插桩

    当处理过程中需要进行集合操作等较为复杂的流程时,如果以字符串形式插入一堆复杂的代码,会导致:

    • 第一可阅读性不佳
    • 第二非常容易出现编译错误
    • 第三调试起来可谓是地狱难度

    所以更稳妥的方法是将相关的处理逻辑封装到方法,在插桩时仅插入这个方法的调用即可。

    e.日志统一

    这里的统一指的是在javaagent中打出的日志应该是一致的,更甚者可能要求跟宿主的日志保持一致。 如果你使用了System.out来进行日志输出,那你大概率会被锤的了。

    f.能够注入自定义的bean

    基于此能够实现一些有趣的东西,参考之前的文章:

    说了这么多,你一定很好奇这样的javaagent到底长什么样吧!

    四、那么,我们来解剖一个优雅的javaagent吧!

    结构图:

    咋一看,这可一点都不优雅了呀,不急,且听我娓娓道来!

    1.复杂逻辑的封装插桩运行

    为了封装相关逻辑,我们将javaagent分成两部分:

    • 一部分是封装复杂业务逻辑,也就结构图中的 helper 模块
    • 一部分则是具体插桩的操作,也就是结构图中的 transformer 模块

    因此在具体操作时,一般只会往字节码中插入方法的调用,如下:

    ps:由于运行环境和类加载的不确定transformer模块不一定能调用helper模块

    2.不与宿主应用的类产生冲突

    回应上文的举例:我们可以使用shade插件的relocation特性,修改javaagent中的StringUtils类的全限定名,如从org.apache.commons.lang3.StringUtils改为 shaded.org.apache.commons.lang3.StringUtils类,这样就不会冲突了。

    如结构图所示:对相关的工具类进行了更改包名的操作(javaassist、jsoup、slf4等),都在其原有包名基础上加入了shaded前缀,这样就能确保不会与宿主应用的依赖产生冲突,因此也不会出现类覆盖的情况。

    3.能使用宿主应用的类

    这里说白了就是要求javaagent内在书写、编译、运行时都能访问到宿主应用的类,但是运行时相关的类在宿主应用的依赖中已经有了,因此javaagent中不能重复出现。

    因此结构图中可见压jar中并没有宿主应用的类,在maven引入这些依赖时scope使用provided即可。

    4.兼容两种运行方式

    方向:处理的重点是helper模块,因为该模块依赖了宿主应用的类。

    几点必要的说明:

    • 第一点:helper模块中的类是会被宿主应用执行过程中被调用的,而helper模块本身又依赖了宿主应用的类,因此,helper模块与 应用的类 必须是被同一个类加载器加载。
    • 第二点:javaagent的jar包会被添加到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模块

    参考:

    5.日志统一

    使用日志组件即可,目前使用的是slf4j。

    6.能够注入自定义的bean

    以依赖形式来注入bean的常用方式是增加 /META-INF/spring.factories 配置,因此结构图中可见,helper模块中是有 /META-INF/spring.factories 文件的。

    ps:细心的你一定发现在jar in jar中的是没有shaded开头的,而顶层目录里是有的,这也是为了兼容两个环境做的处理

    7.可配置

    直接用http请求访问一个统一的apollo配置即可:

    五、那么,要怎样才能生成这样的javaagnent呢?

    1.先看结果

    项目最终是产生了4个子模块:

    • helper:封装复杂的操作逻辑,对应了上文的helper模块
    • transformer:入口、同时也是插桩操作的实现,对应了上文的transformer模块
    • package:没有代码,仅做打包用,为的是能同时将helper模块解压到顶层目录和放到 /BOOT-INF.lib/中。
    • maven-shade-transformer:合并spring.factories需要用到的plugin配置。

    2.演进过程

    该项目结构不是一蹴而就的,而是随着需求丰富逐步增加的:

    • 分离业务逻辑与插桩操作时拆分了helper子项目transformer子项目
    • 修改打包方式,兼容两种形式的启动方式时新增了package子项目
    • 支持自定义bean,合并spring.factories时新增了maven-shade-transformer子项目

    3.依赖关系

    • package依赖了helpertransformer,负责生成最终javaagent的jar
    • helper依赖了transformer,因为helper中需要访问transformer的配置等
    • maven-shade-transformer只是打包支持用的

    4.打包过程

    • a.先打包transformer,仅打包类,没有特殊处理

    • b.再打包helper,此时会对helper中的一些依赖进行shadow操作,如slf4j

    • c.package阶段:
      • package第一阶段:对dependencies进行shadow操作,并解压到顶层目录,此时helper模块非jar in jar依赖会在此时生成。
      • package第二阶段:复制一份helper/BOOT-INF/lib下,也就是jar in jarhelper模块

    六、代码

    github链接(代码中仍有很多可以改善优化的地方,但是我已经迫不及待地分享啦!)

    ps:本次的这个项目结构就是之前想法和实现方案的一些升级和完善,有其它想法意见的话欢迎交流呀!


    来源:https://juejin.cn/post/7135734853869404167

  • 相关阅读:
    布隆过滤器原理,优缺点
    量化风控的贷前实操课—详解的规则调优
    Redis发布订阅
    Squid代理服务器应用
    使用ipdb方式进行debug
    MPC入门
    laravel常用辅助函数
    linux12企业实战 -- 56 zabbix常规监控错误汇总
    STM32 PA15/JTDI 用作普通IO,烧录口不能使用问题解决
    flex布局相关总结&&实现元素框居中效果
  • 原文地址:https://blog.csdn.net/Chenhui98/article/details/126537665