如何把Java代码玩出花?JVM Sandbox入门教程与原理浅谈
作者:互联网
在日常业务代码开发中,我们经常接触到AOP,比如熟知的Spring AOP。我们用它来做业务切面,比如登录校验,日志记录,性能监控,全局过滤器等。但Spring AOP有一个局限性,并不是所有的类都托管在 Spring 容器中,例如很多中间件代码、三方包代码,Java原生代码,都不能被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架构设计
- JVM SandBox代码实战
- JVM SandBox底层技术
- 总结与展望
JVM Sandbox诞生背景
JVM Sandbox诞生的技术背景在引言中已经赘述完毕,下面是作者开发该框架的一些业务背景,以下描述引用自文章:
JVM SandBox 是阿里开源的一款 JVM 平台非侵入式运行期 AOP 解决方案,本质上是一种 AOP 落地形式。那么可能有同学会问:已有成熟的 Spring AOP 解决方案,阿里巴巴为什么还要“重复造轮子”?这个问题要回到 JVM SandBox 诞生的背景中来回答。在 2016 年中,天猫双十一催动了阿里巴巴内部大量业务系统的改动,恰逢徐冬晨(阿里巴巴测试开发专家)所在的团队调整,测试资源保障严重不足,迫使他们必须考虑更精准、更便捷的老业务测试回归验证方案。开发团队面临的是新接手的老系统,老的业务代码架构难以满足可测性的要求,很多现有测试框架也无法应用到老的业务系统架构中,于是需要新的测试思路和测试框架。
为什么不采用 Spring AOP 方案呢?Spring AOP 方案的痛点在于不是所有业务代码都托管在 Spring 容器中,而且更底层的中间件代码、三方包代码无法纳入到回归测试范围,更糟糕的是测试框架会引入自身所依赖的类库,经常与业务代码的类库产生冲突,因此,JVM SandBox 应运而生。
JVM Sandbox整体架构
本章节不详细讲述JVM SandBox的所有架构设计,只讲其中几个最重要的特性。详细的架构设计可以看原框架代码仓库的Wiki。
类隔离
很多框架通过破坏双亲委派(我更愿意称之为直系亲属委派)来实现类隔离,SandBox也不例外。它通过自定义的SandboxClassLoader破坏了双亲委派的约定,实现了几个隔离特性:
- 和目标应用的类隔离:不用担心加载沙箱会引起原应用的类污染、冲突。
- 模块之间类隔离:做到模块与模块之间、模块和沙箱之间、模块和应用之间互不干扰。
无侵入AOP与事件驱动
JVM-SANDBOX属于基于Instrumentation的动态编织类的AOP框架,通过精心构造了字节码增强逻辑,使得沙箱的模块能在不违反JDK约束情况下实现对目标应用方法的无侵入
运行时AOP拦截。
从上图中,可以看到一个方法的整个执行周期都被代码“加强”了,能够带来的好处就是你在使用JVM SandBox只需要对于方法的事件进行处理。
// BEFORE
try {
/*
* do something...
*/
// RETURN
return;
} catch (Throwable cause) {
// THROWS
}
在沙箱的世界观中,任何一个Java方法的调用都可以分解为
BEFORE
、RETURN
和THROWS
三个环节,由此在三个环节上引申出对应环节的事件探测和流程控制机制。基于
BEFORE
、RETURN
和THROWS
三个环节事件分离,沙箱的模块可以完成很多类AOP的操作。
- 可以感知和改变方法调用的入参
- 可以感知和改变方法调用返回值和抛出的异常
- 可以改变方法执行的流程
- 在方法体执行之前直接返回自定义结果对象,原有方法代码将不会被执行
- 在方法体返回之前重新构造新的结果对象,甚至可以改变为抛出异常
- 在方法体抛出异常之后重新抛出新的异常,甚至可以改变为正常返回
一切都是事件驱动的,这一点你可能很迷糊,但是在下文的实战环节中,可以帮助你理解。