当前位置:   article > 正文

测开工具:spring boot 实现mock平台_springboot mock

springboot mock

目录

一、实现功能

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 的这个文件夹里面。

返回结果有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

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

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

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

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

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

 则请求:

 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

三、整体架构&实现思路

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

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

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

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

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

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

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

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

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

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

  1. package com.example.mockserver.controller;
  2. import cn.hutool.core.io.FileUtil;
  3. import com.example.mockserver.model.MappingParamsEntity;
  4. import com.example.mockserver.model.MockContext;
  5. import com.example.mockserver.model.MockDataInfo;
  6. import com.example.mockserver.service.MockService;
  7. import com.example.mockserver.util.ArrayUtil;
  8. import com.example.mockserver.util.YamlUtil;
  9. import lombok.extern.slf4j.Slf4j;
  10. import org.apache.commons.io.FileUtils;
  11. import org.springframework.beans.factory.annotation.Autowired;
  12. import org.springframework.web.bind.annotation.RequestMapping;
  13. import org.springframework.web.bind.annotation.RestController;
  14. import javax.servlet.http.HttpServletRequest;
  15. import java.io.File;
  16. import java.io.IOException;
  17. import java.util.List;
  18. import java.util.Map;
  19. import java.util.stream.Collectors;
  20. @RestController
  21. @Slf4j
  22. public class MockController {
  23. @Autowired
  24. private HttpServletRequest request;
  25. @Autowired
  26. private MockService mockService;
  27. @RequestMapping("/**")
  28. public String doMock() throws IOException {
  29. log.info("请求的URI---------:"+request.getRequestURI());
  30. log.info("请求IP---------:"+request.getRemoteAddr());
  31. log.info("请求的参数---------:"+request.getParameterMap());
  32. // 将获取的用户数据 ip 参数 URI ,存储到 mockContext 这个类里
  33. MockContext mockContext = MockContext.builder()
  34. .requestIp(request.getRemoteAddr()) // 获取ip
  35. .requestParams(getParams(request.getParameterMap()))
  36. .requestURI(request.getRequestURI()) // 获取请求的URI
  37. .build();
  38. String response = mockService.doMock(mockContext);
  39. return response ;
  40. }
  41. // 获取用户的传参,value是一个数组。这里为了将来处理方便,我们将这数组转成一个字符串。
  42. // 我们默认,这个数据的长度是1,那我们只需要取出来数组的第一个值就可以了。
  43. public Map<String,String> getParams(Map<String,String[]> parameterMap){
  44. Map<String,String> params = parameterMap.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(),e -> ArrayUtil.getFirst(e.getValue())));
  45. return params;
  46. }
  47. }

1、拦截所有用户请求

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

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

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

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

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

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

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

  1. package com.example.mockserver.model;
  2. import com.example.mockserver.consts.MockConst;
  3. import lombok.Builder;
  4. import lombok.Data;
  5. import org.apache.commons.lang3.StringUtils;
  6. import java.util.List;
  7. import java.util.Map;
  8. import java.util.stream.Collectors;
  9. @Data
  10. @Builder
  11. public class MockContext {
  12. // 用户传入信息
  13. // 一次用户请求,对应一个MockContext
  14. private String requestURI;
  15. private Map<String,String> requestParams;
  16. private String requestIp;
  17. // 返回结果的List
  18. // 一个接口,对应的返回结果,是一个List
  19. private List<MockDataInfo> mockDataInfoList;
  20. private String finalResponse;
  21. private Long timeout;
  22. private boolean timeoutSet;
  23. private String realUrl;
  24. private boolean realUrlSet;
  25. public void setRealUrl(String realUrl) {
  26. if(StringUtils.isNotEmpty(realUrl)){
  27. this.realUrl = realUrl;
  28. this.realUrlSet = true;
  29. }
  30. }
  31. public void setTimeout(Long timeout){
  32. if(timeout != null && timeout >0 ){
  33. this.timeout = timeout;
  34. timeoutSet = true;
  35. }
  36. }
  37. // 根据uri,得到文件名
  38. // /get/order/info -> get_order_info
  39. // 去掉第一个/ ,取后面的字符串
  40. public String getFileName(){
  41. String str = StringUtils.substringAfter(this.requestURI, "/");
  42. String fileName = StringUtils.replace(str, "/", "_");
  43. return fileName;
  44. }
  45. // 得到文件的路径
  46. public String getFilePath(){
  47. String filePath = MockConst.MOCK_DATA_PATH+"/"+this.getFileName();
  48. return filePath;
  49. }
  50. // 将用户传参,组成一个k=v 的List
  51. public List<String> getParamStringList(){
  52. // 计算权重的方法
  53. // 用户的传参,mockContext.getRequestParams(),是一个Map
  54. // 将用户的传参Map,转换成list。
  55. // 如 k:v, id:123,name:zhangsan 转换成 【"id=123","name=zhangsan"]
  56. List<String> paramStrList = this.getRequestParams().entrySet().stream()
  57. .map(e -> e.getKey() + "=" + e.getValue())
  58. .collect(Collectors.toList());
  59. return paramStrList;
  60. }
  61. }

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

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

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

 查看aaa文件

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

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

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。

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

 一个请求,有多个参数。每个参数作为一个对象。所有的请求作为一个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 返回结果文件对应的实体类

返回结果文件,

对应的实体类MockDataInfo 

  1. package com.example.mockserver.model;
  2. import lombok.Data;
  3. import java.util.List;
  4. @Data
  5. public class MockDataInfo {
  6. private String mappingHost;
  7. private String response;
  8. private List<MappingParamsEntity> mappingParams;
  9. private Long timeout;
  10. private String realUrl;
  11. }

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

MockContext

  1. package com.example.mockserver.model;
  2. import com.example.mockserver.consts.MockConst;
  3. import lombok.Builder;
  4. import lombok.Data;
  5. import org.apache.commons.lang3.StringUtils;
  6. import java.util.List;
  7. import java.util.Map;
  8. import java.util.stream.Collectors;
  9. @Data
  10. @Builder
  11. public class MockContext {
  12. // 用户传入信息
  13. // 一次用户请求,对应一个MockContext
  14. private String requestURI;
  15. private Map<String,String> requestParams;
  16. private String requestIp;
  17. // 返回结果的List
  18. // 一个接口,对应的返回结果,是一个List
  19. private List<MockDataInfo> mockDataInfoList;
  20. private String finalResponse;
  21. private Long timeout;
  22. private boolean timeoutSet;
  23. private String realUrl;
  24. private boolean realUrlSet;
  25. public void setRealUrl(String realUrl) {
  26. if(StringUtils.isNotEmpty(realUrl)){
  27. this.realUrl = realUrl;
  28. this.realUrlSet = true;
  29. }
  30. }
  31. public void setTimeout(Long timeout){
  32. if(timeout != null && timeout >0 ){
  33. this.timeout = timeout;
  34. timeoutSet = true;
  35. }
  36. }
  37. // 根据uri,得到文件名
  38. // /get/order/info -> get_order_info
  39. // 去掉第一个/ ,取后面的字符串
  40. public String getFileName(){
  41. String str = StringUtils.substringAfter(this.requestURI, "/");
  42. String fileName = StringUtils.replace(str, "/", "_");
  43. return fileName;
  44. }
  45. // 得到文件的路径
  46. public String getFilePath(){
  47. String filePath = MockConst.MOCK_DATA_PATH+"/"+this.getFileName();
  48. return filePath;
  49. }
  50. // 将用户传参,组成一个k=v 的List
  51. public List<String> getParamStringList(){
  52. // 计算权重的方法
  53. // 用户的传参,mockContext.getRequestParams(),是一个Map
  54. // 将用户的传参Map,转换成list。
  55. // 如 k:v, id:123,name:zhangsan 转换成 【"id=123","name=zhangsan"]
  56. List<String> paramStrList = this.getRequestParams().entrySet().stream()
  57. .map(e -> e.getKey() + "=" + e.getValue())
  58. .collect(Collectors.toList());
  59. return paramStrList;
  60. }
  61. }

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

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

 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

  1. package com.example.mockserver.util;
  2. import com.example.mockserver.model.MockDataInfo;
  3. import org.yaml.snakeyaml.Yaml;
  4. import java.io.FileInputStream;
  5. import java.io.FileNotFoundException;
  6. public class YamlUtil {
  7. // 这个工具的作用就是,读取yaml文件,将yaml文件里的内容转成一个实体类
  8. // 穿参path,yam文件的路径
  9. // 穿参Class<T> cls,要被转成的实体类
  10. public static <T> T readForObject(String path,Class<T> cls){
  11. try {
  12. Yaml yaml = new Yaml();
  13. // loadAs传参1是文件的流,传参2是要转换成哪个类的对象
  14. T t = yaml.loadAs(new FileInputStream(path), cls);
  15. return t;
  16. } catch (FileNotFoundException e) {
  17. e.printStackTrace();
  18. throw new IllegalArgumentException(e);
  19. }
  20. }
  21. public static void main(String[] args) {
  22. MockDataInfo mockDataInfo = readForObject("/Users/zhaohui/IdeaProjects/mock-server/src/main/resources/mock_data/get_user/aaa", MockDataInfo.class);
  23. System.out.println("mockDataInfo = " + mockDataInfo);
  24. }
  25. }

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

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

用户发起了请求

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

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

根据URI拼接成文件的路径

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

返回结果有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

 AbstractHandler 责任链的处理模版

  1. package com.example.mockserver.chain;
  2. import com.example.mockserver.model.MockContext;
  3. import lombok.Setter;
  4. import java.io.IOException;
  5. @Setter
  6. public abstract class AbstractHandler<T,R> {
  7. // 属性是下一节链条
  8. private AbstractHandler<T,R> nextHandler;
  9. // 当前链条是否能处理
  10. protected abstract boolean preHandle(T t);
  11. // 具体处理的逻辑
  12. protected abstract R onHandle(T t) throws Exception;
  13. // 总的模版处理逻辑
  14. public R doHandle(T t){
  15. // 能处理,直接处理
  16. if (preHandle(t)){
  17. try {
  18. return onHandle(t);
  19. } catch (Exception e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. // 下一节链条处理
  24. if (nextHandler != null){
  25. return nextHandler.doHandle(t);
  26. }
  27. // 所有链条都不能处理,抛出异常
  28. throw new RuntimeException("责任链中,所有链条都不能处理");
  29. }
  30. }

责任链的Manager,ChainManager

  1. package com.example.mockserver.chain;
  2. import com.example.mockserver.model.MockContext;
  3. public class ChainManager {
  4. // 属性就是链条的头
  5. private AbstractHandler<MockContext,String> handler;
  6. // 构造器,私有,不能被new
  7. private ChainManager(){
  8. // 构造器,给属性赋值。链条的头
  9. this.handler = initHandler();
  10. }
  11. private AbstractHandler<MockContext, String> initHandler() {
  12. // 串成链条,返回头
  13. FileHandler fileHandler = new FileHandler();
  14. DirectoryHandle directoryHandle = new DirectoryHandle();
  15. fileHandler.setNextHandler(directoryHandle);
  16. return fileHandler;
  17. }
  18. // ClassHolder属于静态内部类,在加载类Demo03的时候,只会加载内部类ClassHolder,
  19. // 但是不会把内部类的属性加载出来
  20. private static class ClassHolder{
  21. // 这里执行类加载,是jvm来执行类加载,它一定是单例的,不存在线程安全问题
  22. // 这里不是调用,是类加载,是成员变量
  23. private static final ChainManager holder =new ChainManager();
  24. }
  25. public static ChainManager of(){//第一次调用getInstance()的时候赋值
  26. return ClassHolder.holder;
  27. }
  28. // 处理数据
  29. public String doMapping(MockContext mockContext){
  30. return handler.doHandle(mockContext);
  31. }
  32. }

DirectoryHandle 文件夹的处理方式

  1. package com.example.mockserver.chain;
  2. import cn.hutool.core.io.FileUtil;
  3. import com.example.mockserver.model.MockContext;
  4. import com.example.mockserver.observer.ObserverManager;
  5. public class DirectoryHandle extends AbstractHandler<MockContext,String> {
  6. @Override
  7. protected boolean preHandle(MockContext mockContext) {
  8. // 判断是否是目录
  9. return FileUtil.isDirectory(mockContext.getFilePath());
  10. }
  11. @Override
  12. protected String onHandle(MockContext mockContext) throws Exception {
  13. return ObserverManager.of().getMockData(mockContext);
  14. }
  15. }

 FileHandler 文件的处理方式

  1. package com.example.mockserver.chain;
  2. import cn.hutool.core.io.FileUtil;
  3. import com.example.mockserver.model.MockContext;
  4. import org.apache.commons.io.FileUtils;
  5. import java.io.File;
  6. import java.io.IOException;
  7. public class FileHandler extends AbstractHandler<MockContext,String>{
  8. @Override
  9. protected boolean preHandle(MockContext mockContext) {
  10. return FileUtil.isFile(mockContext.getFilePath());
  11. }
  12. @Override
  13. protected String onHandle(MockContext mockContext) throws Exception {
  14. return FileUtils.readFileToString(new File(mockContext.getFilePath()),"utf-8");
  15. }
  16. }

七、观察者模式对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,给下一个逻辑处理。这里使用观察者的设计模式。

IObserver

  1. package com.example.mockserver.observer;
  2. import com.example.mockserver.model.MockContext;
  3. public interface IObserver<T> {
  4. void update(T t);
  5. }

ObserverManager

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

  1. package com.example.mockserver.observer;
  2. import com.example.mockserver.model.MockContext;
  3. import com.google.common.collect.Lists;
  4. import java.util.List;
  5. public class ObserverManager {
  6. // 属性就是List。观察者就是遍历List处理同一个数据
  7. private List<IObserver<MockContext>> observers;
  8. // 构造器,私有,不能被new
  9. private ObserverManager(){
  10. // 构造器,构造这个属性List
  11. // 这是一个工具实体类的表列
  12. observers = Lists.newArrayList(
  13. new LoadMockFileObserver(),// 1、加载本地mock文件,转成我们需要的实体类
  14. new CalcWeightObserver(), // 2 基于请求的参数,计算权重
  15. new RealObserver(), // 插入一个透传
  16. new PackObserver(), // 3 处理数据
  17. new HookResponseObserver(), // 4 hook
  18. new TimeOutObserver() // 超时
  19. );
  20. }
  21. // ClassHolder属于静态内部类,在加载类Demo03的时候,只会加载内部类ClassHolder,
  22. // 但是不会把内部类的属性加载出来
  23. private static class ClassHolder{
  24. // 这里执行类加载,是jvm来执行类加载,它一定是单例的,不存在线程安全问题
  25. // 这里不是调用,是类加载,是成员变量
  26. private static final ObserverManager holder =new ObserverManager();
  27. }
  28. public static ObserverManager of(){//第一次调用getInstance()的时候赋值
  29. return ClassHolder.holder;
  30. }
  31. // 处理数据的方法
  32. public String getMockData(MockContext mockContext){
  33. for (IObserver observer:this.observers){
  34. // 每一个observer,处理mockContext ,都是没有返回值的
  35. // 我们把所有的结果处理结果,都回写进了mockContext
  36. // 这里用的for循环,我们处理的是同一个mockContext,修改的变量得以保存
  37. observer.update(mockContext);
  38. }
  39. return mockContext.getFinalResponse();
  40. }
  41. }

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

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

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

IObserver

  1. package com.example.mockserver.observer;
  2. import com.example.mockserver.model.MockContext;
  3. public interface IObserver<T> {
  4. void update(T t);
  5. }

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

  1. package com.example.mockserver.observer;
  2. import com.example.mockserver.model.MockContext;
  3. import com.example.mockserver.model.MockDataInfo;
  4. import com.example.mockserver.util.YamlUtil;
  5. import java.io.File;
  6. import java.util.Arrays;
  7. import java.util.List;
  8. import java.util.stream.Collectors;
  9. /**
  10. * 加载本地mock文件,转成我们需要的实体类List
  11. */
  12. public class LoadMockFileObserver implements IObserver<MockContext>{
  13. @Override
  14. public void update(MockContext mockContext) {
  15. // 根据请求的目录,获取目录下所有的文件
  16. File[] files = new File(mockContext.getFilePath()).listFiles();
  17. List<MockDataInfo> mockDataInfoList = Arrays.stream(files)
  18. // 转换,把每一个文件转成对象MockDataInfo
  19. .map(f -> YamlUtil.readForObject(f.getAbsolutePath(), MockDataInfo.class))
  20. // 将数组,转成List
  21. .collect(Collectors.toList());
  22. // 将一个接口,对应的所有返回信息List ,回写进MockContext
  23. mockContext.setMockDataInfoList(mockDataInfoList);
  24. }
  25. }

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

CalcWeightObserver

  1. package com.example.mockserver.observer;
  2. import com.example.mockserver.model.MappingParamsEntity;
  3. import com.example.mockserver.model.MockContext;
  4. import com.example.mockserver.model.MockDataInfo;
  5. import com.example.mockserver.util.YamlUtil;
  6. import java.io.File;
  7. import java.util.List;
  8. /**
  9. * 计算一个接口对应的所有文件(List对象)的权重,返回权重大的结果
  10. */
  11. public class CalcWeightObserver implements IObserver<MockContext>{
  12. @Override
  13. public void update(MockContext mockContext) {
  14. // 定义最终的权重结果 和最终的response
  15. int weightResult = 0;
  16. String response = "";
  17. for(MockDataInfo mockDataInfo: mockContext.getMockDataInfoList()){
  18. // 取出实体类的参数,dd得到当前对象的参数list
  19. List<MappingParamsEntity> mappingParams = mockDataInfo.getMappingParams();
  20. int weight = 0;
  21. for (MappingParamsEntity mappingParamsEntity:mappingParams){
  22. // 将参数转成k=v
  23. String paramStr = mappingParamsEntity.getParams().entrySet().stream()
  24. .map(e -> e.getKey()+"="+e.getValue() )
  25. .findFirst().get(); // 我们这里的Map,只有一个值
  26. // 判断 我们yml文件里指定的参数策略,在不在用户传参url的参数列表里。
  27. if(mockContext.getParamStringList().contains(paramStr)){
  28. // 如果在,则累计权重
  29. weight = weight + mappingParamsEntity.getWeight();
  30. }
  31. }
  32. // 每一个文件的权重比较大小,最终返回权重最大的response
  33. if(weight>weightResult){
  34. weightResult = weight;
  35. response = mockDataInfo.getResponse();
  36. }
  37. }
  38. mockContext.setFinalResponse(response);
  39. }
  40. }
 

替换动态变量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位的字符串。

 
  1. package com.example.mockserver.observer;
  2. import com.example.mockserver.decorator.DecoratorManager;
  3. import com.example.mockserver.model.MockContext;
  4. import com.example.mockserver.util.RandomUtil;
  5. import org.apache.commons.lang3.StringUtils;
  6. public class PackObserver implements IObserver<MockContext> {
  7. // @Override
  8. // public void update(MockContext mockContext) {
  9. // String finalResponse = mockContext.getFinalResponse();
  10. // // random -> 随机字符
  11. // String packResponse = StringUtils.replace(finalResponse,"${random}", RandomUtil.random());
  12. // mockContext.setFinalResponse(packResponse);
  13. //
  14. // }
  15. @Override
  16. public void update(MockContext mockContext) {
  17. String finalResponse = mockContext.getFinalResponse();
  18. // random -> 随机字符
  19. String packResponse = DecoratorManager.of().doPack(finalResponse);
  20. mockContext.setFinalResponse(packResponse);
  21. }
  22. }

ObserverManager

  1. package com.example.mockserver.observer;
  2. import com.example.mockserver.model.MockContext;
  3. import com.google.common.collect.Lists;
  4. import java.util.List;
  5. public class ObserverManager {
  6. // 属性就是List。观察者就是遍历List处理同一个数据
  7. private List<IObserver<MockContext>> observers;
  8. // 构造器,私有,不能被new
  9. private ObserverManager(){
  10. // 构造器,构造这个属性List
  11. // 这是一个工具实体类的表列
  12. observers = Lists.newArrayList(
  13. new LoadMockFileObserver(),// 1、加载本地mock文件,转成我们需要的实体类
  14. new CalcWeightObserver(), // 2 基于请求的参数,计算权重
  15. new PackObserver() // 3 处理数据
  16. );
  17. }
  18. // ClassHolder属于静态内部类,在加载类Demo03的时候,只会加载内部类ClassHolder,
  19. // 但是不会把内部类的属性加载出来
  20. private static class ClassHolder{
  21. // 这里执行类加载,是jvm来执行类加载,它一定是单例的,不存在线程安全问题
  22. // 这里不是调用,是类加载,是成员变量
  23. private static final ObserverManager holder =new ObserverManager();
  24. }
  25. public static ObserverManager of(){//第一次调用getInstance()的时候赋值
  26. return ClassHolder.holder;
  27. }
  28. // 处理数据的方法
  29. public String getMockData(MockContext mockContext){
  30. for (IObserver observer:this.observers){
  31. // 每一个observer,处理mockContext ,都是没有返回值的
  32. // 我们把所有的结果处理结果,都回写进了mockContext
  33. // 这里用的for循环,我们处理的是同一个mockContext,修改的变量得以保存
  34. observer.update(mockContext);
  35. }
  36. return mockContext.getFinalResponse();
  37. }
  38. }

再补充一下完善后的MockContext

  1. package com.example.mockserver.model;
  2. import com.example.mockserver.consts.MockConst;
  3. import lombok.Builder;
  4. import lombok.Data;
  5. import org.apache.commons.lang3.StringUtils;
  6. import java.util.List;
  7. import java.util.Map;
  8. import java.util.stream.Collectors;
  9. @Data
  10. @Builder
  11. public class MockContext {
  12. // 用户传入信息
  13. // 一次用户请求,对应一个MockContext
  14. private String requestURI;
  15. private Map<String,String> requestParams;
  16. private String requestIp;
  17. // 返回结果的List
  18. // 一个接口,对应的返回结果,是一个List
  19. private List<MockDataInfo> mockDataInfoList;
  20. private String finalResponse;
  21. // 根据uri,得到文件名
  22. // /get/order/info -> get_order_info
  23. // 去掉第一个/ ,取后面的字符串
  24. public String getFileName(){
  25. String str = StringUtils.substringAfter(this.requestURI, "/");
  26. String fileName = StringUtils.replace(str, "/", "_");
  27. return fileName;
  28. }
  29. // 得到文件的路径
  30. public String getFilePath(){
  31. String filePath = MockConst.MOCK_DATA_PATH+"/"+this.getFileName();
  32. return filePath;
  33. }
  34. // 将用户传参,组成一个k=v 的List
  35. public List<String> getParamStringList(){
  36. // 计算权重的方法
  37. // 用户的传参,mockContext.getRequestParams(),是一个Map
  38. // 将用户的传参Map,转换成list。
  39. // 如 k:v, id:123,name:zhangsan 转换成 【"id=123","name=zhangsan"]
  40. List<String> paramStrList = this.getRequestParams().entrySet().stream()
  41. .map(e -> e.getKey() + "=" + e.getValue())
  42. .collect(Collectors.toList());
  43. return paramStrList;
  44. }
  45. }

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

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

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

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

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

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

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

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

当一个请求,

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

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

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

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

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

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

 则请求:

 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

  1. {
  2. "id":"123",
  3. "name":"zhangsan"
  4. }

转成list

["id"="123","name"="zhangsan"]

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

返回数据的实体类MockDataInfo:

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

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

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

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

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

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

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

整体实现代码

  1. // 如果是文件夹,获取所有文件。是一个数组
  2. // 取出所有的文件
  3. File[] files = file.listFiles();
  4. // 定义最终的权重结果 和最终的response
  5. int weightResult = 0;
  6. String response = "";
  7. // 遍历所有的文件
  8. for(File f:files){
  9. // 循环,将每个文件都转成一个对象。把yml文件转成实体类
  10. MockDataInfo mockDataInfo = YamlUtil.readForObject(f.getAbsolutePath(), MockDataInfo.class);
  11. // 取出实体类的参数,得到当前对象的参数list
  12. List<MappingParamsEntity> mappingParams = mockDataInfo.getMappingParams();
  13. int weight = 0;
  14. //
  15. for (MappingParamsEntity mappingParamsEntity:mappingParams){
  16. // 将参数转成k=v
  17. String paramStr = mappingParamsEntity.getParams().entrySet().stream()
  18. .map(e -> e.getKey()+"="+e.getValue() )
  19. .findFirst().get(); // 我们这里的Map,只有一个值
  20. // 判断 我们yml文件里指定的参数策略,在不在用户传参url的参数列表里。
  21. if(paramStrList.contains(paramStr)){
  22. // 如果在,则累计权重
  23. weight = weight + mappingParamsEntity.getWeight();
  24. }
  25. }
  26. //
  27. if(weight>weightResult){
  28. weightResult = weight;
  29. response = mockDataInfo.getResponse();
  30. }
  31. }

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

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

DecoratorManager.of().doPack(finalResponse);

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

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

1、先写基类的接口IDecorator

这里用了泛型

  1. public interface IDecorator<T> {
  2. T decorate(T data);
  3. }

2、装饰器的基类BaseResponseDecorator

  1. package com.example.mockserver.decorator;
  2. public abstract class BaseResponseDecorator<T> implements IDecorator<T>{
  3. private BaseResponseDecorator<T> decorator;
  4. // 构造器
  5. public BaseResponseDecorator(BaseResponseDecorator<T> decorator) {
  6. this.decorator = decorator;
  7. }
  8. // 自己装饰的方法,重写这个方法
  9. public abstract T onDecorator(T t);
  10. // 整体调用的逻辑
  11. public T decorate(T t){
  12. // 先判断,当前属性是否为空
  13. if(decorator != null){
  14. // 不为空,先让下一节decorator装饰
  15. t = decorator.decorate(t);
  16. // 再自己装饰一次,一共装饰了2次
  17. return onDecorator(t);
  18. }
  19. // 为空,就调用自己的装饰方法。只装饰一次
  20. return onDecorator(t);
  21. }
  22. }

3、对数字的处理RandomIdDecorator

  1. package com.example.mockserver.decorator;
  2. import com.example.mockserver.util.RandomUtil;
  3. import org.apache.commons.lang3.StringUtils;
  4. import java.util.regex.Matcher;
  5. import java.util.regex.Pattern;
  6. public class RandomIdDecorator extends BaseResponseDecorator<String>{
  7. private static final Pattern PATTERN = Pattern.compile("\\$\\{random:id:(\\d+?)\\}");
  8. // 构造器
  9. public RandomIdDecorator(BaseResponseDecorator<String> decorator) {
  10. super(decorator);
  11. }
  12. @Override
  13. public String onDecorator(String data) {
  14. Matcher matcher = PATTERN.matcher(data);
  15. while (matcher.find()){
  16. String replaceStr = matcher.group(0);
  17. int size = Integer.parseInt(matcher.group(1));
  18. // 替换
  19. data = StringUtils.replace(data,replaceStr, RandomUtil.randomNum(size));
  20. }
  21. return data;
  22. }
  23. }

4、对字符串的处理RandomStrDecorator

  1. package com.example.mockserver.decorator;
  2. import com.example.mockserver.util.RandomUtil;
  3. import org.apache.commons.lang3.StringUtils;
  4. import java.util.regex.Matcher;
  5. import java.util.regex.Pattern;
  6. public class RandomStrDecorator extends BaseResponseDecorator<String>{
  7. private static final Pattern PATTERN = Pattern.compile("\\$\\{random:str:(\\d+?)\\}");
  8. // 构造器
  9. public RandomStrDecorator(BaseResponseDecorator<String> decorator) {
  10. super(decorator);
  11. }
  12. @Override
  13. public String onDecorator(String data) {
  14. Matcher matcher = PATTERN.matcher(data);
  15. while (matcher.find()){
  16. String replaceStr = matcher.group(0);
  17. int size = Integer.parseInt(matcher.group(1));
  18. // 替换
  19. data = StringUtils.replace(data,replaceStr, RandomUtil.randomStr(size));
  20. }
  21. return data;
  22. }
  23. }

5、manager   DecoratorManager

  1. package com.example.mockserver.decorator;
  2. public class DecoratorManager {
  3. // 属性
  4. private IDecorator<String> decorator;
  5. // 构造器,私有,不能被new
  6. private DecoratorManager(){
  7. decorator = new RandomIdDecorator(new RandomStrDecorator(null));
  8. }
  9. // ClassHolder属于静态内部类,在加载类Demo03的时候,只会加载内部类ClassHolder,
  10. // 但是不会把内部类的属性加载出来
  11. private static class ClassHolder{
  12. // 这里执行类加载,是jvm来执行类加载,它一定是单例的,不存在线程安全问题
  13. // 这里不是调用,是类加载,是成员变量
  14. private static final DecoratorManager holder =new DecoratorManager();
  15. }
  16. public static DecoratorManager of(){//第一次调用getInstance()的时候赋值
  17. return ClassHolder.holder;
  18. }
  19. public String doPack(String response){
  20. return decorator.decorate(response);
  21. }
  22. }

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 

  1. package com.example.mockserver.observer;
  2. import com.example.mockserver.model.MockContext;
  3. import org.apache.commons.lang3.StringUtils;
  4. import java.util.Map;
  5. import java.util.regex.Matcher;
  6. import java.util.regex.Pattern;
  7. /**
  8. * 建设点:请求数据 hook
  9. * 场景:比如请求的时候,请求参数携带一个requestId, 然后requestId本身还是个变化的,也是随机的。
  10. * 然后在返回的时候,要把这个id带回去,即:虽然返回数据不能写死,但是你也不能自己生成,需要使用请求的参数
  11. * {"key1":"${random:id:6}","key2":"${random:str:10}","count":3,"person":[{"id":${hook:userId},"name":"张三"},{"id":2,"name":"李四"}],"object":{"id":1,"msg":"对象里的对象"}}
  12. * ${hook:userId} => userId
  13. * 实现思路:
  14. * 1. 要保证request params 有我们需要的参数
  15. * 2. 设定标记 ${hook:userId}
  16. * 3. 通过正则 获取参数名
  17. * 4. 从request params 获取参数值
  18. * 5. 完成替换
  19. * 6. 回写
  20. */
  21. public class HookResponseObserver0 implements IObserver<MockContext> {
  22. private static final Pattern PATTERN = Pattern.compile("\\$\\{hook:(.*?)\\}");
  23. @Override
  24. public void update(MockContext mockContext) {
  25. // 拿到返回结果
  26. String finalResponse = mockContext.getFinalResponse();
  27. // 拿到请求参数
  28. Map<String, String> requestParams = mockContext.getRequestParams();
  29. Matcher matcher = PATTERN.matcher(finalResponse);
  30. while ((matcher.find())){
  31. String replaceStr = matcher.group(0);
  32. String paramName = matcher.group(1);
  33. // 如果用户传参数里不包含要替换的值,直接返回
  34. if(!requestParams.containsKey(paramName)){
  35. break;
  36. }
  37. String value = requestParams.get(paramName);
  38. finalResponse = StringUtils.replace(finalResponse,replaceStr,value);
  39. }
  40. // 回写数据
  41. mockContext.setFinalResponse(finalResponse);
  42. }
  43. }

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

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

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

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

具体的处理逻辑CommonHookDecorator

  1. package com.example.mockserver.decorator;
  2. import com.example.mockserver.model.HookContext;
  3. import org.apache.commons.lang3.StringUtils;
  4. import java.util.Map;
  5. import java.util.regex.Matcher;
  6. import java.util.regex.Pattern;
  7. public class CommonHookDecorator extends BaseResponseDecorator<HookContext>{
  8. private static final Pattern PATTERN = Pattern.compile("\\$\\{hook:(.*?)\\}");
  9. public CommonHookDecorator(BaseResponseDecorator<HookContext> decorator) {
  10. super(decorator);
  11. }
  12. @Override
  13. public HookContext onDecorator(HookContext hookContext) {
  14. // 拿到返回结果
  15. String finalResponse = hookContext.getFinalResponse();
  16. // 拿到请求参数
  17. Map<String, String> requestParams = hookContext.getRequestParams();
  18. Matcher matcher = PATTERN.matcher(finalResponse);
  19. while ((matcher.find())){
  20. String replaceStr = matcher.group(0);
  21. String paramName = matcher.group(1);
  22. // 如果用户传参数里不包含要替换的值,直接返回
  23. if(!requestParams.containsKey(paramName)){
  24. break;
  25. }
  26. String value = requestParams.get(paramName);
  27. finalResponse = StringUtils.replace(finalResponse,replaceStr,value);
  28. }
  29. // 回写数据
  30. hookContext.setFinalResponse(finalResponse);
  31. return hookContext;
  32. }
  33. }

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

  1. package com.example.mockserver.decorator;
  2. import com.example.mockserver.model.HookContext;
  3. public class DecoratorManager {
  4. // 属性
  5. private IDecorator<String> packDecorator;
  6. private IDecorator<HookContext> hookDecorator;
  7. // 构造器,私有,不能被new
  8. private DecoratorManager(){
  9. packDecorator = new RandomIdDecorator(new RandomStrDecorator(null));
  10. // 这个hook链条,目前只有一个
  11. hookDecorator = new CommonHookDecorator(null);
  12. }
  13. // ClassHolder属于静态内部类,在加载类Demo03的时候,只会加载内部类ClassHolder,
  14. // 但是不会把内部类的属性加载出来
  15. private static class ClassHolder{
  16. // 这里执行类加载,是jvm来执行类加载,它一定是单例的,不存在线程安全问题
  17. // 这里不是调用,是类加载,是成员变量
  18. private static final DecoratorManager holder =new DecoratorManager();
  19. }
  20. public static DecoratorManager of(){//第一次调用getInstance()的时候赋值
  21. return ClassHolder.holder;
  22. }
  23. public String doPack(String response){
  24. return packDecorator.decorate(response);
  25. }
  26. public HookContext doHook(HookContext hookContext){
  27. return this.hookDecorator.decorate(hookContext);
  28. }
  29. }

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

HookResponseObserver

  1. package com.example.mockserver.observer;
  2. import com.example.mockserver.decorator.DecoratorManager;
  3. import com.example.mockserver.model.HookContext;
  4. import com.example.mockserver.model.MockContext;
  5. import org.apache.commons.lang3.StringUtils;
  6. import java.util.Map;
  7. import java.util.regex.Matcher;
  8. import java.util.regex.Pattern;
  9. /**
  10. * 建设点:请求数据 hook
  11. * 场景:比如请求的时候,请求参数携带一个requestId, 然后requestId本身还是个变化的,也是随机的。
  12. * 然后在返回的时候,要把这个id带回去,即:虽然返回数据不能写死,但是你也不能自己生成,需要使用请求的参数
  13. * {"key1":"${random:id:6}","key2":"${random:str:10}","count":3,"person":[{"id":${hook:userId},"name":"张三"},{"id":2,"name":"李四"}],"object":{"id":1,"msg":"对象里的对象"}}
  14. * ${hook:userId} => userId
  15. * 实现思路:
  16. * 1. 要保证request params 有我们需要的参数
  17. * 2. 设定标记 ${hook:userId}
  18. * 3. 通过正则 获取参数名
  19. * 4. 从request params 获取参数值
  20. * 5. 完成替换
  21. * 6. 回写
  22. */
  23. public class HookResponseObserver implements IObserver<MockContext> {
  24. private static final Pattern PATTERN = Pattern.compile("\\$\\{hook:(.*?)\\}");
  25. @Override
  26. public void update(MockContext mockContext) {
  27. HookContext hookContext = HookContext.builder()
  28. .finalResponse(mockContext.getFinalResponse())
  29. .requestParams(mockContext.getRequestParams())
  30. .build();
  31. hookContext = DecoratorManager.of().doHook(hookContext);
  32. // 再回血mockContext
  33. mockContext.setFinalResponse(hookContext.getFinalResponse());
  34. }
  35. }

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

十二、依赖

springframework.boot 用的2.4.4版本

pom.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <parent>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-parent</artifactId>
  8. <version>2.4.4</version>
  9. <relativePath/> <!-- lookup parent from repository -->
  10. </parent>
  11. <groupId>com.example</groupId>
  12. <artifactId>AutoApi</artifactId>
  13. <version>0.0.1-SNAPSHOT</version>
  14. <name>AutoApi</name>
  15. <description>Demo project for Spring Boot</description>
  16. <properties>
  17. <java.version>1.8</java.version>
  18. </properties>
  19. <dependencies>
  20. <dependency>
  21. <groupId>org.springframework.boot</groupId>
  22. <artifactId>spring-boot-starter-web</artifactId>
  23. </dependency>
  24. <dependency>
  25. <groupId>org.springframework.boot</groupId>
  26. <artifactId>spring-boot-starter-test</artifactId>
  27. <scope>test</scope>
  28. </dependency>
  29. <dependency>
  30. <groupId>org.yaml</groupId>
  31. <artifactId>snakeyaml</artifactId>
  32. <version>1.26</version>
  33. </dependency>
  34. <dependency>
  35. <groupId>org.projectlombok</groupId>
  36. <artifactId>lombok</artifactId>
  37. <version>1.18.16</version>
  38. <scope>provided</scope>
  39. </dependency>
  40. <dependency>
  41. <groupId>com.google.guava</groupId>
  42. <artifactId>guava</artifactId>
  43. <version>30.1.1-jre</version>
  44. </dependency>
  45. <dependency>
  46. <groupId>org.apache.commons</groupId>
  47. <artifactId>commons-lang3</artifactId>
  48. <version>3.11</version>
  49. </dependency>
  50. <dependency>
  51. <groupId>com.squareup.okhttp3</groupId>
  52. <artifactId>okhttp</artifactId>
  53. <version>4.9.0</version>
  54. </dependency>
  55. <dependency>
  56. <groupId>commons-io</groupId>
  57. <artifactId>commons-io</artifactId>
  58. <version>2.6</version>
  59. </dependency>
  60. <dependency>
  61. <groupId>com.google.code.gson</groupId>
  62. <artifactId>gson</artifactId>
  63. <version>2.8.6</version>
  64. </dependency>
  65. <dependency>
  66. <groupId>com.fasterxml.jackson.dataformat</groupId>
  67. <artifactId>jackson-dataformat-yaml</artifactId>
  68. <version>2.12.3</version>
  69. </dependency>
  70. <dependency>
  71. <groupId>org.springframework</groupId>
  72. <artifactId>spring-jdbc</artifactId>
  73. <version>5.3.8</version>
  74. </dependency>
  75. <dependency>
  76. <groupId>mysql</groupId>
  77. <artifactId>mysql-connector-java</artifactId>
  78. <version>8.0.20</version>
  79. </dependency>
  80. <dependency>
  81. <groupId>com.alibaba</groupId>
  82. <artifactId>druid</artifactId>
  83. <version>1.2.6</version>
  84. </dependency>
  85. <dependency>
  86. <groupId>cn.hutool</groupId>
  87. <artifactId>hutool-core</artifactId>
  88. <version>5.7.5</version>
  89. </dependency>
  90. </dependencies>
  91. <build>
  92. <plugins>
  93. <plugin>
  94. <groupId>org.springframework.boot</groupId>
  95. <artifactId>spring-boot-maven-plugin</artifactId>
  96. </plugin>
  97. </plugins>
  98. </build>
  99. </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":"对象里的对象"}}

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/622917
推荐阅读
相关标签
  

闽ICP备14008679号