一、啥是javaagent?

在日常的开发实践中,javaagent的应用场景可谓是非常广泛了,无论是在链路监控的APM中,还是在诊断工具的Arthas中,亦或是在处理log4j2漏洞的”疫苗“中,都能看到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_B 是study业务内(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

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

  • springboot中拦截并替换token来简化身份验证

说了这么多,你一定很好奇这样的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:本次的这个项目结构就是之前想法和实现方案的一些升级和完善,有其它想法意见的话欢迎交流呀!

搞了一个更完善的javaagent项目结构相关推荐

  1. apache geode项目结构_Apache Flink-基于Java项目模板创建Flink应用(流计算和批计算)...

    Apache Flink创建模板项目有2种方式: 1. 通过Maven archetype命令创建: 2. 通过Flink 提供的Quickstart shell脚本创建: 关于Apache Flin ...

  2. Android项目结构和AndroidManifest.xml

    创建项目 在开发一款Android应用的时候,第一步我们需要在Android的IDE开发工具中去创建一个项目.接下来会对创建项目和项目结构中各个步骤,路径功能做个梳理和讲解. Application ...

  3. 为什么Android项目mainactivity中有一个变量R_安卓4:第一个安卓程序 AS 安卓项目结构解析 手机运行app 模拟器运行app...

    学习于:https://www.bilibili.com/video/av22836860?p=2 首先,要知道AS的一个基本模型,1个Android project可以有多个module,而每个mo ...

  4. 前端项目结构构建_如何通过构建项目成为更好的前端开发人员(包括想法)

    前端项目结构构建 If you want to fast-track your growth as a front-end developer, nothing beats doing real de ...

  5. 如何对聚类结果进行分析_如何更合理地给聚类结果贴标签——由一个挖掘学生用户的项目说开去...

    "聚类一时爽,判断两行泪"--这是解决任何一个无监督问题时都会面临的苦恼:最近接到了一个无监督问题的项目--给一群无标签的结构化数据贴标签,随后我便立即展开了工作,首先开始查阅资料 ...

  6. python开发项目架构图_我的第一个python web开发框架(8)——项目结构与RESTful接口风格说明...

    PS:再次说明一下,原本不想写的太啰嗦的,可之前那个系列发布后发现,好多朋友都想马上拿到代码立即能上手开发自己的项目,对代码结构.基础常识.分类目录与文件功能结构.常用函数......等等什么都不懂, ...

  7. 一个基于 Spring Boot 的项目骨架

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 最近使用Spring Boot 配合 MyBatis .通用Map ...

  8. 如何在实际中计划和执行一个机器学习和深度学习项目

    2019-11-27 20:27:28 作者:Sayak Paul 编译:ronghuaiyang 导读 做研究打比赛和真正的做一个机器学习和深度项目是不一样的,如果你有这方面的困惑的话,可以看看这篇 ...

  9. 一个基于 Spring Boot 的项目骨架,拿走即用

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:程序员入职国企,1周上班5小时,晒出薪资感叹:腾讯当CEO也不去个人原创+1博客:点击前往,查看更多 作者:简单 ...

  10. 一个基于 Spring Boot 的项目骨架,少造轮子!

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:牛人 20000 字的 Spring Cloud 总结,太硬核了 最近使用Spring Boot 配合 MyBa ...

最新文章

  1. 【FI 收付款条件】Payment Terms 收付款条件
  2. 什么是CPAN(安装NAGIOS使用到)
  3. csrf-token
  4. Javascript:阻止浏览器默认右键事件,并显示定制内容
  5. 光盘刻录制作Ubuntu等操作系统的启动盘
  6. LeetCode 1087. 字母切换(回溯)
  7. python 字符串函数总结
  8. php判断字符串里有英文,PHP针对中英文混合字符串长度判断及截取方法示例
  9. alwayson故障转移群集服务器 修改虚拟主机名及IP地址
  10. SpringBoot系列五:SpringBoot错误处理(数据验证、处理错误页、全局异常)
  11. 使用FastDFS在CentOS上搭建简易分布式文件系统
  12. EFR32FG1开发教程1--点亮LED
  13. 在delphi中调用chm帮助文件_delphi教程
  14. 原来这就是公文写作领导讲话稿模板(3)
  15. 时序报告要看哪些指标
  16. 软约束、硬约束、Minimum Snap的轨迹优化方法
  17. 什么是ECS + Job
  18. matlab中appdesigner的控件简单讲解
  19. java geojson和数据库_GeoJson和TopoJson数据格式的对比
  20. 【软件过程管理】课程知识点梳理及习题

热门文章

  1. To install it, you can run: npm install --save element-uib/theme-chalk/index.css
  2. Excel表格中正数设置为红色负为绿色
  3. java word 添加图片_java – 在word文档中插入图片
  4. 版本名称SNAPSHOT、alpha、beta、release、GA含义
  5. 使用Java模拟登录KINGOSOFT青果教务系统(湖北三峡职业技术学院)
  6. Python之深入解析Numpy的高级操作和使用
  7. Html设置图片大小代码
  8. Java生成桌面快捷方式(字节流生成)
  9. MAC-快捷键打开终端
  10. 学会演讲必看的五本书籍推荐