ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JVM-SANDBOX 开发者指南实战:用沙箱模块修复“损坏的钟“——BEFORE/THROWS 事件流控制与模块全生命周期

JVM-SANDBOX 开发者指南实战:用沙箱模块修复“损坏的钟“——BEFORE/THROWS 事件流控制与模块全生命周期 开发工具后端【免费下载链接】jvm-sandboxReal - 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的两个地方checkState()方法的实现中抛出了一个IllegalStateExceptiondelay()方法中延时了 10 秒——很可能是编写代码的时候不小心多敲了一个 0。传统修复方式需要改代码、重新编译、重启进程而在沙箱的场景下我们要做的是写一个模块通过字节码增强让checkState()不再抛异常、让delay()不再执行那 10 秒的延时且全程不触碰Clock的源码。创建一个 Java 工程clock-tinker假设使用 Maven首先添加沙箱模块的二方库依赖!-- 沙箱模块的API定义二方包 这个二方包可以被声明为provided -- dependency groupIdcom.alibaba.jvm.sandbox/groupId artifactIdsandbox-api/artifactId version1.3.1/version scopeprovided/scope /dependency !-- javax.servlet的三方包 在沙箱模块中需要用到HttpServletReuqest和HttpServletResponse 整个沙箱模块被放置在Servlet容器中完成加载 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.0.1/version scopeprovided/scope /dependency两个依赖都声明为providedsandbox-api只提供模块开发所需的 API 定义过滤器、事件、监听器、流程控制等实际运行时由沙箱容器提供无需打进模块 JARjavax.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注入ModuleEventWatcherModuleEventWatcher事件观察者是整个沙箱最核心的类通过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.javadoClassFilter(...)用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.javathrowReturnImmediately(Object object)第 41-43 行中断当前代码流程并立即返回指定对象对应状态RETURN_IMMEDIATELYthrowThrowsImmediately(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 规范完成注册创建META-INF/services/com.alibaba.jvm.sandbox.api.Module文件往文件内容中写入模块实现类的全限定名com.github.ompc.demo.jvm.sandbox.clocktinker.BrokenClockTinkerModuleSPI 注册并非沙箱的额外要求而是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 groupIdorg.apache.maven.plugins/groupId artifactIdmaven-assembly-plugin/artifactId executions execution goals goalattached/goal /goals phasepackage/phase configuration descriptorRefs descriptorRefjar-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 luanjiataobao.com info ACTIVE LOADED 0 0 0.0.0.1 luanjiataobao.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标注的方法可以接收四类参数MapString,String、MapString,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, total3;-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-sandboxReal - time non-invasive AOP framework container based on JVM项目地址https://gitcode.com/gh_mirrors/jv/jvm-sandbox点击查看免费下载相关推荐JVM-SANDBOX 开发者指南用沙箱模块在运行期修复损坏方法BEFORE / THROWS 事件与流程控制实战JVM SANDBOX 开发者指南用沙箱模块在运行期修复损坏方法BEFORE / THROWS 事件与流程控制实战 本篇技术指南源自 doc/JVM SA开发工具后端Boss直聘时间展示插件招聘信息透明化的技术实现方案Boss直聘时间展示插件招聘信息透明化的技术实现方案 一款基于Chrome扩展技术栈的招聘平台时间信息增强工具采用Vue3 Webpack Type前端PyQuil与QVM集成搭建本地量子虚拟环境的完整步骤PyQuil与QVM集成搭建本地量子虚拟环境的完整步骤 PyQuil是一个用于量子编程的Python库通过与量子虚拟机器QVM集成开发者可以在本地搭建上一篇vscode-icons图标宝库探索600自定义图标资源的终极指南 下一篇如何参与Tetragon开源安全项目完整贡献指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表