JVMSandbox教程及原理介绍在日常的业务代码开发中,我们经常会接触到AOP,比如大家熟知的SpringAOP。我们将其用于业务方面,例如登录验证、日志记录、性能监控、全局过滤器等。但是SpringAOP有一个局限性。并非所有类都托管在Spring容器中。比如很多中间件代码,三方包代码,Java原生代码,SpringAOP是无法代理的。这样一来,一旦你要做的切面逻辑不属于Spring的管辖范围,或者你想实现不受Spring限制的切面功能,就无法实现了。那么对于Java后端应用,有没有更通用的AOP方式呢?答案是肯定的,Java本身提供了JVMTI、Instrumentation等功能,让用户通过一系列的API来完成对JVM的复杂控制。此后又衍生出许多著名的框架,如Btrace、Arthas等,帮助开发者实现越来越复杂的Java功能。JVMSandbox也是其中之一。当然,不同框架的设计目的和使命是不同的。JVM-Sandbox的设计目的是在不重启或不侵入目标JVM应用程序的情况下实现一个AOP解决方案。你看到这个还是不知道我在说什么?别着急,我举几个典型的JVM-Sandbox应用场景:流量回放:如何记录在线应用的每个接口请求的输入输出参数?更改应用代码当然是可以的,但是成本太高了。通过JVM-Sandbox,可以直接抓取接口的输入输出参数,无需修改代码。安全漏洞热修复:假设某个第三方包(比如大名鼎鼎的fastjson)又出现了漏洞,于是群里很多应用纷纷发布新版本一一修复,漏洞造成了不小的危害。通过JVM-Sandbox,直接修改替换易受攻击的代码,及时止损。接口故障模拟:JVM-Sandbox可以轻松模拟接口超时5s后返回false的情况。故障定位:类似Arthas的功能。接口限流:动态限制指定接口的电流。日志打印……可见,借助JVM-Sandbox,可以实现很多以前业务代码做不到的事情,大大扩展了操作范围。本文围绕JVMSandBox展开,主要介绍以下内容:JVMSandBox诞生背景JVMSandBox架构设计JVMSandBox代码实战JVMSandBox底层技术总结与展望开发该框架的一些业务背景,以下描述引自文章:JVMSandBox是阿里开源的针对JVM平台的非侵入式运行时AOP解决方案,本质上是AOP的一种实现形式。那么可能有同学会问:为什么在已经有成熟的SpringAOP解决方案的情况下,阿里巴巴还要“重新发明轮子”呢?这个问题应该在JVMSandBox诞生的背景下得到解答。2016年年中,天猫双十一引发了阿里巴巴内部业务系统的大量变革。这恰逢徐东辰(阿里巴巴测试开发专家)团队的调整。测试资源保障严重不足,迫使他们考虑更精准、更便捷的老业务测试回归验证方案。开发团队面对的是一个新接手的旧系统。旧的业务代码架构难以满足可测试性要求,很多现有的测试框架无法适用于旧的业务系统架构,需要新的测试思路和测试框架。为什么不使用SpringAOP解决方案?SpringAOP方案的痛点在于,并不是所有的业务代码都托管在Spring容器中,底层的中间件代码和三方包代码无法纳入回归测试范围。更糟糕的是,测试框架会引入它所依赖的类库。经常和业务代码的类库冲突,于是JVMSandBox应运而生。JVMSandbox的整体架构本章并没有详细描述JVMSandbox的所有架构设计,只是介绍了其中最重要的几个特性。详细的架构设计可以参考原框架代码仓库的wiki。类隔离许多框架通过打破双亲委派(我更喜欢称之为直系亲属委派)来实现类隔离,SandBox也不例外。它通过自定义的SandboxClassLoader打破了双亲委托协议,实现了几个隔离特性:与目标应用的类隔离:不用担心加载沙箱会造成类污染和原应用的冲突。模块之间的类隔离:模块之间、模块与沙箱之间、模块与应用程序之间互不干扰。非侵入式AOP和事件驱动JVM-SANDBOX属于基于插桩的动态编织AOP框架。通过精心构建字节码增强逻辑,沙箱模块可以在不违反JDK约束的情况下,实现对目标应用方法的无限制访问。侵入式运行时AOP拦截。从上图可以看出,一个方法的整个执行周期都被代码“强化”了。好处是在使用JVMSandBox时只需要处理方法的事件即可。//BEFOREtry{/**dosomething...*///RETURNreturn;}catch(Throwablecause){//THROWS}在沙箱的世界观中,任何Java方法调用都可以分解为BEFORE、RETURN和THROWS三个环节,从而相应环节的事件检测和流程控制机制都是从这三个环节派生出来的。基于BEFORE、RETURN、THROWS事件的分离,沙盒模块可以完成多种类型的AOP操作。可以感知和改变方法调用的入参可以感知和改变方法调用的返回值和抛出的异常可以改变方法执行的流程在方法体执行前直接返回自定义结果对象,原方法代码将不会在方法体返回前重新构造一个新的结果对象,甚至可以改为抛出异常,在方法体抛出异常后重新抛出新的异常。甚至可以改成正常返回。一切都是事件驱动的,你可能会很困惑,但在下面的实战环节中,可以帮助你理解。JVMSandbox代码实战我把实战章节提前放在这里,目的是方便大家快速了解使用JVMSandBox开发是多么的舒服(相比于使用字节码替换等工具)。使用版本:JVM-Sandbox1.2.0官方源码:https://github.com/alibaba/jv...下面来实现一个小工具。在日常工作中,总会遇到一些庞大的Spring项目。bean和业务代码非常多,启动一个项目可能需要5分钟甚至更长时间,严重阻碍了开发效率。我们尝试使用JVMSandbox开发了一个工具来统计应用程序的SpringBean的启动时间。这样一来就可以一目了然的找到项目启动慢的主要原因,避免盲人优化。最终效果如图:图中展示了一个应用程序启动到所有SpringBeans启动的耗时,从高到低排序。因为是demo应用,所以Bean的耗时较低(业务bean也不多),但是在实际应用中,会有很多bean在几秒甚至十几秒就被初始化了,这可以有针对性地进行优化。如何在JVMSandBox中实现以上工具?其实很简单。先贴出思路的整体流程:首先创建一个Maven项目,在Maven依赖中引用JVMSandBox,官方推荐独立项目使用parent方法。com.alibaba.jvm.sandboxsandbox-module-starter1.2.0创建一个新类作为JVMSandBoxModule,如下图所示:使用@Infomation声明模式为AGENT模式。有两种模式,代理和附加。Agent:从JVM启动开始Attach:在已经运行的JVM进程中,动态插入因为我们监控JVM启动数据,所以需要AGENT方式。二、继承com.alibaba.jvm.sandbox.api.Module和com.alibaba.jvm.sandbox.api.ModuleLifecycle。其中,ModuleLifecycle包含了整个模块的生命周期回调函数。onLoad:模块加载,在模块开始加载之前调用!模块加载是模块生命周期的开始,在模块的生命周期内只会被调用一次。在这里抛出异常将是阻止加载模块的唯一方法。如果模块判断加载失败,会释放所有预申请的资源,模块不会被沙箱感知。onUnload:模块卸载,在模块开始卸载之前调用!模块卸载是模块生命周期的结束,在模块生命周期内只会调用一次。在这里抛出异常将是防止模块被卸载的唯一方法。如果模块判断卸载失败,不会导致任何资源提前关闭释放,模块继续正常工作。onActive:模块激活后,模块enhanced类会被激活,所有com.alibaba.jvm.sandbox.api.listener.EventListener都会开始接收相应的事件onFrozen:模块冻结后,所有com.模块持有的alibaba.jvm.sandbox.api。listener.EventListener会被静音,无法接收到相应的事件。需要注意的是,虽然模块冻结后不再接收到相关事件,但是沙箱编织到相应类中的增强代码仍然存在。loadCompleted:模块加载完成,模块加载完成后调用!模块加载完成是模块完成所有资源加载和分配后的回调,在模块的生命周期内只会调用一次。这里抛出异常不会影响模块加载成功的结果。加载模块后,所有基于模块的操作都可以在这个回调中执行。最常用的是loadCompleted,所以我们重写loadCompleted类,在里面启动我们的监控类SpringBeanStartMonitor线程。SpringBeanStartMonitor的核心代码如下:利用Sandbox的doClassFilter过滤出匹配的类,我们这里是BeanFactory。使用doMethodFilter过滤出要监听的方法,这里是initializeBean。以initializeBean作为统计耗时的入口方法。具体选择这种方式的原因涉及到SpringBean的启动生命周期,不在本文讨论范围之内。(本文作者:满三道江)然后使用moduleEventWatcher.watch(springBeanFilter,springBeanInitListener,Event.Type.BEFORE,Event.Type.RETURN);将我们的springBeanInitListener侦听器绑定到观察到的方法。这样每次调用initializeBean的时候,都会去到我们的监听逻辑。监听器的主要逻辑如下:代码有点长,不用细看。主要是在原方法的BeforeEvent(进入前)和ReturnEvent(正常返回后)执行上述切面逻辑。这里我用一个MAP来存储每个Bean的初始化开始和结束时间,最后统计初始化时间。最后,我们还需要一种方式来知道我们原来的Spring应用程序已经启动了,这样我们就可以手动卸载我们的Sandbox模块了。毕竟他已经完成了自己的历史使命,不需要依附于主进程。我们通过一个简单的方法,通过查看http://127.0.0.1:8080/是否会返回一个小于500的状态码来判断Spring容器是否已经启动。当然,如果你的Spring没有使用web框架,就不能用这种方法来判断启动完成。你或许可以通过Spring自带的生命周期钩子函数来实现。我在这里很懒惰。整个SpringBean监控模块开发完成。你能感觉到你的开发和日常的业务开发几乎没有区别。这是JVMSandbox给你带来的最大好处。以上源码放在我的Github仓库:https://github.com/monitor4al...JVMSandbox底层技术整个JVMSandbox的使用介绍基本讲完了。上面提到了一些JVM技术术语,可能小伙伴们听说过但不太了解。这里对几个重要的概念进行简单的解释,并厘清这些概念之间的关系,以便大家更好的理解JVMSandbox的底层实现。JVMTIJVMTI(JVMToolInterface)是Java虚拟机提供的原生编程接口。JVMTI可用于开发和监控虚拟机,查看JVM内部状态,控制JVM应用程序的执行。可以实现的功能包括但不限于:调试、监控、线程分析、覆盖率分析工具等。很多java监控诊断工具都是基于这种形式工作的。如arthas、jinfo、brace等,这些工具的底层虽然是JVMTI,但是也使用了上层工具JavaAgent。JavaAgent和InstrumentationJavaagent是java命令的一个参数。参数javaagent可以用来指定一个jar包。-agentlib:[=]加载本机代理库,例如-agentlib:hprof另请参阅-agentlib:jdwp=help和-agentlib:hprof=help-agentpath:[=]loadnativeagentlibrarybyfullpathname-javaagent:[=]加载Java编程语言agent,见上面提到的java.lang.instrument-javaagent参数见java.lang.instrument,是一个包定义在rt.jar中,它提供了一些工具帮助开发者在Java程序运行时动态修改系统中的Class类型。使用此包的关键组件之一是Javaagent。看名字好像是Java代理什么的,其实它的功能更像是一个Class类型转换器,可以在运行时接受外部新的请求,修改Class类型。Instrumentation的底层实现依赖于JVMTI。JVM会优先加载带有Instrumentation签名的方法。如果加载成功,第二种方法将被忽略。如果第一个方法不可用,将加载第二个方法。Instrumentation支持的接口:publicinterfaceInstrumentation{//添加一个ClassFileTransformer//类加载后,会通过这个ClassFileTransformer进行转换voidaddTransformer(ClassFileTransformertransformer,booleancanRetransform);voidaddTransformer(ClassFileTransformer转换器);//移除ClassFileTransformerbooleanremoveTransformer(ClassFileTransformertransformer);布尔isRetransformClassesSupported();//重取一些加载的类,经过注册的ClassFileTransformer转换//重转换可以修改方法体,但不能改变方法签名,增删方法/类成员属性voidretransformClasses(Class>...classes)throws不可修改类异常;布尔isRedefineClassesSupported();//重新定义一个类voidredefineClasses(ClassDefinition...definitions)throwsClassNotFoundException,UnmodifiableClassException;booleanisModifiableClass(Class>theClass);@SuppressWarnings("rawtypes")Class[]getAllLoadedClasses();@SuppressWarnings("rawtypes")Class[]getInitiatedClasses(类加载器加载器);长getObjectSize(对象objectToSize);voidappendToBootstrapClassLoaderSearch(JarFilejarfile);voidappendToSystemClassLoaderSearch(JarFilejarfile);布尔isNativeMethodPrefixSupported();voidsetNativeMethodPrefix(ClassFileTransformertransformer,Stringprefix);}Instrumentation的局限性:不能通过字节码文件和自定义类名重新定义一个不存在的类。增强类和老类必须遵循很多限制:比如新类和老类的父类必须相同;新类和旧类实现的接口数量也必须相同。一定是一样的,一定是同一个接口;新类和旧类的访问器必须一致;新类和旧类的编号和字段名必须一致;新类和旧类中添加或删除的方法必须是privatestatic/final修饰的;更详细的原理解释见以下:https://www.cnblogs.com/ricki...说说Attach和Agentattach和agent的区别在上面的实战章节已经讲过了,就在这里说说吧。在Instrumentation中,Agent模式从应用程序启动时通过-javaagent:[=