☰
JVM-SANDBOX 开发者指南实战:用沙箱模块修复“损坏的钟“——BEFORE/THROWS 事件流控制与模块全生命周期
2026/10/6 1:48:16 网站建设 项目流程
  • 开发工具
  • 后端

【免费下载链接】jvm-sandbox

Real - time non-invasive AOP framework container based on JVM

项目地址:https://gitcode.com/gh_mirrors/jv/jvm-sandbox
点击查看免费下载

本指南基于 JVM-SANDBOX 官方开发者文档(doc/JVM-SANDBOX-DEVELOPER-GUIDE-Chinese.md)展开,以"修复一个损坏的钟"为完整实战案例,带你走通沙箱模块从工程搭建、SPI 注册、编译部署到热加载修复的完整链路。读完你将掌握:如何用ModuleEventWatcher监听并改写目标方法的执行流程、如何通过ProcessControlException实现"立即返回/立即抛出"的流程控制,以及BEFORE、THROWS等事件类型的底层语义,从而在不重启 JVM、不修改业务代码的前提下完成线上故障的即时修复。

从一个"损坏的钟"说起:沙箱模块要解决什么问题

JVM-SANDBOX 是一款基于 JVM 的实时、无侵入的 AOP 容器(Real-time non-invasive AOP framework container based on JVM)。它的核心能力是:在不重启目标进程、不改动目标应用代码的情况下,通过"沙箱模块"动态增强目标类的字节码,从而改变既有方法的执行流程。

开发者指南用一个非常直观的例子来引出这一能力——一个抽象的报时钟:

/** * 报时的钟 */ public abstract class Clock { /** * 状态检查 */ abstract void checkState(); // 日期格式化 private final java.text.SimpleDateFormat clockDateFormat = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); /** * 格式化日期对象为字符串 * * @param date 日期对象 * @return 日期格式化输出 */ final String formatDate(java.util.Date date) { return clockDateFormat.format(date); } /** * 获取当前时间 * * @return 当前时间 */ final java.util.Date nowDate() { return new java.util.Date(); } /** * 报告时间 * * @return 报告时间 */ final String report() { checkState(); return formatDate(nowDate()); } /** * 延时一定的时间 * * @throws InterruptedException 中断 */ abstract void delay() throws InterruptedException; /** * 循环播报时间 */ final void loopReport() throws InterruptedException { while (true) { try { System.out.println(report()); } catch (Throwable cause) { cause.printStackTrace(); } delay(); } } }

这个钟有两个实现类,一个是正常的实现:

/** * 一个正常的钟实现 */ static class NormalClock extends Clock { @Override void checkState() { } @Override void delay() throws InterruptedException { Thread.sleep(1000L); } }

运行起来能每隔一秒进行一次报时:

2017-02-27 14:48:58 2017-02-27 14:48:59 2017-02-27 14:49:00 2017-02-27 14:49:01 2017-02-27 14:49:02

另一个是损坏的钟实现:

/** * 一个损坏的钟实现 */ static class BrokenClock extends Clock { @Override void checkState() { throw new IllegalStateException(); } @Override void delay() throws InterruptedException { Thread.sleep(10000L); } }

运行起来后每隔十秒报时的时候就会报错:

java.lang.IllegalStateException at Clock$BrokenClock.checkState(Clock.java:77) at Clock.report(Clock.java:40) at Clock.loopReport(Clock.java:50) at Clock.main(Clock.java:94) java.lang.IllegalStateException at Clock$BrokenClock.checkState(Clock.java:77) at Clock.report(Clock.java:40) at Clock.loopReport(Clock.java:50) at Clock.main(Clock.java:94) java.lang.IllegalStateException at Clock$BrokenClock.checkState(Clock.java:77) at Clock.report(Clock.java:40) at Clock.loopReport(Clock.java:50) at Clock.main(Clock.java:94) java.lang.IllegalStateException at Clock$BrokenClock.checkState(Clock.java:77) at Clock.report(Clock.java:40) at Clock.loopReport(Clock.java:50) at Clock.main(Clock.java:94)

很明显,正常工作的钟才是我们希望的实现,但目前手头上运行的恰恰是一个损坏的钟。接下来,我们就通过构建一个沙箱模块来修复这个损坏的钟,并借此完整介绍沙箱模块的工作机制。

问题定位:故障出在哪

对照正常实现,问题出在BrokenClock的两个地方:

  1. checkState()方法的实现中抛出了一个IllegalStateException;
  2. delay()方法中延时了 10 秒——很可能是编写代码的时候不小心多敲了一个 0。

传统修复方式需要改代码、重新编译、重启进程;而在沙箱的场景下,我们要做的是:写一个模块,通过字节码增强,让checkState()不再抛异常、让delay()不再执行那 10 秒的延时,且全程不触碰Clock的源码。

创建一个 Java 工程clock-tinker

假设使用 Maven,首先添加沙箱模块的二方库依赖:

<!-- 沙箱模块的API定义二方包 这个二方包可以被声明为provided --> <dependency> <groupId>com.alibaba.jvm.sandbox</groupId> <artifactId>sandbox-api</artifactId> <version>1.3.1</version> <scope>provided</scope> </dependency> <!-- javax.servlet的三方包 在沙箱模块中需要用到HttpServletReuqest和HttpServletResponse 整个沙箱模块被放置在Servlet容器中完成加载 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.0.1</version> <scope>provided</scope> </dependency>

两个依赖都声明为provided:sandbox-api只提供模块开发所需的 API 定义(过滤器、事件、监听器、流程控制等),实际运行时由沙箱容器提供,无需打进模块 JAR;javax.servlet-api则是因模块方法可能接收 Servlet 相关对象而引入,同样由宿主环境提供。这样做可以显著缩小模块 JAR 的体积,也避免类冲突。当前仓库中sandbox-api模块的源码就位于 sandbox-api/src/main/java/com/alibaba/jvm/sandbox/api,模块 API 的完整定义都可以在这里找到。

编写模块代码:修复 checkState()

在clock-tinker工程中编写第一个模块BrokenClockTinkerModule:

/** * 修复损坏的钟模块 */ @Information(id = "broken-clock-tinker") public class BrokenClockTinkerModule implements Module { @Resource private ModuleEventWatcher moduleEventWatcher; @Http("/repairCheckState") public void repairCheckState() { moduleEventWatcher.watch( // 匹配到Clock$BrokenClock#checkState() new NameRegexFilter("Clock\\$BrokenClock", "checkState"), // 监听THROWS事件并且改变原有方法抛出异常为正常返回 new EventListener() { @Override public void onEvent(Event event) throws Throwable { // 立即返回 ProcessControlException.throwReturnImmediately(null); } }, // 指定监听的事件为抛出异常 Event.Type.THROWS ); } }

这段代码虽然只有二十余行,却涵盖了沙箱模块的四个核心要素,逐一拆解如下。

要素一:@Information标注模块身份

@Information(id = "broken-clock-tinker")声明了模块的唯一 ID。从源码看,该注解位于 sandbox-common-api/src/main/java/com/alibaba/jvm/sandbox/api/Information.java,除id()外还支持mode()(模块期待沙箱以AGENT或ATTACH方式加载,默认两者都接受)、isActiveOnLoad()(加载后是否自动激活,默认true)、version()与author()(默认值分别为UNKNOWN_VERSION、UNKNOWN_AUTHOR)。本案例中未声明 version 和 author,稍后你会在沙箱模块列表里看到UNKNOW_VERSION/UNKNOW_AUTHOR的字样,正是这两个默认值在起作用。

要素二:@Resource注入ModuleEventWatcher

ModuleEventWatcher(事件观察者)是整个沙箱最核心的类,通过@Resource注入到模块中。它提供了watch(...)、delete(...)、watching(...)等核心方法,接口定义见 sandbox-api/src/main/java/com/alibaba/jvm/sandbox/api/resource/ModuleEventWatcher.java。其中watch(Filter, EventListener, Event.Type...)返回一个watchId,后续删除观察时也要通过同一个watchId完成;watching(...)则是在观察结束时自动delete并还原被渲染的字节码。Progress内部接口还能在大量类渲染时向外部报告进度(begin/progressOnSuccess/progressOnFailed/finish)。

要素三:NameRegexFilter精确匹配目标方法

new NameRegexFilter("Clock\\$BrokenClock", "checkState")是一个按类名、方法名做正则匹配的过滤器。其实现见 sandbox-api/src/main/java/com/alibaba/jvm/sandbox/api/filter/NameRegexFilter.java:doClassFilter(...)用javaClassName.matches(javaNameRegex)判定类,doMethodFilter(...)用javaMethodName.matches(javaMethodRegex)判定方法。注意内部类在 JVM 中的类名带有$,所以在正则里需要写成Clock\\$BrokenClock转义。

要素四:事件监听与流程控制

EventListener接口只定义了一个方法void onEvent(Event event) throws Throwable,见 sandbox-api/src/main/java/com/alibaba/jvm/sandbox/api/listener/EventListener.java。该接口的 Javadoc 里用一张 ASCII 图完整描述了事件流转流程:BEFORE之后可以走到RETURN或THROWS,而return immediately/throws immediately可以让事件流在任何环节被"截断"。

ProcessControlException就是实现这种"截断"的利器,见 sandbox-api/src/main/java/com/alibaba/jvm/sandbox/api/ProcessControlException.java:

  • throwReturnImmediately(Object object)(第 41-43 行):中断当前代码流程并立即返回指定对象,对应状态RETURN_IMMEDIATELY;
  • throwThrowsImmediately(Throwable throwable)(第 51-53 行):中断当前代码流程并立即抛出指定异常,对应状态THROWS_IMMEDIATELY;
  • 状态枚举还包含NONE_IMMEDIATELY(不干预任何流程,sandbox-api:1.0.16起提供)。

在本案例中,我们监听Event.Type.THROWS(方法抛出异常时触发),当checkState()抛出IllegalStateException的那一刻,监听器捕获到THROWS事件并抛出ProcessControlException.throwReturnImmediately(null),把"抛异常"硬生生改写成"正常返回 null"——这就是修复checkState()的原理。

关于Event.Type枚举,完整定义见 sandbox-api/src/main/java/com/alibaba/jvm/sandbox/api/event/Event.java,包括:BEFORE(方法执行前)、RETURN(方法正常返回)、THROWS(方法抛出异常)、LINE(一行被调用)、CALL_BEFORE/CALL_RETURN/CALL_THROWS(方法内部调用其他方法的三阶段事件,源自 GREYS)、以及由流程控制触发的IMMEDIATELY_RETURN/IMMEDIATELY_THROWS。这些事件类型共同构成了沙箱细粒度观察方法行为的基础。

根据 SPI 规范注册模块

模块写好后,必须按照 JDK 6 的 SPI 规范完成注册:

  1. 创建META-INF/services/com.alibaba.jvm.sandbox.api.Module文件;
  2. 往文件内容中写入模块实现类的全限定名:
com.github.ompc.demo.jvm.sandbox.clocktinker.BrokenClockTinkerModule

SPI 注册并非沙箱的额外要求,而是Module接口的硬性约束。从 sandbox-common-api/src/main/java/com/alibaba/jvm/sandbox/api/Module.java 的源码注释可以看到,沙箱环境模块必须同时满足三点:

  • 必须实现Module接口;
  • 必须拥有无参构造函数(模块加载时会调用默认构造函数完成实例化);
  • 必须在META-INF/services/com.alibaba.jvm.sandbox.api.Module文件中注册。

注意:类完成实例化并不代表模块加载完成,后续还要经历沙箱容器的一系列初始化流程。

编译部署clock-tinker模块

打包:把所有依赖打进一个 JAR

推荐将所有依赖的二方包和三方包都打入一个 JAR 文件,需要在pom.xml中增加maven-assembly-plugin配置:

<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <executions> <execution> <goals> <goal>attached</goal> </goals> <phase>package</phase> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> </execution> </executions> </plugin> </plugins> </build>

然后运行命令完成打包:

mvn clean package

将打好的包复制到用户模块目录下:

cp target/clock-tinker-1.0-SNAPSHOT-jar-with-dependencies.jar ~/.sandbox-module/

启动沙箱并查看模块加载情况

启动沙箱并列出已加载模块:

./sandbox.sh -p 64229 -l module-mgr ACTIVE LOADED 0 0 0.0.0.1 luanjia@taobao.com info ACTIVE LOADED 0 0 0.0.0.1 luanjia@taobao.com broken-clock-tinker ACTIVE LOADED 0 0 UNKNOW_VERSION UNKNOW_AUTHOR

可以看到broken-clock-tinker模块已经正确被沙箱加载。表格中的ACTIVE(激活状态)、LOADED(加载状态)、前两个数字(影响类数/影响方法数,此处为 0 表示尚未产生任何增强)以及UNKNOW_VERSION/UNKNOW_AUTHOR(因为@Information未声明 version 和 author)都是模块运行时信息的直观体现。

激活修复命令:让 checkState() 不再抛异常

执行:

./sandbox.sh -p 64229 -d 'broken-clock-tinker/repairCheckState'

-d参数用于向指定模块的指定方法下发命令,命令格式为模块ID/方法路径。过一会,你会发现原本一直抛异常的钟已经开始刷新时间了:

java.lang.IllegalStateException at Clock$BrokenClock.checkState(Clock.java:89) at Clock.report(Clock.java:40) at Clock.loopReport(Clock.java:57) at Clock.main(Clock.java:111) java.lang.IllegalStateException at Clock$BrokenClock.checkState(Clock.java:89) at Clock.report(Clock.java:40) at Clock.loopReport(Clock.java:57) at Clock.main(Clock.java:111) 2017-02-27 21:34:44 2017-02-27 21:34:54 2017-02-27 21:35:04 2017-02-27 21:35:14 2017-02-27 21:35:24 2017-02-27 21:35:34 2017-02-27 21:35:44 2017-02-27 21:35:54 2017-02-27 21:36:04 2017-02-27 21:36:14

日志开头残留的异常是命令生效前抛出的,之后时间开始正常刷新——checkState()的"抛异常"行为已经被成功改写为"正常返回"。

这里值得说明的是,文档示例中的@Http("/repairCheckState")注解(定义见 sandbox-api/src/main/java/com/alibaba/jvm/sandbox/api/http/Http.java)目前已被标记为@Deprecated,官方推荐改用 sandbox-api/src/main/java/com/alibaba/jvm/sandbox/api/annotation/Command.java 中的@Command注解(自sandbox-api:1.2.0起提供),两者都能接收来自sandbox.sh -d的命令。@Command标注的方法可以接收四类参数:Map<String,String>、Map<String,String[]>、String以及文本输出用的PrintWriter,为模块命令的入参解析和结果回显提供了更规范的支持。开发新模块时建议直接使用@Command。

修复错误的 delay() 方法:用 BEFORE 事件绕过方法体

checkState()修好了,但钟仍是每隔 10 秒才报一次时——delay()方法里的 10 秒延时还没解决。这次我们需要在方法体执行之前立即返回,从而避免方法体的执行导致的 10 秒延时:

@Http("/repairDelay") public void repairDelay() { moduleEventWatcher.watch( // 匹配到Clock$BrokenClock#checkState() new NameRegexFilter("Clock\\$BrokenClock", "delay"), // 监听THROWS事件并且改变原有方法抛出异常为正常返回 new EventListener() { @Override public void onEvent(Event event) throws Throwable { // 在这里延时1s Thread.sleep(1000L); // 然后立即返回,因为监听的是BEFORE事件,所以此时立即返回,方法体将不会被执行 ProcessControlException.throwReturnImmediately(null); } }, // 指定监听的事件为方法执行前 Event.Type.BEFORE ); }

与上一个修复的关键差异在于事件类型:这里监听的是Event.Type.BEFORE(方法执行前)。监听器在BEFORE事件中先Thread.sleep(1000L)完成"延时 1 秒"的业务诉求,然后立即抛出ProcessControlException.throwReturnImmediately(null)。由于BEFORE阶段发生在方法体真正执行之前,一旦立即返回,原方法体(Thread.sleep(10000L))将不会被执行——10 秒的延时就这样被"绕开"了。这正是事件流转图中BEFORE -> return immediately这条路径的实战应用。

模块热部署刷新与最终修复

继续打包、部署,这次替换模块之后,执行模块热部署替换:

./sandbox.sh -p 64229 -f module flush finished, total=3;

-f参数执行模块刷新(flush)。模块刷新的时候首先会冻结原有模块,并清理删除原有的插桩代码——所以刷新完成之后,之前被修好的checkState()方法又"发作"了:

2017-02-27 21:45:54 2017-02-27 21:46:04 2017-02-27 21:46:14 2017-02-27 21:46:24 2017-02-27 21:46:34 java.lang.IllegalStateException at Clock$BrokenClock.checkState(Clock.java:89) at Clock.report(Clock.java:40) at Clock.loopReport(Clock.java:57) at Clock.main(Clock.java:111) java.lang.IllegalStateException at Clock$BrokenClock.checkState(Clock.java:89) at Clock.report(Clock.java:40) at Clock.loopReport(Clock.java:57) at Clock.main(Clock.java:111)

这恰恰印证了沙箱的"可逆"特性:插桩是运行时动态注入的,模块刷新会一并清除,因此修复效果不是"写死"进字节码的,而是完全可控、可回退的。没关系,我们继续完成修复工作。

执行修复命令:

# 修复checkState()方法 ./sandbox.sh -p 64229 -d 'broken-clock-tinker/repairCheckState' # 修复delay()方法 ./sandbox.sh -p 64229 -d 'broken-clock-tinker/repairDelay'

最终问题修复,日志尾部已经出现每秒刷新的时间:

java.lang.IllegalStateException at Clock$BrokenClock.checkState(Clock.java:89) at Clock.report(Clock.java:40) at Clock.loopReport(Clock.java:57) at Clock.main(Clock.java:111) java.lang.IllegalStateException at Clock$BrokenClock.checkState(Clock.java:89) at Clock.report(Clock.java:40) at Clock.loopReport(Clock.java:57) at Clock.main(Clock.java:111) java.lang.IllegalStateException at Clock$BrokenClock.checkState(Clock.java:89) at Clock.report(Clock.java:40) at Clock.loopReport(Clock.java:57) at Clock.main(Clock.java:111) 2017-02-27 21:51:24 2017-02-27 21:51:34 2017-02-27 21:51:35 2017-02-27 21:51:36 2017-02-27 21:51:37 2017-02-27 21:51:38 2017-02-27 21:51:39

一个损坏的钟,在不重启、不改源码的前提下,被沙箱模块彻底修好了。

小结:沙箱模块能做什么

这个教程演示了如何利用沙箱模块改变原有方法的执行流程,其中涉及沙箱最核心的类ModuleEventWatcher——通过@Resource注入即可使用。核心要点回顾:

  • 在BEFORE事件环节改变流程:可以规避掉原有方法体的执行,从而绕开delay()方法中延时 10 秒的问题——方法体根本不会运行;
  • 在THROWS事件环节改变流程:可以让原本应该抛出异常的checkState()方法转变为正常返回——异常被"吞掉"并改写为返回值。

更进一步,沙箱模块的能力远不止修复故障:你还可以窥探、篡改入参、返回值、抛出的异常等等,这些都可以通过沙箱模块实现。如果你觉得裸写Filter门槛较高,仓库中还提供了更友好的EventWatchBuilder(见 sandbox-api/src/main/java/com/alibaba/jvm/sandbox/api/listener/ext/EventWatchBuilder.java),它通过 Builder 模式封装了类匹配(支持includeBootstrap()、includeSubClasses()等选项)与方法匹配,并支持WILDCARD通配模式,能显著降低构造精准观察条件的心智负担;配套的AdviceListener、EventWatchCondition等扩展(sandbox-api/src/main/java/com/alibaba/jvm/sandbox/api/listener/ext)则为方法级 AOP 提供了更高层的抽象。沙箱模块还能帮你实现很多有意思的功能,期待你的想象。

  • 开发工具
  • 后端

【免费下载链接】jvm-sandbox

Real - time non-invasive AOP framework container based on JVM

项目地址:https://gitcode.com/gh_mirrors/jv/jvm-sandbox
点击查看免费下载

相关推荐

上一篇:vscode-icons图标宝库:探索600+自定义图标资源的终极指南 🎨
下一篇:如何参与Tetragon开源安全项目:完整贡献指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询