• spring boot 实现mock平台


    使用spring boot 写mock平台。

    一、实现场景:

    1.1请求同一个接口,不同的传参/ip返回的结果不一样。

    数据的匹配

    1. 以create_account接口为例,参数有accountId
    2. accountId=123,返回成功
    3. accountId=456,返回失败
    4. accountId=456,accountName=zhangsan,返回处理中
    5. ip=123.123.123.1 返回成功
    6. ip=123.123.123.2 返回失败

    解决方案:

    将数据存储在yml文件里。如create_account接口,只返回一个固定的结果,那么我们就将返回值放到一个yml文件里。

    如create_name接口,根据传参不同,返回不同的id。一个接口对应一个文件夹, 将create_name接口的返回值写多个yml文件,放到同一个文件夹里。然后再去匹配。

    1.2 返回的结果不是写死的,是动态数据,需要对数据进行处理

    • response: {xxxxx,"orderId":123456}
    • response: {xxxxx,"orderId":123456}
    • response: {xxxxx,"createTime":123456}

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

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

    1.3 回调能力

    就是外部请求到我mock服务之后,我mock服务做了返回,但同时我会按照要求给它去完成某些能力,如:

    • 调http
    • 调公司内部的RPC
    • 或mq
    • 还有可能是写某个db.

    1.4响应时间

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

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

    模拟超时处理

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

    1.5 hook参数

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

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

    1.6 透传请求

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

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

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

    总结:

    其实就是把数据源放在文件里,根据用户的传参,对数据进行处理,然后再返回。

    二、依赖

    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>

    三、开始写

    3.1 新建一个controller,简单的建一个服务,试试是否能ping通

     PingController

    1. package com.example.mockserver.controller;
    2. import org.springframework.web.bind.annotation.RequestMapping;
    3. import org.springframework.web.bind.annotation.RestController;
    4. @RestController
    5. public class PingController {
    6. @RequestMapping("/ping")
    7. public String ping(){
    8. return "ok";
    9. }
    10. }

    要有@RestController 注解

     
    

    修改application.properties 格式,换成yml  application.yml 。并指定端口号。

    application.yml

    1. server:
    2. port: 8081

    启动spring boot ,运行 MockServerApplication 。访问一下127.0.0.1:8081/ping。

    返回ok就是正常的。

    3.2   新建MockController1,/** 这里指的是任意的URI

    MockController1

    1. package com.example.mockserver.controller;
    2. import org.springframework.web.bind.annotation.RequestMapping;
    3. import org.springframework.web.bind.annotation.RestController;
    4. @RestController
    5. public class MockController1 {
    6. @RequestMapping("/**")
    7. public String doMock(){
    8. return "do mock server";
    9. }
    10. }
     
    

    "/**" 这里指的是任意的URI 。启动服务,当请求ping时返回ok,请求其他任何URL时,返回 do mock server

    3.3 将不同的接口返回值,放到不同的txt文件里。根据请求的URI,来返回对应的内容。

    新建2个txt文件,get_order_info 与 get_user_info 文件。存放不同的内容。

     新建MockController2

    MockController2

    拿到请求的URI,将URI拼成对应的文件路径,然后读取这个文件。

    1. package com.example.mockserver.controller;
    2. import org.apache.commons.io.IOUtils;
    3. import org.apache.commons.lang3.StringUtils;
    4. import org.springframework.beans.factory.annotation.Autowired;
    5. import org.springframework.web.bind.annotation.RequestMapping;
    6. import org.springframework.web.bind.annotation.RestController;
    7. import javax.servlet.http.HttpServletRequest;
    8. import java.io.IOException;
    9. @RestController
    10. public class MockController2 {
    11. // 文件存放路径,常量
    12. public static final String MOCK_DATA_PATH = "mock_data";
    13. // 注入
    14. @Autowired
    15. private HttpServletRequest request;
    16. @RequestMapping("/**")
    17. public String doMock() throws IOException {
    18. // 获取请求的URI
    19. String requestURI = request.getRequestURI();
    20. // 将输入的URI,拼成文件的名称
    21. // /get/order/info => get_order_info
    22. // 取/get/order/info ,第一个/后面的数据,get/order/info
    23. requestURI =StringUtils.substringAfter(requestURI,"/");
    24. // 将/替换成_,将uri拼成文件名称
    25. String fileName = StringUtils.replace(requestURI, "/", "_");
    26. // 拼成文件路径
    27. String filePath = MOCK_DATA_PATH+"/"+fileName;
    28. // 读取文件
    29. String response = IOUtils.toString(MockController2.class.getClassLoader().getResourceAsStream(filePath));
    30. return response;
    31. }
    32. }

    访问 http://127.0.0.1:8081/get/order/info

    注意:

    我们这里存放mock文件,放到了项目的resources目录下。正常情况下,是不可以这么写的。当我们打包时,会将这个文件一起打进去,这样再修改数据不好修改了。

    这个路径应该写磁盘上的某个路径。

    方法一,支持用户配置自己的路径

    方法二,默认路径,为磁盘上的某个位置,比如“/user/local/aa" 

    四、

    4.1场景:

    场景一

    1、create_name 接口是一个简单接口,只对应一个文件。则我们读取URI,根据文件名匹配这个文件,读取这个文件,并返回

    场景二

    2、create_user 接口是一个复杂接口,有2个参数,id 与 name 。

    当id=123时,返回A数据

    当id=456时,返回B数据

    当name=zhangsan时,返回C数据

    当name=lisi时,返回D数据。

    那么当id与name组合时候,应该怎么返回数据呢?

    方式一:

    就是把所有的参数字段,拼到文件名称上,我们直接根据文件名称来读取数据。

    当参数过多时,导致文件名称非常长,这是不可取的。

    方式二:

    给每个一个字段设置一个权重,遍历每一个文件,取值最大的。

    如匹配name与id字段。我们设置了 a、b、c、d 四个文件。

    a文件

    1. mappingHost: 127.0.0.1
    2. mappingParams:
    3. - params:
    4. id: 123
    5. weight: 2
    6. - params:
    7. name: "zhangsan"
    8. weight: 4
    9. response: 'AAA'

    b文件

    1. mappingHost: 127.0.0.1
    2. mappingParams:
    3. - params:
    4. id: 456
    5. weight: 2
    6. - params:
    7. name: "zhangsan"
    8. weight: 4
    9. response: 'BBBB'

    c文件

    1. mappingHost: 127.0.0.1
    2. mappingParams:
    3. - params:
    4. id: 123
    5. weight: 2
    6. - params:
    7. name: "lisi"
    8. weight: 4
    9. response: 'CCCC'

    d文件

    1. mappingHost: 127.0.0.1
    2. mappingParams:
    3. - params:
    4. id: 456
    5. weight: 2
    6. - params:
    7. name: "lisi"
    8. weight: 4
    9. response: 'DDDD'

    举例:

    当传参id=123&name=zhangsan时,遍历这四个文件。

    a文件值为2+4=6

    b文件值为4

    c文件值为2

    d文件值为0

    我们取最大的值返回,a文件的值,最大,我们则把a文件返回。

    返回值的模版

    1. mappingHost: 127.0.0.1
    2. mappingParams:
    3. - params:
    4. id: 123
    5. weight: 2
    6. - params:
    7. name: "zhangsan"
    8. weight: 4
    9. response: '{"key1":"${random:id}","key2":"value2","count":3,"person":[{"id":1,"name":"张三"},{"id":2,"name":"李四"}],"object":{"id":1,"msg":"对象里的对象"}}'
    返回值,是一个yml格式的,里面不仅仅包含了返回值response,还包含了传参数。
    这里参数是为了匹配哪个文件而设置的。还包含了发起请求的ip

    4.2整体思路:

    1、简单的接口,对应一个文件,直接读取并返回。复杂接口,计算权重,返回大的文件。

    2、得到用户的请求。包含ip,传参,URI。

    URL用来匹配文件夹,传参用来计算权重,匹配具体的文件

    3、构造用户的请求类。用来储存,传递 用户的请求

    4、构造返回值的类,用来储存和计算比较权重,返回权重大的数据。

    4.3 构建MockContext类,存储传递用户请求

    我们根据用户传进来的ip、传参数和URI返回不同的数据。所以我们建一个MockContext类,用来存储传递这些用户信息。

    MockContext

    1. package com.example.mockserver.model;
    2. import lombok.Builder;
    3. import lombok.Data;
    4. import org.apache.commons.lang3.StringUtils;
    5. import java.util.Map;
    6. @Data
    7. @Builder
    8. public class MockContext {
    9. private String requestURI;
    10. private Map<String,String> requestParams;
    11. private String requestIp;
    12. public String getFileName(){
    13. String str = StringUtils.substringAfter(this.requestURI, "/");
    14. String fileName = StringUtils.replace(str, "/", "_");
    15. return fileName;
    16. }
    17. }

    构造MockContext的对象,建造者模式,直接往里面填数据

    1. // 将获取的用户数据 ip 参数 URI ,存储到 mockContext 这个类里
    2. MockContext mockContext = MockContext.builder()
    3. .requestIp(remoteAddr)
    4. .requestParams(collect)
    5. .requestURI(uri)
    6. .build();

    4.4 构建MockDataInfo类,存储返回值

    我们先查看返回值的模版,是yml格式的。

    1. mappingHost: 127.0.0.1
    2. mappingParams:
    3. - params:
    4. id: 123
    5. weight: 2
    6. - params:
    7. name: "zhangsan"
    8. weight: 4
    9. response: '{"key1":"${random:id}","key2":"value2","count":3,"person":[{"id":1,"name":"张三"},{"id":2,"name":"李四"}],"object":{"id":1,"msg":"对象里的对象"}}'

    MockDataInfo 这个类,mappingHost 、response 是字符串。

    mappingParams 是一个list。

     

    我们先构造mappingParams  list里面的对象MappingParamsEntity

    MappingParamsEntity

    1. package com.example.mockserver.model;
    2. import lombok.Data;
    3. import java.util.Map;
    4. @Data
    5. public class MappingParamsEntity {
    6. private Map<String,String> params;
    7. private int weight;
    8. }

    再构建MockDataInfo 这个类

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

    构造MockDataInfo类的对象。这里就是读取文件,将文件内容封装在这个类里。

    这里我们写一个读取文件,将文件内容转换成这个类的方法。YamlUtil

    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. // 这个工具的作用就是,读取返回值的文件,然后转换成一个对象
    8. public static <T> T readForObject(String path,Class<T> cls){
    9. try {
    10. Yaml yaml = new Yaml();
    11. // loadAs传参1是文件的流,传参2是要转换成哪个类的对象
    12. T t = yaml.loadAs(new FileInputStream(path), cls);
    13. return t;
    14. } catch (FileNotFoundException e) {
    15. e.printStackTrace();
    16. throw new IllegalArgumentException(e);
    17. }
    18. }
    19. public static void main(String[] args) {
    20. MockDataInfo mockDataInfo = readForObject("/Users/zhaohui/IdeaProjects/mock-server/src/main/resources/mock_data/get_user/aaa", MockDataInfo.class);
    21. System.out.println("mockDataInfo = " + mockDataInfo);
    22. }
    23. }

    因为我们文件内容本身是yml格式的,这里我们利用snakeyaml模块的能力,可以直接读取文件内容,并转我们想要的对象。

     T t = yaml.loadAs(new FileInputStream(path), cls);

  • 相关阅读:
    flutter 获取屏幕尺寸
    [含文档+PPT+源码等]精品微信小程序ssm校园二手交易小程序+后台管理系统|前后分离VUE[包运行成功]微信小程序项目源码Java毕业设计
    压六类双绞线网线水晶头,
    JEECMS安装部署方法以及使用说明教程
    Java变量---尚硅谷Java入门视频学习
    云服务部署:AWS、Azure和GCP比较
    【初认Redis】
    每日一练 | 华为认证真题练习Day127
    [代码随想录]二叉树篇
    【电路笔记】-诺顿定理(Norton‘s Theorem)
  • 原文地址:https://blog.csdn.net/qq_39208536/article/details/125471344