Création d'une structure de projet javaagent plus complète

Pré : le livre continue au-dessus

J'ai écrit un javaagent dans l'article précédent, mais j'ai trouvé plus tard qu'il n'est pas très utile, cette fois nous allons en faire un autre !

Analyse et solution de l'erreur NoClassDefFoundError lors de l'exécution de javaagent sur Springboot

1. Qu'est-ce que javaagent ?

Dans la pratique quotidienne du développement, les scénarios d'application de javaagent sont très larges, que ce soit dans APM pour la surveillance des liens, Arthas pour les outils de diagnostic, ou "vaccin" pour traiter les vulnérabilités log4j2.Voir la figure de javaagent et jouer un rôle important.

Faute d'espace, je ne le présenterai pas en détail ici, et les étudiants intéressés pourront l'apprendre par eux-mêmes !

ps : javaagent peut aussi faire beaucoup, beaucoup de choses puissantes et intéressantes !

Deuxièmement, quels problèmes avez-vous besoin d'utiliser javagne pour résoudre ?

pré. État actuel des déploiements multiservices et multienvironnements

Œuvres sans titre 22.png

  • Plusieurs environnements ne font pas référence à plusieurs environnements tels que dev, fat, uat, promais incluent plusieurs environnements tels fatque fat1, , fat2, fat3etc.
  • Dans différentes entreprises, telles que study, play, le nombre d'environnements déployés fatest incertain
  • Dans une même entreprise, fatles services déployés dans chaque environnement ne sont pas forcément cohérents, par exemple study, dans l'entreprise, fat-3seuls les services sont déployés appA, mais pas déployés appB.

ps: fatL'environnement partage un registre, et il n'y a qu'une seule base de données pour chaque service (par exemple appA), et il en va de même pour la configuration apollo

1. Quels problèmes peuvent être causés par le déploiement d'un tel service ?

一些基于或类似于注册中心的调用可能会出现不可控的情况,示意如下: Œuvre sans titre 23.pngappC去调用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隔离

先给个环境隔离的示意图: Œuvres sans titre 17.png

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

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

Œuvres sans titre 16.png

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

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

Œuvres sans titre 15.png

  • 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来启动的线程,目前的场景是竞争处理一个队列中的任务: Œuvres sans titre 24.png

隔离处理方式示意图: Œuvres sans titre 25.png

总结:

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

d.xxl-job隔离

xxl-job注册示意如下: Œuvres sans titre 26.png

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

总结:

  • 需要修改某些环境服务注册使用的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吧!

结构图: javaagent-unzip.png

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

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

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

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

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

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即可。 image.png

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配置即可: image.png

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

1.先看结果

Oeuvre sans titre 19.png 项目最终是产生了4个子模块:

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

2.演进过程

该项目结构不是一蹴而就的,而是随着需求丰富逐步增加的: Œuvres sans titre 18.png

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

3.依赖关系

Œuvre sans titre 20.png

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

4.打包过程

Œuvres sans titre 21.png

  • a.先打包transformer,仅打包类,没有特殊处理 image.png
  • b.再打包helper,此时会对helper中的一些依赖进行shadow操作,如slf4j image.png
  • c.package阶段:
    • package第一阶段:对dependencies进行shadow操作,并解压到顶层目录,此时helper模块非jar in jar依赖会在此时生成。
    • package第二阶段:复制一份helper/BOOT-INF/lib下,也就是jar in jarhelper模块

六、代码

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

ps : la structure de ce projet est constituée de quelques mises à jour et améliorations des idées précédentes et des plans de mise en œuvre. Si vous avez d'autres idées et opinions, bienvenue pour échanger !

Guess you like

Origin juejin.im/post/7135734853869404167