在日常业务代码开发中,我们经常接触到AOP(面向切面编程),比如熟知的Spring AOP。我们经常用它来实现业务切面逻辑,比如登录校验,日志记录,性能监控,全局过滤器等。但Spring AOP有一个局限性, 并不是所有的类都托管在 Spring 容器中 ,例如很多中间件代码、三方包代码和部分原生代码,都不能被Spring AOP代理到。如此一来,一旦你想实现的切面逻辑并不属于Spring的管辖范围,或者你想实现Spring之外的切面功能,就无从下手。
(资料图片仅供参考)
对于Java后端应用, 有没有一种更为通用的AOP方式?答案是有的 ,Java自身提供了JVM TI,Instrumentation等特性和接口,允许使用者以通过一系列原生API完成对JVM的复杂控制。自此衍生出了很多著名的框架,比如Btrace,Arthas等等,帮助开发者们实现更多更复杂的Java功能。
JVM Sandbox也是其中的一员。当然,不同框架的设计目的和使命是不一样的, JVM-Sandbox的设计目的是实现一种在不重启、不侵入目标JVM应用情况下的AOP解决方案。
举几个典型的JVM-Sandbox应用场景:
流量回放 :如何录制线上应用每次接口请求的入参和出参?改动应用代码固然可以,但成本太大,通过JVM-Sandbox,可以直接在不修改代码的情况下,直接抓取接口的出入参。 安全漏洞热修复 :假设某个三方包(例如出名的fastjson)又出现了漏洞,集团内那么多应用,一个个发布新版本修复,漏洞已经造成了大量破坏。通过JVM-Sandbox,直接修改替换有漏洞的代码,及时止损。 接口故障模拟 :想要模拟某个接口超时5s后返回false的情况,JVM-Sandbox很轻松就能实现。 故障定位 :像Arthas类似的功能。 接口限流 :动态对指定的接口做限流。 日志打印 ...可以看到,借助JVM-Sandbox,你可以实现很多之前在传统切面实现中做不了的事,大大拓展了切面可操作的范围。
本文围绕JVM SandBox展开,主要介绍如下内容:
JVM SandBox的诞生背景 JVM SandBox的架构设计 代码实战:Spring Bean初始化耗时统计 JVM SandBox的底层技术 总结JVM Sandbox诞生的技术背景在引言中已经赘述完毕,下面是作者开发该框架的一些业务背景,以下描述引用自文章[1]:
JVM SandBox 是阿里开源的一款 JVM 平台非侵入式运行期 AOP 解决方案,本质上是一种 AOP 落地形式。那么可能有同学会问:已有成熟的 Spring AOP 解决方案,阿里巴巴为什么还要“重复造轮子”?这个问题要回到 JVM SandBox 诞生的背景中来回答。在 2016 年中,天猫双十一催动了阿里巴巴内部大量业务系统的改动,恰逢徐冬晨(阿里巴巴测试开发专家)所在的团队调整,测试资源保障严重不足,迫使他们必须考虑更精准、更便捷的老业务测试回归验证方案。开发团队面临的是新接手的老系统,老的业务代码架构难以满足可测性的要求,很多现有测试框架也无法应用到老的业务系统架构中,于是需要新的测试思路和测试框架。
本章节不详细赘述JVM SandBox的架构设计,只讲其中几个重要的特性,详细的架构设计可以看开源代码的Wiki[2]。
很多框架通过破坏双亲委派(我更愿意称之为直系亲属委派)来实现类隔离,JVM SandBox也不例外。它通过自定义的SandboxClassLoader破坏了双亲委派的约定,实现了几个隔离特性:
和目标应用的类隔离:不用担心加载沙箱会引起原应用的类污染、冲突。 模块之间类隔离:做到模块与模块之间、模块和沙箱之间、模块和应用之间互不干扰。JVM-SANDBOX属于基于Instrumentation的动态编织类的AOP框架, 通过精心构造了字节码增强逻辑,使得沙箱的模块能在不违反JDK约束情况下实现对目标应用方法的无侵入运行时AOP拦截 。
从上图中,可以看到一个方法的整个执行周期都被代码“加强”了,能够带来的好处就是你在使用JVM SandBox只需要对于方法的事件进行处理。
主要是以下三种事件:
// BEFOREtry { /* * do something... */ // RETURN return;} catch (Throwable cause) { // THROWS}
在沙箱的世界观中,任何一个Java方法的调用都可以分解为BEFORE、RETURN和THROWS三个环节,由此在三个环节上引申出对应环节的事件探测和流程控制机制。
基于BEFORE、RETURN和THROWS三个环节事件分离,沙箱的模块可以完成很多类AOP的操作。
可以感知和改变方法调用的入参 可以感知和改变方法调用返回值和抛出的异常 可以改变方法执行的流程 在方法体执行之前直接返回自定义结果对象,原有方法代码将不会被执行 在方法体返回之前重新构造新的结果对象,甚至可以改变为抛出异常 在方法体抛出异常之后重新抛出新的异常,甚至可以改变为正常返回(引用自官方文档)
一切都是事件驱动的,这一点你可能很迷糊,但是别担心,请继续往下阅读,在下文的实战环节中,可以帮助你理解事件驱动的含义。
示例代码版本:JVM-Sandbox 1.2.0
在平常的业务开发中,我们会遇到很多启动时间非常久的应用,通常都是因为业务代码堆积,缺乏维护导致。通常我们会视而不见,忍一忍海阔天空。但是当积累到一定的程度,每次部署应用就成了开发者们的负担,常常需要等待几分钟甚至十几分钟。对于Spring应用来说,大部分启动时间都消耗在了Spring Bean的初始化上,所以我们来实现一个小工具, 统计Spring Bean初始化的耗时 ,最后形成数据表,供开发者优化分析。
在JVM SandBox中如何实现上面的工具?其实非常简单。
先贴上思路的整体流程:
图3 工具流程图
首先新建Maven工程,在Maven依赖中引用JVM SandBox,官方推荐独立工程使用parent方式。
com.alibaba.jvm.sandbox sandbox-module-starter1.2.0
新建一个类作为一个JVM SandBox模块,如下图:
图4 主入口代码实现
使用@Infomation声明mode为AGENT模式,一共有两种模式Agent和Attach。
Agent:随着JVM启动一起启动 Attach:在已经运行的JVM进程中,动态的插入我们由于是监控JVM启动数据,所以需要AGENT模式。
其次,继承com.alibaba.jvm.sandbox.api.Module和com.alibaba.jvm.sandbox.api.ModuleLifecycle。
其中ModuleLifecycle包含了整个模块的生命周期回调函数。
onLoad:模块加载,模块开始加载之前调用!模块加载是模块生命周期的开始,在模块生命中期中有且只会调用一次。 这里抛出异常将会是阻止模块被加载的唯一方式,如果模块判定加载失败,将会释放掉所有预申请的资源,模块也不会被沙箱所感知。 onUnload:模块卸载,模块开始卸载之前调用!模块卸载是模块生命周期的结束,在模块生命中期中有且只会调用一次。 这里抛出异常将会是阻止模块被卸载的唯一方式,如果模块判定卸载失败,将不会造成任何资源的提前关闭与释放,模块将能继续正常工作。 onActive:模块被激活后,模块所增强的类将会被激活,所有com.alibaba.jvm.sandbox.api.listener.EventListener将开始收到对应的事件。 onFrozen:模块被冻结后,模块所持有的所有com.alibaba.jvm.sandbox.api.listener.EventListener将被静默,无法收到对应的事件。 需要注意的是,模块冻结后虽然不再收到相关事件,但沙箱给对应类织入的增强代码仍然还在。 loadCompleted:模块加载完成,模块完成加载后调用!模块完成加载是在模块完成所有资源加载、分配之后的回调,在模块生命中期中有且只会调用一次。 这里抛出异常不会影响模块被加载成功的结果。模块加载完成之后,所有的基于模块的操作都可以在这个回调中进行。最常用的是loadCompleted,所以我们重写loadCompleted类,在里面开启我们的监控类SpringBeanStartMonitor线程。
而SpringBeanStartMonitor的核心代码如下图:
图5 过滤器代码实现
使用Sandbox的doClassFilter过滤出匹配的类,这里我们是BeanFactory,接着使用doMethodFilter过滤出要监听的方法,这里是initializeBean。我们取initializeBean作为统计耗时的切入方法。为什么选择该方法,涉及到SpringBean的启动生命周期,不在本文赘述范围内。
图6 initializeBean入口源码注释
接着,我们使用JVM Sandbox提供的方法moduleEventWatcher.watch(springBeanFilter, springBeanInitListener, Event.Type.BEFORE, Event.Type.RETURN);将我们的springBeanInitListener监听器绑定到被观测的方法上,这样每次initializeBean被调用,都会走到我们的切面逻辑。
监听器的主要逻辑如下:
图7 监听逻辑代码实现
代码有点长,不必细看,主要就是在原方法的BeforeEvent(进入前)和ReturnEvent(执行正常返回后)执行上述的切面逻辑,我这里便是使用了一个HashMap存储每个Bean的初始化开始和结束时间,最终统计出初始化耗时。
最终,我们还需要一个方法来知道我们的原始Spring应用已经启动完毕,这样我们可以手动卸载我们的Sandbox模块,毕竟他已经完成了他的历史使命,不需要再依附在主进程上。
我们通过一个简陋的办法,检查应用的HTTP端口是否会返回小于500的状态码,来判断Spring容器是否已经正确启动并对外提供服务。当然如果你的Spring没有使用Web框架,就不能用这个方法来判断启动完成,你也许可以通过Spring自己的生命周期钩子函数来实现,这里图方便,省略了更复杂的实现。
整个SpringBean监听模块的开发就完成了,你可以感受到,你的开发和日常业务开发几乎没有区别,这就是JVM Sandbox封装带来的便利。
下面是实际运行的效果:
图7 工具生成的SpringBean耗时报表
PS:该工具经过拓展,已经在集团内部已经孵化为 应用启动速度治理平台 ,采集了包括SpringBean、HSF、CPU堆栈等Java后端应用启动数据,帮助集团内部应用优化部署时间,大幅度提升应用优化效率。
点击查看原文,获取更多福利!
https://developer.aliyun.com/article/1138309?utm_content=g_1000367731
版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。