题记:大名鼎鼎的ChaosBlade(混沌工具)和 jvm-sandbox-repeater(无侵入录制回放工具)底层都是基于JVM SandBox。JVM SandBox 使用起来非常很简单,但是 JVM SandBox 背后所涉及到的底层技术原理、实现细节却不简单,比如 Java Agent、Attach、JVMTI、Instrument、Class 字节码修改、ClassLoader、代码锁、事件驱动设计等等。学无止境。
下面是JVM SandBox官网修复时钟demo。举一反三,如何注入异常?
写一个正常的时钟,下述demo中,checkState() 去掉异常,直接return,运行就可以看到一个正常时钟。使用下述代码即可注入异常。步骤不再赘述,参考下方所附修复demo。
package com.alibaba.jvm.sandbox.demo;import com.alibaba.jvm.sandbox.api.Information;import com.alibaba.jvm.sandbox.api.Module;import com.alibaba.jvm.sandbox.api.ProcessController;import com.alibaba.jvm.sandbox.api.annotation.Command;import com.alibaba.jvm.sandbox.api.listener.ext.Advice;import com.alibaba.jvm.sandbox.api.listener.ext.AdviceListener;import com.alibaba.jvm.sandbox.api.listener.ext.EventWatchBuilder;import com.alibaba.jvm.sandbox.api.resource.ModuleEventWatcher;import org.kohsuke.MetaInfServices;import javax.annotation.Resource;@MetaInfServices(Module.class)@Information(id = "normal-clock-tinker")public class NormalClockTinkerModule implements Module { @Resource private ModuleEventWatcher moduleEventWatcher; @Command("injectCheckState") public void injectCheckState() { new EventWatchBuilder(moduleEventWatcher) .onClass("com.taobao.demo.NormalClock") .onBehavior("checkState") .onWatch(new AdviceListener() { /** * 对{@code com.taobao.demo.NormalClock#checkState()}方法注入异常 */ @Override protected void afterReturning(Advice advice) throws Throwable { // 在此,你可以通过ProcessController来改变原有方法的执行流程 // 这里的代码意义是:改变原方法抛出异常的行为,变更为立即返回;void返回值用null表示 ProcessController.throwsImmediately(new IllegalStateException("STATE ERROR!")); } }); }}
注入异常效果
下面是官网修复时钟的摘抄:
我们定义了一个钟,期望可以实现每隔一定的时间进行报时。
package com.taobao.demo;/** * 报时的钟 */public class Clock {// 日期格式化private final java.text.SimpleDateFormat clockDateFormat= new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");/** * 状态检查 */final void checkState() {throw new IllegalStateException("STATE ERROR!");
}/** * 获取当前时间 * * @return 当前时间 */final java.util.Date now() {return new java.util.Date();
}/** * 报告时间 * * @return 报告时间 */final String report() {
checkState();return clockDateFormat.format(now());
}/** * 循环播报时间 */final void loopReport() throws InterruptedException {while (true) {try {System.out.println(report());
} catch (Throwable cause) {
cause.printStackTrace();
}Thread.sleep(1000);
}
}public static void main(String... args) throws InterruptedException {new Clock().loopReport();
}
}
很明显,这个钟的实现有问题,运行起来没有正确的报时,但却一直在报异常!
java.lang.IllegalStateException: STATE ERROR!
at com.taobao.demo.Clock.checkState(Clock.java:16)
at com.taobao.demo.Clock.report(Clock.java:34)
at com.taobao.demo.Clock.loopReport(Clock.java:44)
at com.taobao.demo.Clock.main(Clock.java:53)
java.lang.IllegalStateException: STATE ERROR!
at com.taobao.demo.Clock.checkState(Clock.java:16)
at com.taobao.demo.Clock.report(Clock.java:34)
at com.taobao.demo.Clock.loopReport(Clock.java:44)
at com.taobao.demo.Clock.main(Clock.java:53)
java.lang.IllegalStateException: STATE ERROR!
at com.taobao.demo.Clock.checkState(Clock.java:16)
at com.taobao.demo.Clock.report(Clock.java:34)
at com.taobao.demo.Clock.loopReport(Clock.java:44)
at com.taobao.demo.Clock.main(Clock.java:53)
问题出在了checkState()方法上,这个方法中抛出了异常。接下来我们通过构建一个沙箱模块作为例子,修复这个损坏的钟!
如果能直接修改checkState()方法的执行逻辑,让其不再抛异常,而是直接返回,那么一切就迎刃而解。但如何在不修改目标代码、不重启目标应用的情况下实现这个功能呢?
创建一个Java工程clock-tinker
假设用的是MAVEN,这里通过将parent指向sandbox-module-starter来简化我们的配置工作
com.alibaba.jvm.sandboxgroupId>
sandbox-module-starterartifactId>
1.2.0version>parent>
package com.alibaba.jvm.sandbox.demo;import com.alibaba.jvm.sandbox.api.Information;import com.alibaba.jvm.sandbox.api.Module;import com.alibaba.jvm.sandbox.api.ProcessController;import com.alibaba.jvm.sandbox.api.annotation.Command;import com.alibaba.jvm.sandbox.api.listener.ext.Advice;import com.alibaba.jvm.sandbox.api.listener.ext.AdviceListener;import com.alibaba.jvm.sandbox.api.listener.ext.EventWatchBuilder;import com.alibaba.jvm.sandbox.api.resource.ModuleEventWatcher;import org.kohsuke.MetaInfServices;import javax.annotation.Resource;@MetaInfServices(Module.class)@Information(id = "broken-clock-tinker")public class BrokenClockTinkerModule implements Module {@Resourceprivate ModuleEventWatcher moduleEventWatcher;@Command("repairCheckState")public void repairCheckState() {new EventWatchBuilder(moduleEventWatcher)
.onClass("com.taobao.demo.Clock")
.onBehavior("checkState")
.onWatch(new AdviceListener() {/** * 拦截{@code com.taobao.demo.Clock#checkState()}方法,当这个方法抛出异常时将会被 * AdviceListener#afterThrowing()所拦截 */@Overrideprotected void afterThrowing(Advice advice) throws Throwable {// 在此,你可以通过ProcessController来改变原有方法的执行流程// 这里的代码意义是:改变原方法抛出异常的行为,变更为立即返回;void返回值用null表示ProcessController.returnImmediately(null);
}
});
}
}
clock-tinker
模块运行命令完成打包
mvn clean package
将打好的包复制到用户模块目录下
cp target/clock-tinker-1.0-SNAPSHOT-jar-with-dependencies.jar ~/.sandbox-module/
下载并安装最新版本沙箱:
下载地址:https://ompc.oss.aliyuncs.com/jvm-sandbox/release/sandbox-stable-bin.zip
执行安装
unzip sandbox-stable-bin.zipcd sandbox
启动沙箱
假设目标进程号:64229
可以看到broken-clock-tinker模块已经正确被沙箱所加载
启动沙箱
./sandbox.sh -p 64229
NAMESPACE : default
VERSION : 1.2.0
MODE : ATTACH
SERVER_ADDR : 0.0.0.0
SERVER_PORT : 56854
UNSAFE_SUPPORT : ENABLE
SANDBOX_HOME : /Users/vlinux/opt/sandbox
SYSTEM_MODULE_LIB : /Users/vlinux/opt/sandbox/module
USER_MODULE_LIB : ~/.sandbox-module;
SYSTEM_PROVIDER_LIB : /Users/vlinux/opt/sandbox/provider
EVENT_POOL_SUPPORT : DISABLE
查看模块
./sandbox.sh -p 64229 -l
broken-clock-tinker ACTIVE LOADED 0 0 UNKNOW_VERSION UNKNOW_AUTHOR
sandbox-info ACTIVE LOADED 0 0 0.0.4 [email protected]
sandbox-module-mgr ACTIVE LOADED 0 0 0.0.2 [email protected]
sandbox-control ACTIVE LOADED 0 0 0.0.3 [email protected]
total=4
接下来就是重头戏,如何在不影响目标应用的情况下,无声无息的修复这个故障!
触发broken-clock-tinker模块的repairCheckState(),让修复逻辑生效!
BrokenClockTinkerModule#repairCheckState()
方法执行./sandbox.sh -p 64229 -d 'broken-clock-tinker/repairCheckState'
过一会,模块生效完成,你就会发现原本一直抛异常的钟已经开始在刷新时间了,
java.lang.IllegalStateException: STATE ERROR!
at com.taobao.demo.Clock.checkState(Clock.java:16)
at com.taobao.demo.Clock.report(Clock.java:34)
at com.taobao.demo.Clock.loopReport(Clock.java:44)
at com.taobao.demo.Clock.main(Clock.java:53)
java.lang.IllegalStateException: STATE ERROR!
at com.taobao.demo.Clock.checkState(Clock.java:16)
at com.taobao.demo.Clock.report(Clock.java:34)
at com.taobao.demo.Clock.loopReport(Clock.java:44)
at com.taobao.demo.Clock.main(Clock.java:53)
java.lang.IllegalStateException: STATE ERROR!
at com.taobao.demo.Clock.checkState(Clock.java:16)
at com.taobao.demo.Clock.report(Clock.java:34)
at com.taobao.demo.Clock.loopReport(Clock.java:44)
at com.taobao.demo.Clock.main(Clock.java:53)
2018-10-23 22:31:39
2018-10-23 22:31:40
2018-10-23 22:31:41
2018-10-23 22:31:42
2018-10-23 22:31:43
2018-10-23 22:31:44
当你卸载掉JVM-SANDBOX时候,你就会发现原本已经被修复好的钟,又开始继续报错了。原因是原来通过clock-tinker模块修复的checkState()方法随着沙箱的卸载又恢复成原来错误的代码流程。
卸载沙箱
./sandbox.sh -p 64229 -S
jvm-sandbox[default] shutdown finished.
故障继续
2018-10-23 23:44:10
2018-10-23 23:44:11
2018-10-23 23:44:12
java.lang.IllegalStateException: STATE ERROR!
at com.taobao.demo.Clock.checkState(Clock.java:16)
at com.taobao.demo.Clock.report(Clock.java:34)
at com.taobao.demo.Clock.loopReport(Unknown Source)
at com.taobao.demo.Clock.main(Unknown Source)
java.lang.IllegalStateException: STATE ERROR!
at com.taobao.demo.Clock.checkState(Clock.java:16)
at com.taobao.demo.Clock.report(Clock.java:34)
at com.taobao.demo.Clock.loopReport(Unknown Source)
at com.taobao.demo.Clock.main(Unknown Source)
java.lang.IllegalStateException: STATE ERROR!
at com.taobao.demo.Clock.checkState(Clock.java:16)
at com.taobao.demo.Clock.report(Clock.java:34)
at com.taobao.demo.Clock.loopReport(Unknown Source)
at com.taobao.demo.Clock.main(Unknown Source)
如何调试这一类的程序代码,可以参考GREYS项目中的一个ISSUES:怎么调试啊?
在这个教程中给大家演示了如何利用沙箱的模块改变了原有方法的执行流程,这里涉及到了沙箱最核心的类ModuleEventWatcher,这个类的实现可以通过@Resource
注释注入进来。
通过在THROWS
事件环节的改变流程,可以让原本应该抛出异常的checkState()
方法转变为正常返回值。你甚至可以窥探、篡改入参、返回值、抛出的异常等等,这些都将可以通过沙箱模块来实现。沙箱模块还能帮你实现很多有意思的功能,期待你的想象。
沙箱分发包中自带了实用工具的例子./example/sandbox-debug-module.jar
,代码在沙箱的sandbox-debug-module
模块,也是非常不错的实用工具和学习资料。
例子 | 例子说明 |
---|---|
DebugWatchModule.java | 模仿GREYS的watch 命令 |
DebugTraceModule.java | 模仿GREYES的trace 命令 |
DebugRalphModule.java | 无敌破坏王,故障注入(延时、熔断、并发限流、TPS限流) |
LogExceptionModule.java | 记录下你的应用都发生了哪些异常 $HOME/logs/sandbox/debug/exception-monitor.log |
LogServletAccessModule.java | 记录下你的应用的HTTP服务请求 $HOME/logs/sandbox/debug/servlet-access.log |