注意:
直接下载源码,运行时会报错,因为读取文件路径,我写成了绝对路径,大家需要手动改一下成自己的路径。修改MockConst文件
相对路径方法:
我要读取的绝对路径地址:
/Users/zhaohui/IdeaProjects/mock-server/src/main/resources/mock_data
改成相对路径:
String path = ClassUtils.getDefaultClassLoader().getResource("").getPath();
String MOCK_DATA_PATH = path + "mock_data";
使用spring boot 写mock平台。
源码地址:
GitHub - 18713341733/mockServer
一、实现场景:
1.1请求同一个接口,不同的传参/ip返回的结果不一样。
数据的匹配
以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.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了,我们需要去自己真实的拿着请求的参数,我们再去调真实服务。
拿到返回结果,在返回给调用端。
总结:
其实就是把数据源放在文件里,根据用户的传参,对数据进行处理,然后再返回。
二、整体架构&实现思路
1、收集用户输入信息,存到一个实体类里mockContext
2、将用户输入的URI,拼成一个路径。
路径是文件,直接返回文件内容
路径是目录,则读取这个目录下的所有文件。计算每个文件的权重,取出权重最大的返回结果
3、这里处理文件&文件夹用的是责任链模式
4、具体处理文件夹的逻辑,我们这里使用的是观察者模式。
这里用到了mockContext 这个实体类。mockContext 不仅存储了用户的输入数据,
还存储了根据接口读取的文件内容。
观察者模式,多个实体工具类,for循环处理 数据mockContext,处理完再将数据写入mockContext。 多个方法循环处理mockContext,这个mockContext是同一个变量。
二、依赖
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>
三、实现简单的demo
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"
四、实现思路&实体类的封装
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文件
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
4.2整体思路:
1、简单的接口,对应一个文件,直接读取并返回。复杂接口,计算权重,返回大的文件。
2、得到用户的请求。包含ip,传参,URI。
URL用来匹配文件夹,传参用来计算权重,匹配具体的文件
3、构造用户的请求类。用来储存,传递 用户的请求
4、构造返回值的类,用来储存和计算比较权重,返回权重大的数据。
4.3 构建MockContext类,存储传递用户请求
我们根据用户传进来的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();
4.4 构建MockDataInfo类,存储返回值
我们先查看返回值的模版,是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);
五、权重如何计算
我们需要计算这一个接口文件夹下,每个文件,用户命中的权重之和。然后返回权重最大的那一个。
(其实就是将传参得到一个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:
所有的参数,是一个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();
}
}
-----------------------------------------------------------
六、最终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.util.YamlUtil;
import org.apache.commons.io.FileUtils;
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.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
public class MockController4 {
public static final String MOCK_DATA_PATH= "/Users/zhaohui/IdeaProjects/mock-server/src/main/resources/mock_data";
@Autowired
private HttpServletRequest request;
@RequestMapping("/**")
public String doMock() throws IOException {
// 获取ip
String remoteAddr = request.getRemoteAddr();
// 获取请求的URI
String uri = request.getRequestURI();
// 获取参数,注意这里的value是一个数组[]
Map<String,String[]> parameterMap = request.getParameterMap();
// 获取用户的传参,value是一个数组。这里为了将来处理方便,我们将这数组转成一个字符串。
// 我们默认,这个数据的长度是1,那我们只需要取出来数组的第一个值就可以了。
Map<String,String> collect = getParams(parameterMap);
// 将获取的用户数据 ip 参数 URI ,存储到 mockContext 这个类里
MockContext mockContext = MockContext.builder()
.requestIp(remoteAddr)
.requestParams(collect)
.requestURI(uri)
.build();
// 计算权重的方法
// 用户的传参,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());
// 得到文件的路径
String filePath = MOCK_DATA_PATH+"/"+mockContext.getFileName();
File file = new File(filePath);
// 判断filePath是否是文件
if (FileUtil.isFile(filePath)) {
// 是文件,则直接读取文件内容
return FileUtils.readFileToString(file,"utf-8");
}
// 如果是文件夹,获取所有文件。是一个数组
// 取出所有的文件
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();
}
}
// 每一个文件的权重比较大小,最终返回权重最大的response
if(weight>weightResult){
weightResult = weight;
response = mockDataInfo.getResponse();
}
}
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 -> getFirst(e.getValue())));
return params;
}
// 数组取第一个值,封装了一个方法,做了异常处理。
public String getFirst(String[] arr){
if(arr.length==0 || arr == null){
return "";
}
return arr[0];
}
}
验证功能是否正确:
启动服务,
访问:http://127.0.0.1:8081/get/user?name=lisi
返回:DDDD
访问:http://127.0.0.1:8081/get/user?name=lisi&id=123
返回:CCC
这个是根据权重返回的。
访问:
http://127.0.0.1:8081/get/user/info
返回:test user info abc
这个是结果就是一个文件,直接读取的文件。
七、项目重构
如上,我们把处理逻辑全部写在了MockController4里,且判断用户输入的是文件还是文件夹,用if处理的。
我们要做的就是:
1、把处理逻辑摘出来,单独写一个service
2、用责任链模式处理文件和文件夹
责任链就是用来代替if的。
重构的意义,就是增强这个项目的扩展能力。
7.1 重写MockController
新建MockController
新建service包,接口MockService
package com.example.mockserver.service;
import com.example.mockserver.model.MockContext;
public interface MockService {
String doMock(MockContext mockContext);
}
进行mock的逻辑处理,传入MockContext,输出response
实现类MockServiceImpl
package com.example.mockserver.service;
import com.example.mockserver.model.MockContext;
import org.springframework.stereotype.Service;
@Service
public class MockServiceImpl implements MockService{
@Override
public String doMock(MockContext mockContext) {
return "no data";
}
}
将取数组第一个位的,封装成一个工具
ArrayUtil
package com.example.mockserver.util;
public class ArrayUtil {
// 数组取第一个值,封装了一个方法,做了异常处理。
public static String getFirst(String[] arr){
if(arr.length==0 || arr == null){
return "";
}
return arr[0];
}
}
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 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
public class MockController {
public static final String MOCK_DATA_PATH= "/Users/zhaohui/IdeaProjects/mock-server/src/main/resources/mock_data";
@Autowired
private HttpServletRequest request;
@Autowired
private MockService mockService;
@RequestMapping("/**")
public String doMock() throws IOException {
// 将获取的用户数据 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;
}
}
7.2 责任链设计
处理文件、处理文件夹
AbstractHandler
package com.example.mockserver.chain;
import com.example.mockserver.model.MockContext;
import lombok.Setter;
@Setter
public abstract class AbstractHandler {
// 属性是下一节链条
private AbstractHandler nextHandler;
// 当前链条是否能处理
protected abstract boolean preHandle(MockContext mockContext);
// 具体处理的逻辑
protected abstract String onHandle(MockContext mockContext);
// 总的模版处理逻辑
public String doHandle(MockContext mockContext){
// 能处理,直接处理
if (preHandle(mockContext)){
return onHandle(mockContext);
}
// 下一节链条处理
if (nextHandler != null){
return nextHandler.doHandle(mockContext);
}
// 所有链条都不能处理,抛出异常
throw new RuntimeException("no chain");
}
}
我们再改造一下AbstractHandler,使用泛型
package com.example.mockserver.chain;
import com.example.mockserver.model.MockContext;
import lombok.Setter;
@Setter
public abstract class AbstractHandler<T,R> {
// 属性是下一节链条
private AbstractHandler<T,R> nextHandler;
// 当前链条是否能处理
protected abstract boolean preHandle(T t);
// 具体处理的逻辑
protected abstract R onHandle(T t);
// 总的模版处理逻辑
public R doHandle(T t){
// 能处理,直接处理
if (preHandle(t)){
return onHandle(t);
}
// 下一节链条处理
if (nextHandler != null){
return nextHandler.doHandle(t);
}
// 所有链条都不能处理,抛出异常
throw new RuntimeException("no chain");
}
}
一共涉及到2个变量,AbstractHandler<T,R>
T代表,输入mockContent
R代表输出,String
单独定义一个接口,来存放常量
存放文件的路径
接口的属性,就是常量
package com.example.mockserver.consts;
public interface MockConst {
String MOCK_DATA_PATH= "/Users/zhaohui/IdeaProjects/mock-server/src/main/resources/mock_data";
}
在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.Map;
@Data
@Builder
public class MockContext {
private String requestURI;
private Map<String,String> requestParams;
private String requestIp;
// 根据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;
}
}
处理文件的实体类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");
}
}
处理文件夹的实体类DirectoryHandle
package com.example.mockserver.chain;
import cn.hutool.core.io.FileUtil;
import com.example.mockserver.model.MockContext;
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 "no data";
}
}
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);
}
}
修改 MockServiceImpl
package com.example.mockserver.service;
import com.example.mockserver.chain.ChainManager;
import com.example.mockserver.model.MockContext;
import org.springframework.stereotype.Service;
@Service
public class MockServiceImpl implements MockService{
@Override
public String doMock(MockContext mockContext) {
return ChainManager.of().doMapping(mockContext);
}
}
MockServiceImpl 这里,没有写构造器,这里它其实就是一个工具类。
在MockController 注入实体类的时候,它就是一个工具类,没有构造器。
(spring 注入实体类的时候,是不是只注入这种没有构造器的工具类?)
完善处理文件夹的方法 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);
}
}
7.3 具体处理文件夹的方法,这里做了一个观察者模式。
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;
}
}
八、 处理动态变量,使用了装饰器模式
在处理动态变量时,我们使用的观察者模式,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);
---------启动项目
访问:
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":"对象里的对象"}}
这第一阶段就完成了。
下一章: