搞了一个更完善的javaagent项目结构
一、啥是javaagent?
在日常的开发实践中,javaagent的应用场景可谓是非常广泛了,无论是在链路监控的APM中,还是在诊断工具的Arthas中,亦或是在处理log4j2漏洞的”疫苗“中,都能看到javaagent的身影,并发挥着重要的作用。
二、遇到了啥问题需要用javaagnent来解决呢?
pre.多服务、多环境的部署的现状
- 多环境不是指
dev
、fat
、uat
、pro
这样的多环境,而是fat
中包含了fat1
、fat2
、fat3
等多套环境 - 不同的业务中,如
study
、play
,它们所部署的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_2
、Topic_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
依赖了helper
与transformer
,负责生成最终javaagent的jarhelper
依赖了transformer
,因为helper
中需要访问transformer
的配置等maven-shade-transformer
只是打包支持用的
4.打包过程
a.先打包transformer
,仅打包类,没有特殊处理
b.再打包helper
,此时会对helper
中的一些依赖进行shadow操作,如slf4j
- c.
package
阶段: - package第一阶段:对dependencies进行shadow操作,并解压到
顶层目录
,此时helper模块
的非jar in jar
依赖会在此时生成。
- package第一阶段:对dependencies进行shadow操作,并解压到
- package第二阶段:复制一份
helper
到/BOOT-INF/lib
下,也就是jar in jar
的helper模块
。
- package第二阶段:复制一份
六、代码
github链接(代码中仍有很多可以改善优化的地方,但是我已经迫不及待地分享啦!)
ps:本次的这个项目结构就是之前想法和实现方案的一些升级和完善,有其它想法意见的话欢迎交流呀!
搞了一个更完善的javaagent项目结构相关推荐
- apache geode项目结构_Apache Flink-基于Java项目模板创建Flink应用(流计算和批计算)...
Apache Flink创建模板项目有2种方式: 1. 通过Maven archetype命令创建: 2. 通过Flink 提供的Quickstart shell脚本创建: 关于Apache Flin ...
- Android项目结构和AndroidManifest.xml
创建项目 在开发一款Android应用的时候,第一步我们需要在Android的IDE开发工具中去创建一个项目.接下来会对创建项目和项目结构中各个步骤,路径功能做个梳理和讲解. Application ...
- 为什么Android项目mainactivity中有一个变量R_安卓4:第一个安卓程序 AS 安卓项目结构解析 手机运行app 模拟器运行app...
学习于:https://www.bilibili.com/video/av22836860?p=2 首先,要知道AS的一个基本模型,1个Android project可以有多个module,而每个mo ...
- 前端项目结构构建_如何通过构建项目成为更好的前端开发人员(包括想法)
前端项目结构构建 If you want to fast-track your growth as a front-end developer, nothing beats doing real de ...
- 如何对聚类结果进行分析_如何更合理地给聚类结果贴标签——由一个挖掘学生用户的项目说开去...
"聚类一时爽,判断两行泪"--这是解决任何一个无监督问题时都会面临的苦恼:最近接到了一个无监督问题的项目--给一群无标签的结构化数据贴标签,随后我便立即展开了工作,首先开始查阅资料 ...
- python开发项目架构图_我的第一个python web开发框架(8)——项目结构与RESTful接口风格说明...
PS:再次说明一下,原本不想写的太啰嗦的,可之前那个系列发布后发现,好多朋友都想马上拿到代码立即能上手开发自己的项目,对代码结构.基础常识.分类目录与文件功能结构.常用函数......等等什么都不懂, ...
- 一个基于 Spring Boot 的项目骨架
点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 最近使用Spring Boot 配合 MyBatis .通用Map ...
- 如何在实际中计划和执行一个机器学习和深度学习项目
2019-11-27 20:27:28 作者:Sayak Paul 编译:ronghuaiyang 导读 做研究打比赛和真正的做一个机器学习和深度项目是不一样的,如果你有这方面的困惑的话,可以看看这篇 ...
- 一个基于 Spring Boot 的项目骨架,拿走即用
点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:程序员入职国企,1周上班5小时,晒出薪资感叹:腾讯当CEO也不去个人原创+1博客:点击前往,查看更多 作者:简单 ...
- 一个基于 Spring Boot 的项目骨架,少造轮子!
点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:牛人 20000 字的 Spring Cloud 总结,太硬核了 最近使用Spring Boot 配合 MyBa ...
最新文章
- 【FI 收付款条件】Payment Terms 收付款条件
- 什么是CPAN(安装NAGIOS使用到)
- csrf-token
- Javascript:阻止浏览器默认右键事件,并显示定制内容
- 光盘刻录制作Ubuntu等操作系统的启动盘
- LeetCode 1087. 字母切换(回溯)
- python 字符串函数总结
- php判断字符串里有英文,PHP针对中英文混合字符串长度判断及截取方法示例
- alwayson故障转移群集服务器 修改虚拟主机名及IP地址
- SpringBoot系列五:SpringBoot错误处理(数据验证、处理错误页、全局异常)
- 使用FastDFS在CentOS上搭建简易分布式文件系统
- EFR32FG1开发教程1--点亮LED
- 在delphi中调用chm帮助文件_delphi教程
- 原来这就是公文写作领导讲话稿模板(3)
- 时序报告要看哪些指标
- 软约束、硬约束、Minimum Snap的轨迹优化方法
- 什么是ECS + Job
- matlab中appdesigner的控件简单讲解
- java geojson和数据库_GeoJson和TopoJson数据格式的对比
- 【软件过程管理】课程知识点梳理及习题
热门文章
- To install it, you can run: npm install --save element-uib/theme-chalk/index.css
- Excel表格中正数设置为红色负为绿色
- java word 添加图片_java – 在word文档中插入图片
- 版本名称SNAPSHOT、alpha、beta、release、GA含义
- 使用Java模拟登录KINGOSOFT青果教务系统(湖北三峡职业技术学院)
- Python之深入解析Numpy的高级操作和使用
- Html设置图片大小代码
- Java生成桌面快捷方式(字节流生成)
- MAC-快捷键打开终端
- 学会演讲必看的五本书籍推荐