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

2023-09-21 10:47:42

一、背景

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. 如果是回放流程,则从采集上下文中取特定时间作为返回结果即可 alt

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

  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;
    }
}

整个插件的结构如下

alt 代码实现可以到我的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, 这个需要自己支持下,因为这个比较简单,我就不再多说;

alt

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 多平台发布

更多推荐

【ArcGIS】基本概念-矢量空间分析

栅格数据与矢量数据1.1栅格数据栅格图是一个规则的阵列,包含着一定数量的像元或者栅格常用的栅格图格式有:tif,png,jpeg/jpg等1.2矢量数据矢量图是由一组描述点、线、面,以及它们的色彩、位置的数据,通过软件算法计算得到的图形。常用的矢量图格式有:shp、eps、dwg、dxf等GIS中矢量数据可以分为地图层

阿里云CDN架构接入WAF应用防火墙案例实践

文章目录1.网站架构变化2.配置WAF应用防火墙2.1.配置网站接入WAF防火墙2.2.WAF防火墙生成CNAME地址2.3.配置WAF防火墙HTTPS证书2.4.WAF防火墙开启HTTP回源SLB3.配置CDN加速器回源WAF防火墙4.将域名DNS解析指向CDN的域名5.测试网站是否能正常访问6.模拟攻击观察WAF的

基于微信小程序的小区服务管理系统设计与实现(源码+lw+部署文档+讲解等)

前言💗博主介绍:✌全网粉丝10W+,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌💗👇🏻精彩专栏推荐订阅👇🏻2023-2024年最值得选的微信小程序毕业设计选题大全:100个热门选

【Linux】线程的概念

文章目录📖前言1.线程的引入1.1执行流:1.2线程的创建:1.3线程的等待:2.查看线程2.1链接线程库:2.2ps-aL:2.3获取线程的LWP:3.页表的认识3.1二级页表:3.2页表的实际大小:4.再看线程4.1线程总结:4.2线程的优点:4.3线程的缺点:📖前言从本章开始,我们进入Linux系统编程最后一

HarmonyOS应用开发者基础认证考试题目及答案

小试了一下HarmonyOS应用开发者基础认证考试,顺利通过,下面试题及答案。不过考试好像每次题目不尽相同,好像是抽取的,仅供参考。【判断题】1.所有使用@Component修饰的自定义组件都支持onPageShow,onBackPress和onPageHide生命周期函数。(错)2.每一个自定义组件都有自己的生命周期

【TypeScript】项目中对于TypeScript的打包处理

webpack通常情况下,实际开发中我们都需要使用构建工具对代码进行打包,TS同样也可以结合构建工具一起使用,下边以webpack为例介绍一下如何结合构建工具使用TS。步骤:初始化项目进入项目根目录,执行命令npminit-y主要作用:创建package.json文件下载构建工具npmi-Dwebpackwebpack

自动化项目实战:用requests库自动保存王者荣耀英雄皮肤到本地,文末附源码下载!

前言王者荣耀是一款备受欢迎的手机游戏,拥有众多精美的英雄皮肤。如果你想获取这些皮肤的图片或者其他相关信息,可以利用Python编写一个简单的爬虫来实现。安装第三方库首先,我们需要安装Python的requests和BeautifulSoup库。可以使用以下命令来安装它们:pipinstallrequestspipins

Ubuntu上通过源码方式安装Redis

上一篇文章Ubuntu上安装、使用Redis的详细教程已经介绍了再Ubuntu操作系统上安装Redis的详细过程,但是因为安装的Redis只有最主要的配置文件和redis-server,为了更深入地学习Redis和进行更复杂的操作,需要安装一个完整的Redis服务。这篇文章就介绍一下怎么在ubuntu上通过源码编译方式

[每周一更]-(第63期):Linux-nsenter命令使用说明

nsenter命令是一个可以在指定进程的命令空间下运行指定程序的命令。它位于util-linux包中。1、用途一个最典型的用途就是进入容器的网络命令空间。相当多的容器为了轻量级,是不包含较为基础的命令的,比如说ipaddress,ping,telnet,ss,tcpdump等等命令,这就给调试容器网络带来相当大的困扰:

HTTP 响应头Cache-Control

每个资源都可以通过Http头Cache-Control来定义自己的缓存策略,Cache-Control控制谁在什么条件下可以缓存响应以及可以缓存多久。最快的请求是不必与服务器进行通信的请求:通过响应的本地副本,我们可以避免所有的网络延迟以及数据传输的数据成本。为此,HTTP规范允许服务器返回一系列不同的Cache-Co

【Python】PySpark 数据计算 ④ ( RDD#filter 方法 - 过滤 RDD 中的元素 | RDD#distinct 方法 - 对 RDD 中的元素去重 )

文章目录一、RDD#filter方法1、RDD#filter方法简介2、RDD#filter函数语法3、代码示例-RDD#filter方法示例二、RDD#distinct方法1、RDD#distinct方法简介2、代码示例-RDD#distinct方法示例一、RDD#filter方法1、RDD#filter方法简介RD

热文推荐