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()即可

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

2.2 基于sandbox怎么实现

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

复制代码
@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

复制代码
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

复制代码
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:

复制代码
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下: https://github.com/penghu2/sandbox-repeater/tree/master/repeater-plugins/date-plugin

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

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段逻辑:

复制代码
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里:

复制代码
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参数初始化流程即可)~~

本文由mdnice多平台发布

相关推荐
Rust研习社22 分钟前
Reqwest 兼顾简洁与高性能的现代 HTTP 客户端
开发语言·网络·后端·http·rust
绿草在线23 分钟前
SpringBoot请求与响应全解析
spring boot·后端·lua
Victor3562 小时前
MongoDB(103)如何处理分片集群中的数据不一致?
后端
Victor3562 小时前
MongoDB(104)如何处理MongoDB中的磁盘空间不足问题?
后端
立莹Sir3 小时前
商品中台架构设计与技术落地实践——基于Spring Cloud微服务体系的完整解决方案
分布式·后端·spring cloud·docker·容器·架构·kubernetes
杨凯凡9 小时前
【021】反射与注解:Spring 里背后的影子
java·后端·spring
Ares-Wang10 小时前
Flask》》 Flask-Bcrypt 哈希加密
后端·python·flask
小码哥_常10 小时前
Spring Boot项目大变身:为何要拆成这六大模块?
后端
码事漫谈12 小时前
兵临城下:DeepSeek-V4 的技术突围与算力“成人礼”
后端
三水不滴12 小时前
SpringAI + SpringDoc + Knife4j 构建企业级智能问卷系统
经验分享·spring boot·笔记·后端·spring