jvmsandboxrepeater时间mock插件设计与实现

2023年 9月 21日 70.9k 0

一、背景

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

二、调研

2.1 如何mock当前时间

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

  • java.util.Date new Date() 获取当前时间
  • 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 初步设计

    基本的流程如下:

  • 拦截System.currentTimeMillis();
  • 判断本次调用是否为回放流量,如果不是回放流量,调用System.currentTimeMillis()原生逻辑返回结果
  • 如果是回放流程,则从采集上下文中取特定时间作为返回结果即可
  • image.png
    看似简单的流程,实现过程中会遇到如下问题:

  • 拦截 System.currentTimeMillis() 怎么实现
  • 如何判断本次流量是否为回放流量
  • 取什么时间作为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 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下:
    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段逻辑:

    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入口流量类型插件
     * 

    * * @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参数初始化流程即可

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论