目录

一、实现功能

1、使用spring boot 实现mock平台

2、返回结果数据的存放:

3、如何根据url返回对应的结果?

1.3.1  将请求的URI拼成返回结果的文件/文件夹路径

1.3.2 根据请求的ip不同,返回不同的结果。

1.3.3  根据参数不同,返回对应的数据。

1.4 返回结果不是写死的数据,而是动态数据

1.5 调用其他服务/透传请求

1.6.模拟响应时间

1.7 hook参数

二、注意事项

三、整体架构&实现思路

四、MockController 拦截所有的请求,并对请求进行处理

五、用户的请求与返回结果的封装

5.1 返回结果文件对应的实体类

5.2 最终返回结果信息的封装MockContext

5.3 读取结果文件,封装到实体类YamlUtil

六、根据用户请求,读取对应的结果文件(根据URI拼接成文件的路径)

6.1 责任链设计模式处理文件&文件夹

七、观察者模式对mock数据的处理

7.1 MockContext  mock内容类的作用

7.2 观察者模式MockContext 处理

八、 对文件&文件夹的处理逻辑

8.1 请求只有一个返回结果,对应一个文件

8.2 当请求对应一个文件夹时 

 九、匹配返回结果文件(权重计算)

十、 处理动态变量,使用了装饰器模式

十一、hook参数处理

11.1 方式一:直接对response数据进行处理

11.2 方式二:采用装饰器模式对response数据进行处理

十二、依赖

十二、启动项目


一、实现功能

 源码地址:

GitHub - 18713341733/mockServer

实现的功能很简单,就是对url请求的返回结果进行mock。但是里面细节比较多。

本文在讲解的时候,是根据某个功能的实现来针对性的讲解的。

想要整体的了解这个项目,需要自己去看源码。

1、使用spring boot 实现mock平台

2、返回结果数据的存放:

我们可以将需要的返回结果数据,存放在数据库中,或者存放在本地文件。

本项目,将需要的返回结果,存放在了本地的txt文件中。具体存放在哪里根据需要来,各有各的好处。

3、如何根据url返回对应的结果?

将所有的数据都存放在了mock_data 的这个文件夹里面。

spring 实现mock挡板 springboot mock_java

返回结果有2种情况。

情况1,就是这个接口请求,只有一种返回结果。那我们只需要有一个txt文件与之对应就好了。

如文件get_order_info 。

情况2,这个接口请求,根据请求传参不同,我们需要返回对应结果。则需要建立一个文件夹,将这个请求的各种返回结果全部存放在这个文件夹下。

我的项目中mock_data文件的路径为:

/Users/zhaohui/IdeaProjects/mock-server/src/main/resources/mock_data

1.3.1  将请求的URI拼成返回结果的文件/文件夹路径

我们将服务部署在本地,则mock平台的host为127.0.0.1

将用户请求中的uri拼成一个文件/文件夹的名称,去匹配对应的结果。

如上图中,我们返回结果有文件也有文件。

用户请求

http:127.0.0.1:8080/get/user?id=123&name=zhangsan

获取请求的URI, /get/user

去掉第一个/,将后面的/替换成_,然后拼成一个文件/文件夹的名称。

 /get/user ==> get_user

将得到的名称在前边拼接上数据存放的目录,(拼接上mock_data文件的路径)

/Users/zhaohui/IdeaProjects/mock-server/src/main/resources/mock_data/get_user

这个路径,是我们通过用户请求的URI拼接出来的,我们再去对应的位置,找这个文件。

在真实的路径/Users/zhaohui/IdeaProjects/mock-server/src/main/resources/mock_data/get_user

下,判断get_user 是一个文件夹,则我们取文件夹下的某个一文件作为返回结果进行返回。

用户请求,http:127.0.0.1:8080/get/order_info?id=123&name=zhangsan

获取请求的URI, /get/order/info,转换成文件/文件夹的名称get_order_info

再拼接上mock_data文件的路径,则我们通过用户请求得到的路径为

/Users/zhaohui/IdeaProjects/mock-server/src/main/resources/mock_data/get_order_info

我们再去真实的这个路径下,去获取这个文件。判断得到这个路径是一个文件,则我们直接将这个文件里的内容作为返回结果。

1.3.2 根据请求的ip不同,返回不同的结果。

1.3.3  根据参数不同,返回对应的数据。

当一个请求,

http:127.0.0.1:8080/get/user?id=123&name=zhangsan

有多个返回结果,如果根据传参不同,返回对应的结果?

spring 实现mock挡板 springboot mock_spring boot_02

如,get_user请求,对应多个返回结果。

在返回结果中文件中,我们标明对应的传参与这个参数的权重。取权重最大的。

spring 实现mock挡板 springboot mock_java_03

a文件:传参,id=123的权重为8,name=zhangsan的权重为10

spring 实现mock挡板 springboot mock_spring 实现mock挡板_04

 b文件:传参,id=456的权重为2,name=zhangsan的权重为4.

spring 实现mock挡板 springboot mock_返回结果_05

 则请求:

 http:127.0.0.1:8080/get/user?id=123&name=zhangsan

与a文件匹配,id=123与name=zhangsan都命中了,则a文件的权重为8+10=18

与b文件匹配,只命中了name=zhangsan,则b文件的权重为4。

在文件夹内,a匹配权重最大,我们取a的数据作为返回结果。

1.4 返回结果不是写死的数据,而是动态数据

1、 返回的数据中不能全部都是写死的,有的可能是随机的id,有的可能是时间戳,还有的可能是固定格式的数据

2、实际业务有一个case: 要返回merId:xxxx, 但是这个merId的获取,是要从别的业务的接口中获取返回信息。

1.5 调用其他服务/透传请求

mock的返回结果,需要调用数据库,或者其他http请求。

比如10个请求,请求mock服务,其中参数id=123的走mock,id=456的走真实的服务。

所以这个时候如果我们判断id=456了,我们需要去自己真实的拿着请求的参数,我们再去调真实服务。

拿到返回结果,在返回给调用端。

1.6.模拟响应时间

比如服务调我们的mock时,我们是直接给返回。

那要是模拟一下真实的服务处理,比如处理超时,假设用时 3秒在返回。

模拟超时处理

思考: 如果你做线上压测的时候,相应时间不能给返回一个固定值,所以返回是一个区间的概率。

1.7 hook参数

比如请求的时候,请求参数携带一个requestId, 然后requestId本身还是个变化的,也是随机的。

然后在返回的时候,要把这个id带回去,即:虽然返回数据不能写死,但是你也不能自己生成,需要使用请求的参数。

二、注意事项

注意:

在这个项目中,我将请求的返回结果存放在了resouces文件夹下了。

当读取这些文件时,我读取的是文件的绝对路径。当你使用这个项目时,你需要把文件的绝对路径,改成自己的路径。

修改位置:MockContext

spring 实现mock挡板 springboot mock_spring 实现mock挡板_06

spring 实现mock挡板 springboot mock_返回结果_07

三、整体架构&实现思路

1、收集用户输入信息,存到一个实体类里mockContext

2、将用户输入的URI,拼成一个路径。

路径是文件,直接返回文件内容

路径是目录,则读取这个目录下的所有文件。计算每个文件的权重,取出权重最大的返回结果

3、这里处理文件&文件夹用的是责任链模式

4、具体处理文件夹的逻辑,我们这里使用的是观察者模式

这里用到了mockContext 这个实体类。mockContext 不仅存储了用户的输入数据,

还存储了根据接口读取的文件内容。

观察者模式,多个实体工具类,for循环处理 数据mockContext,处理完再将数据写入mockContext。 多个方法循环处理mockContext,这个mockContext是同一个变量。

四、MockController 拦截所有的请求,并对请求进行处理

package com.example.mockserver.controller;

import cn.hutool.core.io.FileUtil;
import com.example.mockserver.model.MappingParamsEntity;
import com.example.mockserver.model.MockContext;
import com.example.mockserver.model.MockDataInfo;
import com.example.mockserver.service.MockService;
import com.example.mockserver.util.ArrayUtil;
import com.example.mockserver.util.YamlUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@RestController
@Slf4j
public class MockController {



    @Autowired
    private HttpServletRequest request;

    @Autowired
    private MockService mockService;

    @RequestMapping("/**")
    public String doMock() throws IOException {
        log.info("请求的URI---------:"+request.getRequestURI());
        log.info("请求IP---------:"+request.getRemoteAddr());
        log.info("请求的参数---------:"+request.getParameterMap());


        // 将获取的用户数据 ip 参数 URI ,存储到 mockContext 这个类里
        MockContext mockContext = MockContext.builder()
                .requestIp(request.getRemoteAddr()) // 获取ip
                .requestParams(getParams(request.getParameterMap()))
                .requestURI(request.getRequestURI()) // 获取请求的URI
                .build();

        String response = mockService.doMock(mockContext);

        return response ;

    }


    // 获取用户的传参,value是一个数组。这里为了将来处理方便,我们将这数组转成一个字符串。
    // 我们默认,这个数据的长度是1,那我们只需要取出来数组的第一个值就可以了。
    public Map<String,String> getParams(Map<String,String[]> parameterMap){
        Map<String,String> params = parameterMap.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(),e -> ArrayUtil.getFirst(e.getValue())));
        return params;

    }


}

spring 实现mock挡板 springboot mock_java_08

1、拦截所有用户请求

2、将用户的所有请求信息,封装到mockContext 这个类里

3、对用户的请求信息mockContext 进行处理。

五、用户的请求与返回结果的封装

用户,进行请求。我们需要用一个实体类来存储用户的请求。

http://127.0.0.1:8081/get/user?name=lisi&id=123

这里我们并没有用一个单独的实体类存储用户信息,我们用了一个比较综合的实体类来存储用户请求信息。 MockContext 

MockContext 不仅存储了请求的信息,也存储了对应接口返回的信息。

package com.example.mockserver.model;

import com.example.mockserver.consts.MockConst;
import lombok.Builder;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Data
@Builder
public class MockContext {
    // 用户传入信息
    // 一次用户请求,对应一个MockContext
    private String requestURI;
    private Map<String,String> requestParams;
    private String requestIp;

    // 返回结果的List
    // 一个接口,对应的返回结果,是一个List
    private List<MockDataInfo> mockDataInfoList;
    private String finalResponse;
    private Long timeout;
    private boolean timeoutSet;
    private String realUrl;
    private boolean realUrlSet;

    public void setRealUrl(String realUrl) {
        if(StringUtils.isNotEmpty(realUrl)){
            this.realUrl = realUrl;
            this.realUrlSet = true;
        }
    }

    public void setTimeout(Long timeout){
        if(timeout != null && timeout >0 ){
            this.timeout = timeout;
            timeoutSet = true;
        }
    }

    // 根据uri,得到文件名
    // /get/order/info -> get_order_info
    // 去掉第一个/ ,取后面的字符串
    public String getFileName(){
        String str = StringUtils.substringAfter(this.requestURI, "/");
        String fileName = StringUtils.replace(str, "/", "_");
        return fileName;
    }

    // 得到文件的路径
    public String getFilePath(){
        String filePath = MockConst.MOCK_DATA_PATH+"/"+this.getFileName();
        return filePath;
    }
    // 将用户传参,组成一个k=v 的List
    public List<String> getParamStringList(){
        // 计算权重的方法
        // 用户的传参,mockContext.getRequestParams(),是一个Map
        // 将用户的传参Map,转换成list。
        // 如 k:v, id:123,name:zhangsan 转换成 【"id=123","name=zhangsan"]
        List<String> paramStrList = this.getRequestParams().entrySet().stream()
                .map(e -> e.getKey() + "=" + e.getValue())
                .collect(Collectors.toList());
        return paramStrList;
    }


}

spring 实现mock挡板 springboot mock_前端_09

1、用户的请求信息,包含请求的uri、请求ip、请求参数。这些信息从请求中获取

2、是请求返回结果相关信息的封装。

我们将接口的返回信息,用文件存储了起来。看一下这个存储接口返回

spring 实现mock挡板 springboot mock_返回结果_10

 查看aaa文件

mappingHost: 127.0.0.1
timeout: 3000
realUrl: http://www.baidu.com
mappingParams:
    - params:
        id: 123
      weight: 8
    - params:
        name: "zhangsan"
      weight: 10
response: '{"key11":"${random:id:6}","key2":"${random:str:10}","count":3,"person":[{"id":${hook:id}},"name":"张三"},{"id":2,"name":"李四"}],"object":{"id":1,"msg":"对象里的对象"}}'

这个请求结果返回文件,存储了3块内容。

spring 实现mock挡板 springboot mock_spring boot_11

1、存储的是请求的逻辑处理。

 timeout: 3000 ,用来设置我们接口请求的返回时间。我们不想请求的mock结果瞬间返回,想要有个3s的延迟,再返回结果。这个场景在压测中可能会遇到。比如我们模拟第三方的服务,接口请求不是瞬间返回的,会有一个3秒的处理过程。

realUrl: http://www.baidu.com,请求的透传。应用场景:

同一个接口请求,根据传参不通,部分请求走mock数据,部分请求走真实的服务。

这里就是设置的对应的真实服务url的地址。

2、存贮的是请求的参数。

用户的请求如下:

http://127.0.0.1:8081/get/user?name=lisi&id=123

我们存储的请求参数为,name=lisi&id=123。

注意这里请求参数的格式。

spring 实现mock挡板 springboot mock_java_12

 一个请求,有多个参数。每个参数作为一个对象。所有的请求作为一个List集合。

为啥要每个参数单独作为一个对象?

因为我们将来要根据请求的参数不同,匹配对应的返回结果文件。这样好处理一些。

3、请求的返回结果

3存储的就是请求的最终返回结果,一个json格式的数据。

请求的返回结果不一定是死数据,有可能是动态数据。

response: '{"key11":"${random:id:6}","key2":"${random:str:10}","count":3,"person":[{"id":${hook:id}},"name":"张三"},{"id":2,"name":"李四"}],"object":{"id":1,"msg":"对象里的对象"}}'

"key2":"${random:str:10}",就是生成一个10位的随机字符串。

"key11":"${random:id:6}",生成一个6位的随机数字

"person":[{"id":${hook:id}},回调数据。

5.1 返回结果文件对应的实体类

返回结果文件,

spring 实现mock挡板 springboot mock_java_13

对应的实体类MockDataInfo 

package com.example.mockserver.model;

import lombok.Data;

import java.util.List;
@Data
public class MockDataInfo {
    private String mappingHost;
    private String response;
    private List<MappingParamsEntity> mappingParams;
    private Long timeout;
    private String realUrl;
    
}

5.2 最终返回结果信息的封装MockContext

MockContext

package com.example.mockserver.model;

import com.example.mockserver.consts.MockConst;
import lombok.Builder;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Data
@Builder
public class MockContext {
    // 用户传入信息
    // 一次用户请求,对应一个MockContext
    private String requestURI;
    private Map<String,String> requestParams;
    private String requestIp;

    // 返回结果的List
    // 一个接口,对应的返回结果,是一个List
    private List<MockDataInfo> mockDataInfoList;
    private String finalResponse;
    private Long timeout;
    private boolean timeoutSet;
    private String realUrl;
    private boolean realUrlSet;

    public void setRealUrl(String realUrl) {
        if(StringUtils.isNotEmpty(realUrl)){
            this.realUrl = realUrl;
            this.realUrlSet = true;
        }
    }

    public void setTimeout(Long timeout){
        if(timeout != null && timeout >0 ){
            this.timeout = timeout;
            timeoutSet = true;
        }
    }

    // 根据uri,得到文件名
    // /get/order/info -> get_order_info
    // 去掉第一个/ ,取后面的字符串
    public String getFileName(){
        String str = StringUtils.substringAfter(this.requestURI, "/");
        String fileName = StringUtils.replace(str, "/", "_");
        return fileName;
    }

    // 得到文件的路径
    public String getFilePath(){
        String filePath = MockConst.MOCK_DATA_PATH+"/"+this.getFileName();
        return filePath;
    }
    // 将用户传参,组成一个k=v 的List
    public List<String> getParamStringList(){
        // 计算权重的方法
        // 用户的传参,mockContext.getRequestParams(),是一个Map
        // 将用户的传参Map,转换成list。
        // 如 k:v, id:123,name:zhangsan 转换成 【"id=123","name=zhangsan"]
        List<String> paramStrList = this.getRequestParams().entrySet().stream()
                .map(e -> e.getKey() + "=" + e.getValue())
                .collect(Collectors.toList());
        return paramStrList;
    }


}

 当我们一个请求,可能有多个返回结果时,就对应多个结果文件。

spring 实现mock挡板 springboot mock_前端_14

 经过一系列的逻辑判断,我们最终只能返回其中一个文件。

spring 实现mock挡板 springboot mock_前端_15

 MockContext 中,针对一个请求,最终只对应一个文件。

private List<MockDataInfo> mockDataInfoList; 就是存在的这个请求,所有的返回结果。

经过一些列逻辑判断,我们最终只能应用其中一个文件中的数据。


private String finalResponse; private Long timeout; private boolean timeoutSet; private String realUrl; private boolean realUrlSet;


这些都是经过逻辑判断后,最终应用的数据。

最终的返回结果finalResponse,最终的超时时间private Long timeout;,

最终的透传地址:private String realUrl;

5.3 读取结果文件,封装到实体类YamlUtil

我们是如何读取结果文件,将数据封装到MockDataInfo实体类中的呢?

YamlUtil

package com.example.mockserver.util;

import com.example.mockserver.model.MockDataInfo;
import org.yaml.snakeyaml.Yaml;

import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class YamlUtil {
    // 这个工具的作用就是,读取yaml文件,将yaml文件里的内容转成一个实体类
    // 穿参path,yam文件的路径
    // 穿参Class<T> cls,要被转成的实体类
    public static <T> T readForObject(String path,Class<T> cls){
        try {
            Yaml yaml = new Yaml();
            // loadAs传参1是文件的流,传参2是要转换成哪个类的对象
            T t = yaml.loadAs(new FileInputStream(path), cls);
            return t;
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            throw new IllegalArgumentException(e);
        }
    }

    public static void main(String[] args) {
        MockDataInfo mockDataInfo = readForObject("/Users/zhaohui/IdeaProjects/mock-server/src/main/resources/mock_data/get_user/aaa", MockDataInfo.class);
        System.out.println("mockDataInfo = " + mockDataInfo);
    }
}

我们借助yaml.loadAs,读取文件,然后将文件结果封装到实体类中。

六、根据用户请求,读取对应的结果文件(根据URI拼接成文件的路径)

用户发起了请求

 访问:http://127.0.0.1:8081/get/user?name=lisi

我们如何根据这个请求,去找到对应的文件?

根据URI拼接成文件的路径

 将所有的数据都存放在了mock_data 的这个文件夹里面。

spring 实现mock挡板 springboot mock_java

返回结果有2种情况。

情况1,就是这个接口请求,只有一种返回结果。那我们只需要有一个txt文件与之对应就好了。

如文件get_order_info 。

情况2,这个接口请求,根据请求传参不同,我们需要返回对应结果。则需要建立一个文件夹,将这个请求的各种返回结果全部存放在这个文件夹下。

我的项目中mock_data文件的路径为:

/Users/zhaohui/IdeaProjects/mock-server/src/main/resources/mock_data

 将请求的URI拼成返回结果的文件/文件夹路径

我们将服务部署在本地,则mock平台的host为127.0.0.1

将用户请求中的uri拼成一个文件/文件夹的名称,去匹配对应的结果。

如上图中,我们返回结果有文件也有文件。

用户请求

http:127.0.0.1:8080/get/user?id=123&name=zhangsan

获取请求的URI, /get/user

去掉第一个/,将后面的/替换成_,然后拼成一个文件/文件夹的名称。

 /get/user ==> get_user

将得到的名称在前边拼接上数据存放的目录,(拼接上mock_data文件的路径)

/Users/zhaohui/IdeaProjects/mock-server/src/main/resources/mock_data/get_user

这个路径,是我们通过用户请求的URI拼接出来的,我们再去对应的位置,找这个文件。

在真实的路径/Users/zhaohui/IdeaProjects/mock-server/src/main/resources/mock_data/get_user

下,判断get_user 是一个文件夹,则我们取文件夹下的某个一文件作为返回结果进行返回。

用户请求,http:127.0.0.1:8080/get/order_info?id=123&name=zhangsan

获取请求的URI, /get/order/info,转换成文件/文件夹的名称get_order_info

再拼接上mock_data文件的路径,则我们通过用户请求得到的路径为

/Users/zhaohui/IdeaProjects/mock-server/src/main/resources/mock_data/get_order_info

我们再去真实的这个路径下,去获取这个文件。判断得到这个路径是一个文件,则我们直接将这个文件里的内容作为返回结果。

6.1 责任链设计模式处理文件&文件夹

根据用户请求,拼接的路径,有可能是一个文件,或者是一个文件夹。正常处理的逻辑就是

if 文件,一个处理逻辑,ifelse 文件夹一个处理逻辑。

这里我们使用责任链设计模式来代替if ..else

spring 实现mock挡板 springboot mock_spring boot_17

 AbstractHandler 责任链的处理模版

package com.example.mockserver.chain;

import com.example.mockserver.model.MockContext;
import lombok.Setter;

import java.io.IOException;

@Setter
public abstract class AbstractHandler<T,R> {
    // 属性是下一节链条
    private AbstractHandler<T,R>  nextHandler;

    // 当前链条是否能处理
    protected abstract boolean preHandle(T t);

    // 具体处理的逻辑
    protected abstract R onHandle(T t) throws Exception;

    // 总的模版处理逻辑
    public R doHandle(T t){
        // 能处理,直接处理
        if (preHandle(t)){
            try {
                return onHandle(t);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 下一节链条处理
        if (nextHandler != null){
            return nextHandler.doHandle(t);
        }
        // 所有链条都不能处理,抛出异常
        throw new RuntimeException("责任链中,所有链条都不能处理");
    }
}

责任链的Manager,ChainManager

package com.example.mockserver.chain;

import com.example.mockserver.model.MockContext;

public class ChainManager {
    // 属性就是链条的头
    private AbstractHandler<MockContext,String> handler;
    // 构造器,私有,不能被new
    private ChainManager(){
        // 构造器,给属性赋值。链条的头
        this.handler = initHandler();
    }

    private AbstractHandler<MockContext, String> initHandler() {
        // 串成链条,返回头
        FileHandler fileHandler = new FileHandler();
        DirectoryHandle directoryHandle = new DirectoryHandle();
        fileHandler.setNextHandler(directoryHandle);
        return fileHandler;

    }

    // ClassHolder属于静态内部类,在加载类Demo03的时候,只会加载内部类ClassHolder,
    // 但是不会把内部类的属性加载出来
    private static class ClassHolder{
        // 这里执行类加载,是jvm来执行类加载,它一定是单例的,不存在线程安全问题
        // 这里不是调用,是类加载,是成员变量
        private static final ChainManager holder =new ChainManager();

    }

    public static ChainManager of(){//第一次调用getInstance()的时候赋值
        return ClassHolder.holder;
    }

    // 处理数据
    public String doMapping(MockContext mockContext){
        return handler.doHandle(mockContext);
    }



}

DirectoryHandle 文件夹的处理方式

package com.example.mockserver.chain;

import cn.hutool.core.io.FileUtil;
import com.example.mockserver.model.MockContext;
import com.example.mockserver.observer.ObserverManager;

public class DirectoryHandle extends AbstractHandler<MockContext,String> {
    @Override
    protected boolean preHandle(MockContext mockContext) {
        // 判断是否是目录
        return FileUtil.isDirectory(mockContext.getFilePath());
    }

    @Override
    protected String onHandle(MockContext mockContext) throws Exception {
        return ObserverManager.of().getMockData(mockContext);
    }
}

 FileHandler 文件的处理方式

package com.example.mockserver.chain;

import cn.hutool.core.io.FileUtil;
import com.example.mockserver.model.MockContext;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;

public class FileHandler extends AbstractHandler<MockContext,String>{
    @Override
    protected boolean preHandle(MockContext mockContext) {
        return FileUtil.isFile(mockContext.getFilePath());
    }

    @Override
    protected String onHandle(MockContext mockContext) throws Exception {
        return FileUtils.readFileToString(new File(mockContext.getFilePath()),"utf-8");
    }
}

七、观察者模式对mock数据的处理

7.1 MockContext  mock内容类的作用

我们定义了MockContext 类,这个类包含了用户请求的信息,也包含了返回结果的信息。

定义这个类,是比较巧妙的。

当我们对数据进行mock处理时,

1、加载本地mock文件,转成我们需要的实体类。(处理完后返回MockContext)

2、基于请求的参数,计算权重。(处理完后返回MockContext)

3、透传处理。(处理完后返回MockContext)

4、对返回结果的response 数据进行处理(处理完后返回MockContext)

5、hook 处理(处理完后返回MockContext)

6、对请求的超时mock(处理完后返回MockContext)

我们对请求数据及返回结果,做了很多的处理。每次处理完成后,我们都把更新的数据放到

MockContext中,然后再拿着这个MockContext,给下一个逻辑处理。每个逻辑处理完,都把数据更新在MockContext中。

7.2 观察者模式MockContext 处理

我们要对mock数据进行各种逻辑的处理,每个逻辑处理完,都把数据更新在MockContext,给下一个逻辑处理。这里使用观察者的设计模式。

spring 实现mock挡板 springboot mock_spring 实现mock挡板_18

IObserver

package com.example.mockserver.observer;

import com.example.mockserver.model.MockContext;

public interface IObserver<T> {
    void update(T t);
}

ObserverManager

将各种处理逻辑,串成一个链条。

package com.example.mockserver.observer;

import com.example.mockserver.model.MockContext;
import com.google.common.collect.Lists;

import java.util.List;

public class ObserverManager {
    // 属性就是List。观察者就是遍历List处理同一个数据
    private List<IObserver<MockContext>> observers;
    // 构造器,私有,不能被new
    private ObserverManager(){
        // 构造器,构造这个属性List
        // 这是一个工具实体类的表列
        observers = Lists.newArrayList(
                new LoadMockFileObserver(),// 1、加载本地mock文件,转成我们需要的实体类
                new CalcWeightObserver(), // 2 基于请求的参数,计算权重
                new RealObserver(),     // 插入一个透传
                new PackObserver(),  // 3 处理数据
                new HookResponseObserver(), // 4 hook
                new TimeOutObserver()  // 超时
        );
    }

    // ClassHolder属于静态内部类,在加载类Demo03的时候,只会加载内部类ClassHolder,
    // 但是不会把内部类的属性加载出来
    private static class ClassHolder{
        // 这里执行类加载,是jvm来执行类加载,它一定是单例的,不存在线程安全问题
        // 这里不是调用,是类加载,是成员变量
        private static final ObserverManager holder =new ObserverManager();

    }

    public static ObserverManager of(){//第一次调用getInstance()的时候赋值
        return ClassHolder.holder;
    }

    // 处理数据的方法
    public String getMockData(MockContext mockContext){
        for (IObserver observer:this.observers){
            // 每一个observer,处理mockContext ,都是没有返回值的
            // 我们把所有的结果处理结果,都回写进了mockContext
            // 这里用的for循环,我们处理的是同一个mockContext,修改的变量得以保存
            observer.update(mockContext);
        }
        return mockContext.getFinalResponse();
    }



}

 1、先读取接口对应所有的文件,转成一个实体类的List

2、再计算List里,每一个对象的权重大小,取出权重最大的结果。

3、将最后的结果动态变量进行替换。

IObserver

package com.example.mockserver.observer;

import com.example.mockserver.model.MockContext;

public interface IObserver<T> {
    void update(T t);
}

LoadMockFileObserver加载本地mock文件,转成我们需要的实体类List

package com.example.mockserver.observer;

import com.example.mockserver.model.MockContext;
import com.example.mockserver.model.MockDataInfo;
import com.example.mockserver.util.YamlUtil;

import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 加载本地mock文件,转成我们需要的实体类List
 */
public class LoadMockFileObserver implements IObserver<MockContext>{
    @Override
    public void update(MockContext mockContext) {
        // 根据请求的目录,获取目录下所有的文件
        File[] files = new File(mockContext.getFilePath()).listFiles();
        List<MockDataInfo> mockDataInfoList = Arrays.stream(files)
                // 转换,把每一个文件转成对象MockDataInfo
                .map(f -> YamlUtil.readForObject(f.getAbsolutePath(), MockDataInfo.class))
                // 将数组,转成List
                .collect(Collectors.toList());

        // 将一个接口,对应的所有返回信息List ,回写进MockContext
        mockContext.setMockDataInfoList(mockDataInfoList);
    }
}

计算一个接口对应的所有文件(List对象)的权重,返回权重大的结果

CalcWeightObserver

package com.example.mockserver.observer;

import com.example.mockserver.model.MappingParamsEntity;
import com.example.mockserver.model.MockContext;
import com.example.mockserver.model.MockDataInfo;
import com.example.mockserver.util.YamlUtil;

import java.io.File;
import java.util.List;

/**
 * 计算一个接口对应的所有文件(List对象)的权重,返回权重大的结果
 */
public class CalcWeightObserver implements IObserver<MockContext>{
    @Override
    public void update(MockContext mockContext) {

        // 定义最终的权重结果 和最终的response
        int weightResult = 0;
        String response = "";
        for(MockDataInfo mockDataInfo: mockContext.getMockDataInfoList()){

            // 取出实体类的参数,dd得到当前对象的参数list
            List<MappingParamsEntity> mappingParams = mockDataInfo.getMappingParams();
            int weight = 0;
            for (MappingParamsEntity mappingParamsEntity:mappingParams){
                // 将参数转成k=v
                String paramStr = mappingParamsEntity.getParams().entrySet().stream()
                        .map(e -> e.getKey()+"="+e.getValue() )
                        .findFirst().get(); // 我们这里的Map,只有一个值

                // 判断 我们yml文件里指定的参数策略,在不在用户传参url的参数列表里。
                if(mockContext.getParamStringList().contains(paramStr)){
                    // 如果在,则累计权重
                    weight = weight + mappingParamsEntity.getWeight();
                }
            }

            // 每一个文件的权重比较大小,最终返回权重最大的response
            if(weight>weightResult){
                weightResult = weight;
                response = mockDataInfo.getResponse();
            }

        }

        mockContext.setFinalResponse(response);


    }
}



替换动态变量PackObserver

字符串

response: '{"key11":"${random:id:6}","key2":"${random:str:10}","count":3,"person":[{"id":1,"name":"张三"},{"id":2,"name":"李四"}],"object":{"id":1,"msg":"对象里的对象"}}'

将${random:id:6} 替换成6位的数字,将${random:str:10} 替换成10位的字符串。



package com.example.mockserver.observer;

import com.example.mockserver.decorator.DecoratorManager;
import com.example.mockserver.model.MockContext;
import com.example.mockserver.util.RandomUtil;
import org.apache.commons.lang3.StringUtils;

public class PackObserver implements IObserver<MockContext> {
//    @Override
//    public void update(MockContext mockContext) {
//        String finalResponse = mockContext.getFinalResponse();
//        // random -> 随机字符
//        String packResponse  =  StringUtils.replace(finalResponse,"${random}", RandomUtil.random());
//        mockContext.setFinalResponse(packResponse);
//
//    }

    @Override
    public void update(MockContext mockContext) {
        String finalResponse = mockContext.getFinalResponse();
        // random -> 随机字符
        String packResponse  = DecoratorManager.of().doPack(finalResponse);
        mockContext.setFinalResponse(packResponse);

    }


}

ObserverManager

package com.example.mockserver.observer;

import com.example.mockserver.model.MockContext;
import com.google.common.collect.Lists;

import java.util.List;

public class ObserverManager {
    // 属性就是List。观察者就是遍历List处理同一个数据
    private List<IObserver<MockContext>> observers;
    // 构造器,私有,不能被new
    private ObserverManager(){
        // 构造器,构造这个属性List
        // 这是一个工具实体类的表列
        observers = Lists.newArrayList(
                new LoadMockFileObserver(),// 1、加载本地mock文件,转成我们需要的实体类
                new CalcWeightObserver(), // 2 基于请求的参数,计算权重
                new PackObserver()  // 3 处理数据


        );
    }

    // ClassHolder属于静态内部类,在加载类Demo03的时候,只会加载内部类ClassHolder,
    // 但是不会把内部类的属性加载出来
    private static class ClassHolder{
        // 这里执行类加载,是jvm来执行类加载,它一定是单例的,不存在线程安全问题
        // 这里不是调用,是类加载,是成员变量
        private static final ObserverManager holder =new ObserverManager();

    }

    public static ObserverManager of(){//第一次调用getInstance()的时候赋值
        return ClassHolder.holder;
    }

    // 处理数据的方法
    public String getMockData(MockContext mockContext){
        for (IObserver observer:this.observers){
            // 每一个observer,处理mockContext ,都是没有返回值的
            // 我们把所有的结果处理结果,都回写进了mockContext
            // 这里用的for循环,我们处理的是同一个mockContext,修改的变量得以保存
            observer.update(mockContext);
        }
        return mockContext.getFinalResponse();
    }



}

再补充一下完善后的MockContext

package com.example.mockserver.model;

import com.example.mockserver.consts.MockConst;
import lombok.Builder;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Data
@Builder
public class MockContext {
    // 用户传入信息
    // 一次用户请求,对应一个MockContext
    private String requestURI;
    private Map<String,String> requestParams;
    private String requestIp;

    // 返回结果的List
    // 一个接口,对应的返回结果,是一个List
    private List<MockDataInfo> mockDataInfoList;
    private String finalResponse;

    // 根据uri,得到文件名
    // /get/order/info -> get_order_info
    // 去掉第一个/ ,取后面的字符串
    public String getFileName(){
        String str = StringUtils.substringAfter(this.requestURI, "/");
        String fileName = StringUtils.replace(str, "/", "_");
        return fileName;
    }

    // 得到文件的路径
    public String getFilePath(){
        String filePath = MockConst.MOCK_DATA_PATH+"/"+this.getFileName();
        return filePath;
    }
    // 将用户传参,组成一个k=v 的List
    public List<String> getParamStringList(){
        // 计算权重的方法
        // 用户的传参,mockContext.getRequestParams(),是一个Map
        // 将用户的传参Map,转换成list。
        // 如 k:v, id:123,name:zhangsan 转换成 【"id=123","name=zhangsan"]
        List<String> paramStrList = this.getRequestParams().entrySet().stream()
                .map(e -> e.getKey() + "=" + e.getValue())
                .collect(Collectors.toList());
        return paramStrList;
    }


}

八、 对文件&文件夹的处理逻辑

8.1 请求只有一个返回结果,对应一个文件

我们直接读取对应的文件就可以了。

spring 实现mock挡板 springboot mock_返回结果_19

8.2 当请求对应一个文件夹时 

调用我们上面的观察者模式来处理。进行数据匹配,找到文件夹中,与之对应的文件。

spring 实现mock挡板 springboot mock_spring 实现mock挡板_20

 九、匹配返回结果文件(权重计算)

当一个请求,返回结果对应一个文件夹时,我们需要在该文件夹中找到,与之匹配的文件。

我们需要计算这一个接口文件夹下,每个文件,用户命中的权重之和。然后返回权重最大的那一个。

当一个请求,

http:127.0.0.1:8080/get/user?id=123&name=zhangsan

有多个返回结果,如果根据传参不同,返回对应的结果?

spring 实现mock挡板 springboot mock_spring boot_02

如,get_user请求,对应多个返回结果。

在返回结果中文件中,我们标明对应的传参与这个参数的权重。取权重最大的。

spring 实现mock挡板 springboot mock_java_03

a文件:传参,id=123的权重为8,name=zhangsan的权重为10

spring 实现mock挡板 springboot mock_spring 实现mock挡板_04

 b文件:传参,id=456的权重为2,name=zhangsan的权重为4.

spring 实现mock挡板 springboot mock_返回结果_05

 则请求:

 http:127.0.0.1:8080/get/user?id=123&name=zhangsan

与a文件匹配,id=123与name=zhangsan都命中了,则a文件的权重为8+10=18

与b文件匹配,只命中了name=zhangsan,则b文件的权重为4。

在文件夹内,a匹配权重最大,我们取a的数据作为返回结果。

(其实就是将传参得到一个k=v的list,然后遍历文件,将每个文件里面的参数,都转成k=v的,在判断这个在不在list里面,在则权重相加)

先了解一下,我们这个实体类的组成。

 用户传参MockContext实体类:

 用户的实体类MockContext,传参requestParams 是一个Map。

我们先将Map转成List

Map

{
    "id":"123",
    "name":"zhangsan"
    }

转成list

["id"="123","name"="zhangsan"]
// 计算权重的方法
        // 用户的传参,mockContext.getRequestParams(),是一个Map
        // 将用户的传参Map,转换成list。
        // 如 k:v, id:123,name:zhangsan 转换成 【"id=123","name=zhangsan"]
        List<String> paramStrList = mockContext.getRequestParams().entrySet().stream()
                .map(e -> e.getKey() + "=" + e.getValue())
                .collect(Collectors.toList());

返回数据的实体类MockDataInfo:

spring 实现mock挡板 springboot mock_返回结果_25

所有的参数,是一个List。每个字段(包含权重)是一个小的实体类。实体类的map里只有一个值

1、取出实体类的参数,得到当前对象的参数list

List<MappingParamsEntity> mappingParams = mockDataInfo.getMappingParams();

2、遍历这个list,把每个元素,转成k=v的格式

String paramStr = mappingParamsEntity.getParams().entrySet().stream()
                        .map(e -> e.getKey()+"="+e.getValue() )
                        .findFirst().get(); // 我们这里的Map,只有一个值

 3、然后再判断这个k=v格式的元素,在不在用户传参的List里面,如果在里面,则权重相加。

// 判断 我们yml文件里指定的参数策略,在不在用户传参url的参数列表里。
                if(paramStrList.contains(paramStr)){
                    // 如果在,则累计权重
                    weight = weight + mappingParamsEntity.getWeight();
                }

整体实现代码

// 如果是文件夹,获取所有文件。是一个数组
        // 取出所有的文件
        File[] files = file.listFiles();
        // 定义最终的权重结果 和最终的response
        int weightResult = 0;
        String response = "";
        // 遍历所有的文件
        for(File f:files){
            // 循环,将每个文件都转成一个对象。把yml文件转成实体类
            MockDataInfo mockDataInfo = YamlUtil.readForObject(f.getAbsolutePath(), MockDataInfo.class);
            // 取出实体类的参数,得到当前对象的参数list
            List<MappingParamsEntity> mappingParams = mockDataInfo.getMappingParams();
            int weight = 0;
            //
            for (MappingParamsEntity mappingParamsEntity:mappingParams){
                // 将参数转成k=v
                String paramStr = mappingParamsEntity.getParams().entrySet().stream()
                        .map(e -> e.getKey()+"="+e.getValue() )
                        .findFirst().get(); // 我们这里的Map,只有一个值

                // 判断 我们yml文件里指定的参数策略,在不在用户传参url的参数列表里。
                if(paramStrList.contains(paramStr)){
                    // 如果在,则累计权重
                    weight = weight + mappingParamsEntity.getWeight();
                }
            }

            //
            if(weight>weightResult){
                weightResult = weight;
                response = mockDataInfo.getResponse();
            }

        }

十、 处理动态变量,使用了装饰器模式

在处理动态变量时,我们使用的观察者模式,PackObserver(),调用了

DecoratorManager.of().doPack(finalResponse);

这里具体对字符串进行处理的,用了装饰器模式 。

先处理数字,再处理字符串

1、先写基类的接口IDecorator

这里用了泛型

public interface IDecorator<T> {
    T decorate(T data);
}

2、装饰器的基类BaseResponseDecorator

package com.example.mockserver.decorator;

public abstract class BaseResponseDecorator<T> implements IDecorator<T>{
    private BaseResponseDecorator<T> decorator;

    // 构造器
    public BaseResponseDecorator(BaseResponseDecorator<T> decorator) {
        this.decorator = decorator;
    }

    // 自己装饰的方法,重写这个方法
    public abstract T onDecorator(T t);

    // 整体调用的逻辑
    public T decorate(T t){
        // 先判断,当前属性是否为空
        if(decorator != null){
            // 不为空,先让下一节decorator装饰
            t = decorator.decorate(t);
            // 再自己装饰一次,一共装饰了2次
            return onDecorator(t);
        }
        // 为空,就调用自己的装饰方法。只装饰一次
        return onDecorator(t);
    }

}

3、对数字的处理RandomIdDecorator

package com.example.mockserver.decorator;

import com.example.mockserver.util.RandomUtil;
import org.apache.commons.lang3.StringUtils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RandomIdDecorator extends BaseResponseDecorator<String>{

    private static final Pattern PATTERN = Pattern.compile("\\$\\{random:id:(\\d+?)\\}");
    // 构造器
    public RandomIdDecorator(BaseResponseDecorator<String> decorator) {
        super(decorator);
    }

    @Override
    public String onDecorator(String data) {
        Matcher matcher = PATTERN.matcher(data);
        while (matcher.find()){
            String replaceStr = matcher.group(0);
            int size = Integer.parseInt(matcher.group(1));
            // 替换
            data = StringUtils.replace(data,replaceStr, RandomUtil.randomNum(size));
        }
        return data;
    }

}

4、对字符串的处理RandomStrDecorator

package com.example.mockserver.decorator;

import com.example.mockserver.util.RandomUtil;
import org.apache.commons.lang3.StringUtils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RandomStrDecorator extends BaseResponseDecorator<String>{

    private static final Pattern PATTERN = Pattern.compile("\\$\\{random:str:(\\d+?)\\}");
    // 构造器
    public RandomStrDecorator(BaseResponseDecorator<String> decorator) {
        super(decorator);
    }

    @Override
    public String onDecorator(String data) {
        Matcher matcher = PATTERN.matcher(data);
        while (matcher.find()){
            String replaceStr = matcher.group(0);
            int size = Integer.parseInt(matcher.group(1));
            // 替换
            data = StringUtils.replace(data,replaceStr, RandomUtil.randomStr(size));
        }
        return data;
    }

}

5、manager   DecoratorManager

package com.example.mockserver.decorator;

public class DecoratorManager {
    // 属性
    private IDecorator<String> decorator;

    // 构造器,私有,不能被new
    private DecoratorManager(){
        decorator = new RandomIdDecorator(new RandomStrDecorator(null));
    }

    // ClassHolder属于静态内部类,在加载类Demo03的时候,只会加载内部类ClassHolder,
    // 但是不会把内部类的属性加载出来
    private static class ClassHolder{
        // 这里执行类加载,是jvm来执行类加载,它一定是单例的,不存在线程安全问题
        // 这里不是调用,是类加载,是成员变量
        private static final DecoratorManager holder =new DecoratorManager();

    }

    public static DecoratorManager of(){//第一次调用getInstance()的时候赋值
        return ClassHolder.holder;
    }

    public String doPack(String response){
        return decorator.decorate(response);
    }


}

6、调用

String packResponse  = DecoratorManager.of().doPack(finalResponse);

十一、hook参数处理

/**
 * 建设点:请求数据 hook
 * 场景:比如请求的时候,请求参数携带一个requestId, 然后requestId本身还是个变化的,也是随机的。
 *      然后在返回的时候,要把这个id带回去,即:虽然返回数据不能写死,但是你也不能自己生成,需要使用请求的参数
 * {"key1":"${random:id:6}","key2":"${random:str:10}","count":3,"person":[{"id":${hook:userId},"name":"张三"},{"id":2,"name":"李四"}],"object":{"id":1,"msg":"对象里的对象"}}
 * ${hook:userId}   =>   userId
 * 实现思路:
 *      1. 要保证request params 有我们需要的参数
 *      2. 设定标记 ${hook:userId}
 *      3. 通过正则 获取参数名
 *      4. 从request params  获取参数值
 *      5. 完成替换
 *      6. 回写
 */

我们这里讲解两种方式

11.1 方式一:直接对response数据进行处理

HookResponseObserver0 

package com.example.mockserver.observer;

import com.example.mockserver.model.MockContext;
import org.apache.commons.lang3.StringUtils;

import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 建设点:请求数据 hook
 * 场景:比如请求的时候,请求参数携带一个requestId, 然后requestId本身还是个变化的,也是随机的。
 *      然后在返回的时候,要把这个id带回去,即:虽然返回数据不能写死,但是你也不能自己生成,需要使用请求的参数
 * {"key1":"${random:id:6}","key2":"${random:str:10}","count":3,"person":[{"id":${hook:userId},"name":"张三"},{"id":2,"name":"李四"}],"object":{"id":1,"msg":"对象里的对象"}}
 * ${hook:userId}   =>   userId
 * 实现思路:
 *      1. 要保证request params 有我们需要的参数
 *      2. 设定标记 ${hook:userId}
 *      3. 通过正则 获取参数名
 *      4. 从request params  获取参数值
 *      5. 完成替换
 *      6. 回写
 */
public class HookResponseObserver0 implements IObserver<MockContext> {
    private static final Pattern PATTERN = Pattern.compile("\\$\\{hook:(.*?)\\}");
    @Override
    public void update(MockContext mockContext) {
        // 拿到返回结果
        String finalResponse = mockContext.getFinalResponse();
        // 拿到请求参数
        Map<String, String> requestParams = mockContext.getRequestParams();

        Matcher matcher = PATTERN.matcher(finalResponse);
        while ((matcher.find())){
            String replaceStr = matcher.group(0);
            String paramName = matcher.group(1);
            // 如果用户传参数里不包含要替换的值,直接返回
            if(!requestParams.containsKey(paramName)){
                break;
            }
            String value = requestParams.get(paramName);
            finalResponse = StringUtils.replace(finalResponse,replaceStr,value);
        }
        // 回写数据
        mockContext.setFinalResponse(finalResponse);

    }
}

再把这个HookResponseObserver0  串到观察者的链中就可以了。

11.2 方式二:采用装饰器模式对response数据进行处理

在当前业务场景中,我们只遇到了一种hook数据的处理。当多种hook数据处理时,不太容易扩展。

我们采用装饰器的设计模式,方便扩展,可以处理更多种情况的hook数据。

具体的处理逻辑CommonHookDecorator

package com.example.mockserver.decorator;

import com.example.mockserver.model.HookContext;
import org.apache.commons.lang3.StringUtils;

import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CommonHookDecorator extends BaseResponseDecorator<HookContext>{
    private static final Pattern PATTERN = Pattern.compile("\\$\\{hook:(.*?)\\}");

    public CommonHookDecorator(BaseResponseDecorator<HookContext> decorator) {
        super(decorator);
    }

    @Override
    public HookContext onDecorator(HookContext hookContext) {
        // 拿到返回结果
        String finalResponse = hookContext.getFinalResponse();
        // 拿到请求参数
        Map<String, String> requestParams = hookContext.getRequestParams();

        Matcher matcher = PATTERN.matcher(finalResponse);
        while ((matcher.find())){
            String replaceStr = matcher.group(0);
            String paramName = matcher.group(1);
            // 如果用户传参数里不包含要替换的值,直接返回
            if(!requestParams.containsKey(paramName)){
                break;
            }
            String value = requestParams.get(paramName);
            finalResponse = StringUtils.replace(finalResponse,replaceStr,value);
        }
        // 回写数据
        hookContext.setFinalResponse(finalResponse);
        return hookContext;

    }


}

DecoratorManager 在装饰器链条中,加入hook装饰器

package com.example.mockserver.decorator;

import com.example.mockserver.model.HookContext;

public class DecoratorManager {
    // 属性
    private IDecorator<String> packDecorator;
    private IDecorator<HookContext> hookDecorator;


    // 构造器,私有,不能被new
    private DecoratorManager(){
        packDecorator = new RandomIdDecorator(new RandomStrDecorator(null));
        // 这个hook链条,目前只有一个
        hookDecorator = new CommonHookDecorator(null);

    }

    // ClassHolder属于静态内部类,在加载类Demo03的时候,只会加载内部类ClassHolder,
    // 但是不会把内部类的属性加载出来
    private static class ClassHolder{
        // 这里执行类加载,是jvm来执行类加载,它一定是单例的,不存在线程安全问题
        // 这里不是调用,是类加载,是成员变量
        private static final DecoratorManager holder =new DecoratorManager();

    }

    public static DecoratorManager of(){//第一次调用getInstance()的时候赋值
        return ClassHolder.holder;
    }

    public String doPack(String response){
        return packDecorator.decorate(response);
    }

    public HookContext doHook(HookContext hookContext){
        return this.hookDecorator.decorate(hookContext);
    }


}

把hook 装饰器,加入到观察者模式中

HookResponseObserver

package com.example.mockserver.observer;

import com.example.mockserver.decorator.DecoratorManager;
import com.example.mockserver.model.HookContext;
import com.example.mockserver.model.MockContext;
import org.apache.commons.lang3.StringUtils;

import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 建设点:请求数据 hook
 * 场景:比如请求的时候,请求参数携带一个requestId, 然后requestId本身还是个变化的,也是随机的。
 *      然后在返回的时候,要把这个id带回去,即:虽然返回数据不能写死,但是你也不能自己生成,需要使用请求的参数
 * {"key1":"${random:id:6}","key2":"${random:str:10}","count":3,"person":[{"id":${hook:userId},"name":"张三"},{"id":2,"name":"李四"}],"object":{"id":1,"msg":"对象里的对象"}}
 * ${hook:userId}   =>   userId
 * 实现思路:
 *      1. 要保证request params 有我们需要的参数
 *      2. 设定标记 ${hook:userId}
 *      3. 通过正则 获取参数名
 *      4. 从request params  获取参数值
 *      5. 完成替换
 *      6. 回写
 */
public class HookResponseObserver implements IObserver<MockContext> {
    private static final Pattern PATTERN = Pattern.compile("\\$\\{hook:(.*?)\\}");
    @Override
    public void update(MockContext mockContext) {
        HookContext hookContext = HookContext.builder()
                .finalResponse(mockContext.getFinalResponse())
                .requestParams(mockContext.getRequestParams())
                .build();
        hookContext = DecoratorManager.of().doHook(hookContext);
        // 再回血mockContext
        mockContext.setFinalResponse(hookContext.getFinalResponse());


    }
}

将hook观察者加入到链条中ObserverManager

十二、依赖

springframework.boot 用的2.4.4版本

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>AutoApi</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>AutoApi</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
            <version>1.26</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>30.1.1-jre</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.11</version>
        </dependency>
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.9.0</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.6</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-yaml</artifactId>
            <version>2.12.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.3.8</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.20</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.6</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-core</artifactId>
            <version>5.7.5</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

十二、启动项目

---------启动项目

访问:

 http://127.0.0.1:8081/get/user?name=zhangsan&id=123

返回结果:

{"key11":"931604","key2":"CsmBVUDAXu","count":3,"person":[{"id":1,"name":"张三"},{"id":2,"name":"李四"}],"object":{"id":1,"msg":"对象里的对象"}}