首页IT科技完整java代码(如何把Java代码玩出花?JVM Sandbox入门教程与原理浅谈)

完整java代码(如何把Java代码玩出花?JVM Sandbox入门教程与原理浅谈)

时间2025-09-19 12:20:58分类IT科技浏览6219
导读:在日常业务代码开发中,我们经常接触到AOP,比如熟知的Spring AOP。我们用它来做业务切面,比如登录校验,日志记录,性能监控,全局过滤器等。但Spring AOP有一个局限性,...

在日常业务代码开发中                 ,我们经常接触到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的操作。

可以感知和改变方法调用的入参 可以感知和改变方法调用返回值和抛出的异常 可以改变方法执行的流程 在方法体执行之前直接返回自定义结果对象,原有方法代码将不会被执行 在方法体返回之前重新构造新的结果对象                 ,甚至可以改变为抛出异常 在方法体抛出异常之后重新抛出新的异常                           ,甚至可以改变为正常返回

一切都是事件驱动的         ,这一点你可能很迷糊                 ,但是在下文的实战环节中                          ,可以帮助你理解                          。

JVM Sandbox代码实战

我将实战章节提前到这里         ,目的是方便大家快速了解使用JVM SandBox开发是一件多么舒服的事情(相比于自己使用字节码替换等工具)                           。

使用版本:JVM-Sandbox 1.2.0

官方源码:https://github.com/alibaba/jvm-sandbox

我们来实现一个小工具         ,在日常工作中                          ,我们总会遇到一些巨大的Spring工程                  ,里面有茫茫多的Bean和业务代码         ,启动一个工程可能需要5分钟甚至更久                          ,严重拖累开发效率。

我们尝试使用JVM Sandbox来开发一个工具                  ,对应用的Spring Bean启动耗时进行一次统计                 。这样能一目了然的发现工程启动慢的主要原因,避免去盲人摸象的优化                           。

最终效果如图:

图中统计了一个应用从启动开始到所有SpringBean的启动耗时                          ,按照从高到低排序                           ,我由于是demo应用,Bean的耗时都偏低(也没有太多业务Bean)                 ,但在实际应用中会有非常多几秒甚至十几秒才完成初始化的Bean                           ,可以进行针对性优化         。

在JVMSandBox中如何实现上面的工具?其实非常简单                 。

先贴上思路的整体流程:

首先新建Maven工程         ,在Maven依赖中引用JVM SandBox                 ,官方推荐独立工程使用parent方式                          。

<parent> <groupId>com.alibaba.jvm.sandbox</groupId> <artifactId>sandbox-module-starter</artifactId> <version>1.2.0</version> </parent>

新建一个类作为一个JVM SandBox模块                          ,如下图:

使用@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的核心代码如下图:

使用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监听模块的开发就完成了                 ,你可以感受到                           ,你的开发和日常业务开发几乎没有区别         ,这就是JVM Sandbox带给你的最大好处                          。

上述源码放在了我的Github仓库:

https://github.com/monitor4all/javaMonitor

JVM Sandbox底层技术

整个JVM Sandbox的入门使用基本上讲完了                 ,上文提到了一些JVM技术名词                          ,可能小伙伴们听过但不是特别了解                  。这里简单阐述几个重要的概念         ,理清楚这几个概念之间的关系         ,以便大家更好的理解JVM Sandbox底层的实现。

JVMTI

JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 native 编程接口                          ,JVMTI可以用来开发并监控虚拟机                  ,可以查看JVM内部的状态         ,并控制JVM应用程序的执行                          。可实现的功能包括但不限于:调试                           、监控                          、线程分析、覆盖率分析工具等                           。

很多java监控                  、诊断工具都是基于这种形式来工作的。如果arthas                          、jinfo         、brace等                          ,虽然这些工具底层是JVM TI                  ,但是它们还使用到了上层工具JavaAgent                 。

JavaAgent和Instrumentation

Javaagent是java命令的一个参数                           。参数 javaagent 可以用于指定一个 jar 包         。

-agentlib:<libname>[=<选项>] 加载本机代理库 <libname>, 例如 -agentlib:hprof 另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help -agentpath:<pathname>[=<选项>] 按完整路径名加载本机代理库 -javaagent:<jarpath>[=<选项>] 加载 Java 编程语言代理, 请参阅 java.lang.instrument

在上面-javaagent参数中提到了参阅java.lang.instrument,这是在rt.jar 中定义的一个包                          ,该包提供了一些工具帮助开发人员在 Java 程序运行时                           ,动态修改系统中的 Class 类型                 。其中,使用该软件包的一个关键组件就是 Javaagent                          。从名字上看                 ,似乎是个 Java 代理之类的                           ,而实际上         ,他的功能更像是一个Class 类型的转换器                 ,他可以在运行时接受重新外部请求                          ,对Class类型进行修改         。

Instrumentation的底层实现依赖于JVMTI         。

JVM 会优先加载 带 Instrumentation 签名的方法         ,加载成功忽略第二种         ,如果第一种没有                          ,则加载第二种方法                          。

Instrumentation支持的接口:

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); }

Instrumentation的局限性:

不能通过字节码文件和自定义的类名重新定义一个本来不存在的类 增强类和老类必须遵循很多限制:比如新类和老类的父类必须相同;新类和老类实现的接口数也要相同         ,并且是相同的接口;新类和老类访问符必须一致                  。 新类和老类字段数和字段名要一致;新类和老类新增或删除的方法必须是private static/final修饰的;

更详细的原理阐述可以看下文:

https://www.cnblogs.com/rickiyang/p/11368932.html

再谈Attach和Agent

上面的实战章节中已经提到了attach和agent两者的区别                          ,这里再展开聊聊         。

在Instrumentation中                  ,Agent模式是通过-javaagent:<jarpath>[=<选项>]从应用启动时候就插桩,随着应用一起启动                          。它要求指定的类中必须要有premain()方法                          ,并且对premain方法的签名也有要求                           ,签名必须满足以下两种格式:

public static void premain(String agentArgs, Instrumentation inst) public static void premain(String agentArgs)

一个java程序中-javaagent参数的个数是没有限制的,所以可以添加任意多个javaagent                  。所有的java agent会按照你定义的顺序执行                 ,例如:

java -javaagent:agent1.jar -javaagent:agent2.jar -jar MyProgram.jar

上面介绍Agent模式的Instrumentation是在 JDK 1.5中提供的                           ,在1.6中         ,提供了attach方式的Instrumentation                 ,你需要的是agentmain方法                          ,并且签名如下:

public static void agentmain (String agentArgs, Instrumentation inst) public static void agentmain (String agentArgs)

这两种方式各有不同用途         ,一般来说         ,Attach方式适合于动态的对代码进行功能修改                          ,在排查问题的时候用的比较多。而Agent模式随着应用启动                  ,所以经常用于提前实现一些增强功能         ,比如我上面实战中的启动观测                          ,应用防火墙                  ,限流策略等等                          。

总结

本文花了较短的篇幅重点介绍了JVM Sandbox的功能,实际用法                          ,以及基础原理                           。它通过封装一些底层JVM控制的框架                           ,使得对JVM层面的AOP开发变的异常简单,就像作者自己所说“JVM-SANDBOX还能帮助你做很多很多                 ,取决于你的脑洞有多大了。         ”

笔者在公司内部也通过它实现了很多小工具                           ,比如上面的应用启动数据观测(公司内部是一个更为稳定复杂的版本         ,还监控了大量中间件的数据)                 ,帮助了很多部门同事                          ,优化他们应用的启动速度                 。所以如果对JVM感兴趣         ,不妨大开脑洞         ,想一想JVM Sandbox还能在哪里帮助到你的工作                          ,给自己的工作添彩                           。

参考

https://www.infoq.cn/article/tsy4lgjvsfweuxebw*gp

https://www.cnblogs.com/rickiyang/p/11368932.html

https://www.jianshu.com/p/eff047d4480a

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
苹果6splus手机保护壳(教你如何正确选购iPhone6s/6s Plus保护壳)