浅谈阿里开源JVM Sandbox(内含代码实战)
举几个典型的JVM-Sandbox应用场景:
流量回放:如何录制线上应用每次接口请求的入参和出参?改动应用代码固然可以,但成本太大,通过JVM-Sandbox,可以直接在不修改代码的情况下,直接抓取接口的出入参。
安全漏洞热修复:假设某个三方包(例如出名的fastjson)又出现了漏洞,集团内那么多应用,一个个发布新版本修复,漏洞已经造成了大量破坏。通过JVM-Sandbox,直接修改替换有漏洞的代码,及时止损。
接口故障模拟:想要模拟某个接口超时5s后返回false的情况,JVM-Sandbox很轻松就能实现。
故障定位:像Arthas类似的功能。
接口限流:动态对指定的接口做限流。
日志打印
...
本文围绕JVM SandBox展开,主要介绍如下内容:
JVM SandBox的诞生背景
JVM SandBox的架构设计
代码实战:Spring Bean初始化耗时统计
JVM SandBox的底层技术
总结
JVM Sandbox诞生背景
JVM SandBox 是阿里开源的一款 JVM 平台非侵入式运行期 AOP 解决方案,本质上是一种 AOP 落地形式。那么可能有同学会问:已有成熟的 Spring AOP 解决方案,阿里巴巴为什么还要“重复造轮子”?这个问题要回到 JVM SandBox 诞生的背景中来回答。在 2016 年中,天猫双十一催动了阿里巴巴内部大量业务系统的改动,恰逢徐冬晨(阿里巴巴测试开发专家)所在的团队调整,测试资源保障严重不足,迫使他们必须考虑更精准、更便捷的老业务测试回归验证方案。开发团队面临的是新接手的老系统,老的业务代码架构难以满足可测性的要求,很多现有测试框架也无法应用到老的业务系统架构中,于是需要新的测试思路和测试框架。
JVM Sandbox整体架构
类隔离
很多框架通过破坏双亲委派(我更愿意称之为直系亲属委派)来实现类隔离,JVM SandBox也不例外。它通过自定义的SandboxClassLoader破坏了双亲委派的约定,实现了几个隔离特性:
和目标应用的类隔离:不用担心加载沙箱会引起原应用的类污染、冲突。
模块之间类隔离:做到模块与模块之间、模块和沙箱之间、模块和应用之间互不干扰。
无侵入AOP与事件驱动
// BEFORE
try {
/*
* do something...
*/
// RETURN
return;
} catch (Throwable cause) {
// THROWS
}
在沙箱的世界观中,任何一个Java方法的调用都可以分解为BEFORE、RETURN和THROWS三个环节,由此在三个环节上引申出对应环节的事件探测和流程控制机制。
基于BEFORE、RETURN和THROWS三个环节事件分离,沙箱的模块可以完成很多类AOP的操作。
可以感知和改变方法调用的入参
可以感知和改变方法调用返回值和抛出的异常
可以改变方法执行的流程
在方法体执行之前直接返回自定义结果对象,原有方法代码将不会被执行
在方法体返回之前重新构造新的结果对象,甚至可以改变为抛出异常
在方法体抛出异常之后重新抛出新的异常,甚至可以改变为正常返回 (引用自官方文档)
一切都是事件驱动的,这一点你可能很迷糊,但是别担心,请继续往下阅读,在下文的实战环节中,可以帮助你理解事件驱动的含义。
代码实战
<parent>
<groupId>com.alibaba.jvm.sandbox</groupId>
<artifactId>sandbox-module-starter</artifactId>
<version>1.2.0</version>
</parent>
图4 主入口代码实现
Agent:随着JVM启动一起启动
Attach:在已经运行的JVM进程中,动态的插入
其中ModuleLifecycle包含了整个模块的生命周期回调函数。
onLoad:模块加载,模块开始加载之前调用!模块加载是模块生命周期的开始,在模块生命中期中有且只会调用一次。这里抛出异常将会是阻止模块被加载的唯一方式,如果模块判定加载失败,将会释放掉所有预申请的资源,模块也不会被沙箱所感知。
onUnload:模块卸载,模块开始卸载之前调用!模块卸载是模块生命周期的结束,在模块生命中期中有且只会调用一次。这里抛出异常将会是阻止模块被卸载的唯一方式,如果模块判定卸载失败,将不会造成任何资源的提前关闭与释放,模块将能继续正常工作。
onActive:模块被激活后,模块所增强的类将会被激活,所有com.alibaba.jvm.sandbox.api.listener.EventListener将开始收到对应的事件。
onFrozen:模块被冻结后,模块所持有的所有com.alibaba.jvm.sandbox.api.listener.EventListener将被静默,无法收到对应的事件。需要注意的是,模块冻结后虽然不再收到相关事件,但沙箱给对应类织入的增强代码仍然还在。
loadCompleted:模块加载完成,模块完成加载后调用!模块完成加载是在模块完成所有资源加载、分配之后的回调,在模块生命中期中有且只会调用一次。这里抛出异常不会影响模块被加载成功的结果。模块加载完成之后,所有的基于模块的操作都可以在这个回调中进行。
图5 过滤器代码实现
图6 initializeBean入口源码注释
图7 监听逻辑代码实现
图8 工具生成的SpringBean耗时报表
JVM Sandbox底层技术
JVMTI
JavaAgent和Instrumentation
-agentlib:<libname>[=<选项>] # 加载本机代理库 <libname>, 例如 -agentlib:hprof,另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<选项>] # 按完整路径名加载本机代理库
-javaagent:<jarpath>[=<选项>] # 加载 Java 编程语言代理, 请参阅 java.lang.instrument
public interface Instrumentation {
//添加一个ClassFileTransformer
//之后类加载时都会经过这个ClassFileTransformer转换
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void addTransformer(ClassFileTransformer transformer);
//移除ClassFileTransformer
boolean removeTransformer(ClassFileTransformer transformer);
boolean isRetransformClassesSupported();
//将一些已经加载过的类重新拿出来经过注册好的ClassFileTransformer转换
//retransformation可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
boolean isRedefineClassesSupported();
//重新定义某个类
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException;
boolean isModifiableClass(Class<?> theClass);
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);
long getObjectSize(Object objectToSize);
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
void appendToSystemClassLoaderSearch(JarFile jarfile);
boolean isNativeMethodPrefixSupported();
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}
不能通过字节码文件和自定义的类名重新定义一个本来不存在的类;
增强类和老类必须遵循很多限制:比如新类和老类的父类必须相同;新类和老类实现的接口数也要相同,并且是相同的接口;新类和老类访问符必须一致。新类和老类字段数和字段名要一致;新类和老类新增或删除的方法必须是private static/final修饰的;
再谈Attach和Agent
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
java -javaagent:agent1.jar -javaagent:agent2.jar -jar MyProgram.jar
public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)
这两种方式各有不同用途,一般来说,Attach方式适合于动态的对代码进行功能修改,在排查问题的时候用的比较多。而Agent模式随着应用启动,所以经常用于提前实现一些增强功能,比如我上面实战中的启动观测,应用防火墙,限流策略等等。
总结
参考:
[1] JVM SandBox 的技术原理与应用分析
https://www.infoq.cn/article/tsy4lgjvsfweuxebw*gp
[2] Github jvm-sandbox
https://github.com/alibaba/jvm-sandbox/wiki
[3] javaagent使用指南
https://www.cnblogs.com/rickiyang/p/11368932.html
[4] Java JVMTI和Instrumention机制介绍
https://www.jianshu.com/p/eff047d4480a
“乘风者计划”全新升级
2021年开发者社区推出乘风者计划,邀请广大创作者泼墨云间,书写天地。新年新气象,发文三篇即可申请入驻,领取好礼享受丰厚权益!
点击阅读原文查看详情。