使用spring boot 写mock平台。
数据的匹配
- 以create_account接口为例,参数有accountId
- accountId=123,返回成功
- accountId=456,返回失败
- accountId=456,accountName=zhangsan,返回处理中
- ip=123.123.123.1 返回成功
- ip=123.123.123.2 返回失败
解决方案:
将数据存储在yml文件里。如create_account接口,只返回一个固定的结果,那么我们就将返回值放到一个yml文件里。
如create_name接口,根据传参不同,返回不同的id。一个接口对应一个文件夹, 将create_name接口的返回值写多个yml文件,放到同一个文件夹里。然后再去匹配。
1、 返回的数据中不能全部都是写死的,有的可能是随机的id,有的可能是时间戳,还有的可能是固定格式的数据
2、实际业务有一个case: 要返回merId:xxxx, 但是这个merId的获取,是要从别的业务的接口中获取返回信息。
就是外部请求到我mock服务之后,我mock服务做了返回,但同时我会按照要求给它去完成某些能力,如:
比如服务调我们的mock时,我们是直接给返回。
那要是模拟一下真实的服务处理,比如处理超时,假设用时 3秒在返回。
模拟超时处理
思考: 如果你做线上压测的时候,相应时间不能给返回一个固定值,所以返回是一个区间的概率。
比如请求的时候,请求参数携带一个requestId, 然后requestId本身还是个变化的,也是随机的。
然后在返回的时候,要把这个id带回去,即:虽然返回数据不能写死,但是你也不能自己生成,需要使用请求的参数。
比如10个请求,请求mock服务,其中参数id=123的走mock,id=456的走真实的服务。
所以这个时候如果我们判断id=456了,我们需要去自己真实的拿着请求的参数,我们再去调真实服务。
拿到返回结果,在返回给调用端。
其实就是把数据源放在文件里,根据用户的传参,对数据进行处理,然后再返回。
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>
3.1 新建一个controller,简单的建一个服务,试试是否能ping通

PingController
- package com.example.mockserver.controller;
-
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- @RestController
- public class PingController {
-
- @RequestMapping("/ping")
- public String ping(){
- return "ok";
-
- }
- }
要有@RestController 注解
修改application.properties 格式,换成yml application.yml 。并指定端口号。
application.yml
- server:
- port: 8081
启动spring boot ,运行 MockServerApplication 。访问一下127.0.0.1:8081/ping。
返回ok就是正常的。
3.2 新建MockController1,/** 这里指的是任意的URI
MockController1
- package com.example.mockserver.controller;
-
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- @RestController
- public class MockController1 {
-
- @RequestMapping("/**")
- public String doMock(){
- return "do mock server";
- }
- }
"/**" 这里指的是任意的URI 。启动服务,当请求ping时返回ok,请求其他任何URL时,返回 do mock server
3.3 将不同的接口返回值,放到不同的txt文件里。根据请求的URI,来返回对应的内容。
新建2个txt文件,get_order_info 与 get_user_info 文件。存放不同的内容。

新建MockController2
MockController2
拿到请求的URI,将URI拼成对应的文件路径,然后读取这个文件。
- package com.example.mockserver.controller;
-
-
- import org.apache.commons.io.IOUtils;
- import org.apache.commons.lang3.StringUtils;
- 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.IOException;
-
- @RestController
- public class MockController2 {
- // 文件存放路径,常量
- public static final String MOCK_DATA_PATH = "mock_data";
- // 注入
- @Autowired
- private HttpServletRequest request;
-
- @RequestMapping("/**")
- public String doMock() throws IOException {
- // 获取请求的URI
- String requestURI = request.getRequestURI();
- // 将输入的URI,拼成文件的名称
- // /get/order/info => get_order_info
- // 取/get/order/info ,第一个/后面的数据,get/order/info
- requestURI =StringUtils.substringAfter(requestURI,"/");
- // 将/替换成_,将uri拼成文件名称
- String fileName = StringUtils.replace(requestURI, "/", "_");
- // 拼成文件路径
- String filePath = MOCK_DATA_PATH+"/"+fileName;
- // 读取文件
- String response = IOUtils.toString(MockController2.class.getClassLoader().getResourceAsStream(filePath));
- return response;
- }
- }
访问 http://127.0.0.1:8081/get/order/info
注意:
我们这里存放mock文件,放到了项目的resources目录下。正常情况下,是不可以这么写的。当我们打包时,会将这个文件一起打进去,这样再修改数据不好修改了。
这个路径应该写磁盘上的某个路径。
方法一,支持用户配置自己的路径
方法二,默认路径,为磁盘上的某个位置,比如“/user/local/aa"
场景一
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文件
- mappingHost: 127.0.0.1
- mappingParams:
- - params:
- id: 123
- weight: 2
- - params:
- name: "zhangsan"
- weight: 4
- response: 'AAA'
b文件
- mappingHost: 127.0.0.1
- mappingParams:
- - params:
- id: 456
- weight: 2
- - params:
- name: "zhangsan"
- weight: 4
- response: 'BBBB'
c文件
- mappingHost: 127.0.0.1
- mappingParams:
- - params:
- id: 123
- weight: 2
- - params:
- name: "lisi"
- weight: 4
- response: 'CCCC'
d文件
- mappingHost: 127.0.0.1
- mappingParams:
- - params:
- id: 456
- weight: 2
- - params:
- name: "lisi"
- weight: 4
- response: 'DDDD'
举例:
当传参id=123&name=zhangsan时,遍历这四个文件。
a文件值为2+4=6
b文件值为4
c文件值为2
d文件值为0
我们取最大的值返回,a文件的值,最大,我们则把a文件返回。
返回值的模版
- mappingHost: 127.0.0.1
- mappingParams:
- - params:
- id: 123
- weight: 2
- - params:
- name: "zhangsan"
- weight: 4
- response: '{"key1":"${random:id}","key2":"value2","count":3,"person":[{"id":1,"name":"张三"},{"id":2,"name":"李四"}],"object":{"id":1,"msg":"对象里的对象"}}'
返回值,是一个yml格式的,里面不仅仅包含了返回值response,还包含了传参数。 这里参数是为了匹配哪个文件而设置的。还包含了发起请求的ip
1、简单的接口,对应一个文件,直接读取并返回。复杂接口,计算权重,返回大的文件。
2、得到用户的请求。包含ip,传参,URI。
URL用来匹配文件夹,传参用来计算权重,匹配具体的文件
3、构造用户的请求类。用来储存,传递 用户的请求
4、构造返回值的类,用来储存和计算比较权重,返回权重大的数据。
我们根据用户传进来的ip、传参数和URI返回不同的数据。所以我们建一个MockContext类,用来存储传递这些用户信息。
MockContext
- package com.example.mockserver.model;
-
- import lombok.Builder;
- import lombok.Data;
- import org.apache.commons.lang3.StringUtils;
-
- import java.util.Map;
-
- @Data
- @Builder
- public class MockContext {
- private String requestURI;
- private Map<String,String> requestParams;
- private String requestIp;
-
- public String getFileName(){
- String str = StringUtils.substringAfter(this.requestURI, "/");
- String fileName = StringUtils.replace(str, "/", "_");
- return fileName;
- }
- }
构造MockContext的对象,建造者模式,直接往里面填数据
- // 将获取的用户数据 ip 参数 URI ,存储到 mockContext 这个类里
- MockContext mockContext = MockContext.builder()
- .requestIp(remoteAddr)
- .requestParams(collect)
- .requestURI(uri)
- .build();
我们先查看返回值的模版,是yml格式的。
- mappingHost: 127.0.0.1
- mappingParams:
- - params:
- id: 123
- weight: 2
- - params:
- name: "zhangsan"
- weight: 4
- 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
- package com.example.mockserver.model;
-
- import lombok.Data;
-
- import java.util.Map;
-
- @Data
- public class MappingParamsEntity {
- private Map<String,String> params;
- private int weight;
-
- }
再构建MockDataInfo 这个类
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;
-
-
- }
构造MockDataInfo类的对象。这里就是读取文件,将文件内容封装在这个类里。
这里我们写一个读取文件,将文件内容转换成这个类的方法。YamlUtil
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 {
- // 这个工具的作用就是,读取返回值的文件,然后转换成一个对象
- 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);
- }
- }
因为我们文件内容本身是yml格式的,这里我们利用snakeyaml模块的能力,可以直接读取文件内容,并转我们想要的对象。
T t = yaml.loadAs(new FileInputStream(path), cls);