jvm-sandbox-repeater时间mock插件设计与实现

一、背景

jvm-sandbox-repeater实现了基础的录制回放流程编排,并简单的给了几个插件的demo,离实际项目运用其实还需要二次开发很多东西,其中时间mock能力是一个非常基础的能力,业务代码里经常需要用到这块;

二、调研

2.1 如何mock当前时间

我们mock的主要是"当前时间",java里获取当前时间的主要方式是以下两种(LocalDate其实也很常用,但是我没有去做mock了,感兴趣的参考文档自行开发):

  1. java.util.Date new Date() 获取当前时间
  2. System.currentTimeMillis() 获取当前时间

new Date()构造函数实现中,我们发现取当前时间调用的就是System.currentTimeMillis(),因此我们只需要mock System.currentTimeMillis()即可

java 复制代码
//默认构造函数
public Date() {
    //这里取的就是System.currentTimeMillis(), 所以我们只需要mock 
    this(System.currentTimeMillis());
}

2.2 基于sandbox怎么实现

我自己经过多次测试,下面的实现方式是能够有效拦截并生效的,因此sandbox是有能力拦截java native实现的, 基于此,怎么实现就简单了;

java 复制代码
@MetaInfServices(Module.class)
@Information(id = "date-mocker")
public class DateMockModule  implements Module {

    @Resource
    private ModuleEventWatcher moduleEventWatcher;

    @Command("repairCheckState")
    public void repairCheckState() {

        new EventWatchBuilder(moduleEventWatcher)
                .onClass(System.class)
                .includeBootstrap()
                .onBehavior("currentTimeMillis")
                .onWatch(
                        new AdviceListener() {
                            protected void before(Advice advice) throws Throwable {
                                System.out.println("come here");
                            }
                        }
                );
    }
}

三、设计与实现

3.1 初步设计

基本的流程如下:

  1. 拦截System.currentTimeMillis();
  2. 判断本次调用是否为回放流量,如果不是回放流量,调用System.currentTimeMillis()原生逻辑返回结果
  3. 如果是回放流程,则从采集上下文中取特定时间作为返回结果即可

看似简单的流程,实现过程中会遇到如下问题:

  1. 拦截 System.currentTimeMillis() 怎么实现
  2. 如何判断本次流量是否为回放流量
  3. 取什么时间作为mock的时间

接下来,我们针对上面的问题具体解答;

3.2 jvm-sandbox-repeater新增一个Date插件

新增插件需要定义以下三个东西

  • 继承 AbstractInvokePluginAdapter,定义插件类型、名称以及拦截点
  • 定义EventListener, 处理拦截点返回的BEFORE/RETURN/THROWS等事件;
  • 定义InvocationProcessor, 根据拦截信息组装Invocation信息,或者mock的时候直接返回结果;

首先定义DatePlugin

java 复制代码
package com.alibaba.jvm.sandbox.repeater.plugin.date;

import com.alibaba.jvm.sandbox.api.event.Event;
import com.alibaba.jvm.sandbox.api.listener.EventListener;
import com.alibaba.jvm.sandbox.repeater.plugin.api.InvocationListener;
import com.alibaba.jvm.sandbox.repeater.plugin.api.InvocationProcessor;
import com.alibaba.jvm.sandbox.repeater.plugin.core.impl.AbstractInvokePluginAdapter;
import com.alibaba.jvm.sandbox.repeater.plugin.core.model.EnhanceModel;
import com.alibaba.jvm.sandbox.repeater.plugin.domain.InvokeType;
import com.alibaba.jvm.sandbox.repeater.plugin.spi.InvokePlugin;
import com.google.common.collect.Lists;
import org.kohsuke.MetaInfServices;

import java.util.List;

@MetaInfServices(InvokePlugin.class)
public class DatePlugin extends AbstractInvokePluginAdapter {

    @Override
    public InvokeType getType() {
        return InvokeType.JAVA_DATE;
    }

    @Override
    public String identity() {
        return "java-date";
    }

    @Override
    public boolean isEntrance() {
        return false;
    }

    @Override
    protected List<EnhanceModel> getEnhanceModels() {
        
        //这里是拦截点信息
        EnhanceModel em = EnhanceModel.builder()
                //这里需要扩展支持下,sandbox操作原生类需要支持
                .includeBootstrap(true)
                .classPattern("java.lang.System")
                .methodPatterns(EnhanceModel.MethodPattern.transform("currentTimeMillis"))
                .watchTypes(Event.Type.BEFORE, Event.Type.RETURN, Event.Type.THROWS, Event.Type.CALL_RETURN)
                .build();

        return Lists.newArrayList(em);
    }

    protected EventListener getEventListener(InvocationListener listener) {
        return new DatePluginEventListener(getType(), isEntrance(), listener, getInvocationProcessor());
    }

    @Override
    protected InvocationProcessor getInvocationProcessor() {
        return new DatePluginProcessor(getType());
    }

}

再定义DatePluginEventListener

java 复制代码
package com.alibaba.jvm.sandbox.repeater.plugin.date;

import com.alibaba.jvm.sandbox.api.ProcessControlException;
import com.alibaba.jvm.sandbox.api.event.BeforeEvent;
import com.alibaba.jvm.sandbox.api.event.Event;
import com.alibaba.jvm.sandbox.repeater.plugin.api.InvocationListener;
import com.alibaba.jvm.sandbox.repeater.plugin.api.InvocationProcessor;
import com.alibaba.jvm.sandbox.repeater.plugin.core.cache.RepeatCache;
import com.alibaba.jvm.sandbox.repeater.plugin.core.impl.api.DefaultEventListener;
import com.alibaba.jvm.sandbox.repeater.plugin.core.trace.Tracer;
import com.alibaba.jvm.sandbox.repeater.plugin.domain.InvokeType;
import com.alibaba.jvm.sandbox.repeater.plugin.domain.RepeatContext;

import java.util.Date;

public class DatePluginEventListener extends DefaultEventListener {

    public DatePluginEventListener(InvokeType invokeType, boolean entrance, InvocationListener listener, InvocationProcessor processor) {
        super(invokeType, entrance, listener, processor);
    }

    @Override
    public void onEvent(Event event) throws Throwable {
        if (!event.type.equals(Event.Type.BEFORE)) {
            return;
        }

        BeforeEvent e = (BeforeEvent) event;


        //只处理回放流量
        if (RepeatCache.isRepeatFlow(Tracer.getTraceId())) {

            //processor.doMock(event, entrance, invokeType);
            RepeatContext repeatContext = RepeatCache.getRepeatContext(Tracer.getTraceId());
            if (repeatContext == null) {
                return;
            }
            
            //特殊场景必须这么判断
            if (!repeatContext.getCanMockDate()) {
                return;
            }

            //获取录制时间
            long recordTime = repeatContext.getRecordModel().getTimestamp();

            if (e.javaClassName.equals("java.lang.System")) {
                //这里是sandbox的一个约定,抛异常直接返回结果
                ProcessControlException.throwReturnImmediately(recordTime);
            }
        }
    }
}

最后定义DatePluginProcessor:

java 复制代码
package com.alibaba.jvm.sandbox.repeater.plugin.date;


import com.alibaba.jvm.sandbox.api.event.InvokeEvent;
import com.alibaba.jvm.sandbox.repeater.plugin.core.impl.api.DefaultInvocationProcessor;
import com.alibaba.jvm.sandbox.repeater.plugin.domain.InvokeType;

import static com.alibaba.jvm.sandbox.api.event.Event.Type.BEFORE;

public class DatePluginProcessor extends DefaultInvocationProcessor {

    public DatePluginProcessor(InvokeType type) {
        super(type);
    }


    @Override
    public boolean ignoreEvent(InvokeEvent event) {
        if (!event.type.equals(BEFORE)) {
            return true;
        }

        return false;
    }
}

整个插件的结构如下

代码实现可以到我的github下: github.com/penghu2/san...

四、实践过程中遇到的问题

4.1 jvm-sandbox-repeater 原生代码不支持 includeBootstrap

com.alibaba.jvm.sandbox.repeater.plugin.core.model.EnhanceModel.EnhanceModelBuilder 里没有地方可以设置 includeBootstrap, 这个需要自己支持下,因为这个比较简单,我就不再多说;

4.2 spring mvc controller @RequestBody中带Date的,以及java主调用请求参数中的Date是不可以mock的

我们判断流量是否为回放流量,有2段逻辑:

java 复制代码
if (RepeatCache.isRepeatFlow(Tracer.getTraceId())) {

            //processor.doMock(event, entrance, invokeType);
            RepeatContext repeatContext = RepeatCache.getRepeatContext(Tracer.getTraceId());
            if (repeatContext == null) {
                return;
            }
            
            //特殊场景必须这么判断
            if (!repeatContext.getCanMockDate()) {
                return;
            }
}

RepeatCache.isRepeatFlow(Tracer.getTraceId()) 是从线程变量里判断本次是否为回放流量;!repeatContext.getCanMockDate()是为了确保 回放的入参都初始化之后在mock时间,否则会导致入参被覆盖!repeatContext中我们定义了个变量boolean canMockDate,这个变量的修改放在了HttpPlugin里:

java 复制代码
package com.alibaba.jvm.sandbox.repater.plugin.http;

/**
 * {@link HttpPlugin} http入口流量类型插件
 * <p>
 *
 * @author zhaoyb1990
 */
@MetaInfServices(InvokePlugin.class)
public class HttpPlugin extends AbstractInvokePluginAdapter {

    。。。。省略其他冗余代码

    @Override
    public void onLoaded() throws PluginLifeCycleException {
        new EventWatchBuilder(watcher)
                .onClass("org.springframework.web.method.support.InvocableHandlerMethod")
                .onBehavior("doInvoke")
                .onWatch(new AdviceListener() {

                    protected void before(Advice advice) throws Throwable {
                        if (RepeatCache.isRepeatFlow(Tracer.getTraceId())) {
                            RepeatContext repeatContext = RepeatCache.getRepeatContext(Tracer.getTraceId());
                            if (repeatContext!=null) {
                                repeatContext.setCanMockDate(true);
                            }

                        }
                    }
                    protected void afterReturning(Advice advice) throws Throwable {
                        if (RepeatCache.isRepeatFlow(Tracer.getTraceId())) {
                            RepeatContext repeatContext = RepeatCache.getRepeatContext(Tracer.getTraceId());
                            if (repeatContext!=null) {
                                repeatContext.setCanMockDate(false);
                            }
                        }
                    }

                });
    }
}

我们拦截了org.springframework.web.method.support.InvocableHandlerMethod#doInvoke的入口,在执行之前repeatContext.setCanMockDate(true),执行之后 repeatContext.setCanMockDate(false);那为什么是这里拦截呢,就需要你自行去调研了(调研下spring @RequestBody参数初始化流程即可

相关推荐
why1512 小时前
腾讯(QQ浏览器)后端开发
开发语言·后端·golang
浪裡遊2 小时前
跨域问题(Cross-Origin Problem)
linux·前端·vue.js·后端·https·sprint
声声codeGrandMaster2 小时前
django之优化分页功能(利用参数共存及封装来实现)
数据库·后端·python·django
呼Lu噜2 小时前
WPF-遵循MVVM框架创建图表的显示【保姆级】
前端·后端·wpf
bing_1583 小时前
为什么选择 Spring Boot? 它是如何简化单个微服务的创建、配置和部署的?
spring boot·后端·微服务
学c真好玩3 小时前
Django创建的应用目录详细解释以及如何操作数据库自动创建表
后端·python·django
Asthenia04123 小时前
GenericObjectPool——重用你的对象
后端
Piper蛋窝3 小时前
Go 1.18 相比 Go 1.17 有哪些值得注意的改动?
后端
excel3 小时前
招幕技术人员
前端·javascript·后端
盖世英雄酱581363 小时前
什么是MCP
后端·程序员