Skip to content

FIRST MODULE

李夏驰 edited this page Feb 13, 2019 · 3 revisions

BANNER

让我们来快速开始写一个入门模块

修复一个损坏了的钟

一个损坏的钟

我们定义了一个钟,期望可以实现每隔一定的时间进行报时。

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来简化我们的配置工作

<parent>
    <groupId>com.alibaba.jvm.sandbox</groupId>
    <artifactId>sandbox-module-starter</artifactId>
    <version>1.2.0</version>
</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 {

    @Resource
    private 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()所拦截
                     */
                    @Override
                    protected void afterThrowing(Advice advice) throws Throwable {
                        
                        // 在此,你可以通过ProcessController来改变原有方法的执行流程
                        // 这里的代码意义是:改变原方法抛出异常的行为,变更为立即返回;void返回值用null表示
                        ProcessController.returnImmediately(null);
                    }
                });

    }

}

编译部署clock-tinker模块

  1. 运行命令完成打包

    mvn clean package
    
  2. 将打好的包复制到用户模块目录下

    cp target/clock-tinker-1.0-SNAPSHOT-jar-with-dependencies.jar ~/.sandbox-module/
    
  3. 下载并安装最新版本沙箱:

  4. 启动沙箱

    假设目标进程号:64229

    • 启动沙箱

      ./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           luanjia@taobao.com
      sandbox-module-mgr  ACTIVE  LOADED  0  0  0.0.2           luanjia@taobao.com
      sandbox-control     ACTIVE  LOADED  0  0  0.0.3           luanjia@taobao.com
      total=4

    可以看到broken-clock-tinker模块已经正确被沙箱所加载

修复clock#checkState()方法

接下来就是重头戏,如何在不影响目标应用的情况下,无声无息的修复这个故障!

触发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

恢复被修复的check()方法

当你卸载掉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