0
点赞
收藏
分享

微信扫一扫

spring boot 实现mock平台


注意:

直接下载源码,运行时会报错,因为读取文件路径,我写成了绝对路径,大家需要手动改一下成自己的路径。修改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通

spring boot 实现mock平台_spring

 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 文件。存放不同的内容。

spring boot 实现mock平台_前端_02

 新建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。

spring boot 实现mock平台_前端_03

我们先构造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:

spring boot 实现mock平台_前端_04

所有的参数,是一个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":"对象里的对象"}}

这第一阶段就完成了。 

 下一章: 




举报

相关推荐

0 条评论