本篇主要说明一些主要实现思路,不会放太多代码
主要基于 mock-sever,官方地址https://www.mock-server.com/
maven
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-netty</artifactId>
<version>5.11.1</version>
</dependency>
数据库表结构
CREATE TABLE `t_mock_api` (
`api_id` int NOT NULL AUTO_INCREMENT,
`server_id` int DEFAULT NULL,
`desc` varchar(30) DEFAULT NULL COMMENT '描述',
`url` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '请求地址',
`method` varchar(20) DEFAULT NULL COMMENT '请求方式',
`response_code` int DEFAULT NULL COMMENT '状态码',
`response_headers` varchar(1000) DEFAULT NULL COMMENT '响应头',
`response_body` mediumtext COMMENT '响应body',
`response_delay` int DEFAULT NULL COMMENT '响应时延ms',
`status` tinyint DEFAULT NULL COMMENT '0启用1禁用',
`created_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`response_headers_enable_rely` tinyint DEFAULT NULL COMMENT '0是1否 是否解析headers依赖',
`response_body_enable_rely` tinyint DEFAULT NULL COMMENT '0是1否 是否解析body依赖',
`creator_id` int DEFAULT NULL COMMENT '创建人id',
`creator_name` varchar(30) DEFAULT NULL COMMENT '创建人名称',
`response_body_type` tinyint DEFAULT NULL COMMENT '0文本1json2xml3html',
PRIMARY KEY (`api_id`)
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8;
CREATE TABLE `t_mock_hit_policy` (
`id` int NOT NULL AUTO_INCREMENT,
`api_id` int DEFAULT NULL COMMENT 'api_id',
`match_scope` tinyint DEFAULT NULL COMMENT '0请求头1请求body2pathparams3请求queryparams',
`match_type` tinyint DEFAULT NULL COMMENT '0固定值1包含2正则3jsonschema4xpath5jsonpath',
`name` varchar(200) DEFAULT NULL COMMENT '名称',
`value` varchar(200) DEFAULT NULL COMMENT '值',
`status` tinyint DEFAULT NULL COMMENT '0启用1禁用',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8;
CREATE TABLE `t_mock_sever` (
`server_id` int NOT NULL AUTO_INCREMENT,
`port` int DEFAULT NULL COMMENT '端口号',
`remote_host` varchar(30) DEFAULT NULL COMMENT '当没有命中时转发的主机地址',
`remote_port` int DEFAULT NULL COMMENT '当没有命中时转发的端口',
`desc` varchar(200) DEFAULT NULL COMMENT '描述',
`creator_id` int DEFAULT NULL COMMENT '创建人user_id',
`creator_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '创建人realname',
`created_time` datetime DEFAULT NULL COMMENT 'update_time',
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`server_id`),
UNIQUE KEY `port` (`port`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
多节点即指在同一台服务器上开启多台 mock-server,避免人员之间交叉维护同一个接口,主要的实现方式是类似一个 “池” 的概念,有即直接取,无即创建。
private static class SinglePool {
private static final HashMap<String, ClientAndServer> INSTANCE = new HashMap<>(16);
}
public static HashMap<String, ClientAndServer> getInstance() {
return SinglePool.INSTANCE;
}
/**
* 启动MockServer
* @param port 端口
* @return ClientAndServer
* @throws BusinessException 端口被占用
*/
public static ClientAndServer start(Integer port) throws BusinessException {
if (port == null) {
throw new BusinessException("端口号不能为空");
}
HashMap<String, ClientAndServer> instance = MockServerPool.getInstance();
ClientAndServer mockServerClient;
try {
mockServerClient = new ClientAndServer(port);
} catch (Exception e) {
throw new BusinessException("启动失败,请更换端口重试");
}
instance.put(port.toString(), mockServerClient);
return mockServerClient;
}
/**
* 判断端口是否已启用MockServer
* @param port 端口
* @return boolean
*/
public static boolean isRunning(Integer port) {
if (port == null) {
throw new IllegalArgumentException("端口号不能为空");
}
HashMap<String, ClientAndServer> instance = MockServerPool.getInstance();
ClientAndServer hit = instance.get(port.toString());
if (hit == null) {
return false;
} else {
return hit.getPort().equals(port);
}
}
/**
* 停止指定端口的MockServer
* @param port 端口
*/
public static void stop(Integer port) {
if (port == null) {
throw new IllegalArgumentException("端口号不能为空");
}
HashMap<String, ClientAndServer> instance = MockServerPool.getInstance();
ClientAndServer hit = instance.get(port.toString());
if (hit != null) {
if (isRunning(port)) {
hit.stop(true);
instance.remove(port.toString());
}
}
}
/**
* 获取一个mock sever, 端口已经启动则直接返回;否则创建再返回
* @param port 端口
* @return ClientAndServer
* @throws BusinessException 端口被占用
*/
public static ClientAndServer get(Integer port) throws BusinessException {
HashMap<String, ClientAndServer> instance = MockServerPool.getInstance();
if (isRunning(port)) {
ClientAndServer hit = instance.get(port.toString());
InetSocketAddress remoteAddress = hit.getRemoteAddress();
if (remoteAddress == null) {
return hit;
} else {
hit.stop(true);
return start(port);
}
} else {
return start(port);
}
}
public static ClientAndServer justGet(Integer port) throws BusinessException {
HashMap<String, ClientAndServer> instance = MockServerPool.getInstance();
ClientAndServer server = instance.get(String.valueOf(port));
if (server == null) {
throw new BusinessException("获取mock server失败");
} else {
return server;
}
}
根据前端的不同操作,后端做出不同响应
新增:根据绑定的 mock-server 获取其端口号,从节点池获取 ClientAndServer,仅 ClientAndServer 为运行时,才做新增和注入 api 操作;否则只新增数据,无需向 mock-server 注入一个 api(因为它未运行)
修改:修改分为三个步骤:删除修改之前 mock-api、修改数据库数据、启动修改后的 mock-api。
/**
* 清空mock server 下指定路径的api
* @param port 指定端口
* @param path path
*/
public static void clearByPath(Integer port, String path) {
if (isRunning(port)) {
HashMap<String, ClientAndServer> instance = MockServerPool.getInstance();
ClientAndServer clientAndServer = instance.get(String.valueOf(port));
if (clientAndServer != null) {
clientAndServer.clear(HttpRequest.request().withPath(path));
}
}
}
mock-server 提供了报告,其访问地址为http://host:port/mockserver/dashboard
mock-server 提供了很多种命中策略,我只对一些常用的进行了封装 header、body、queryparams、pathparams。这一块可以去官网查看
在创建 ClientAndServer 对象时,提供了两种构造方法。第二个构造方法,Integer remotePort, Integer... ports,可配置在未命中 mock-api 时支持自动转发真实服务。
public ClientAndServer(Integer... ports) {
super(new CompletableFuture());
this.mockServer = new MockServer(ports);
this.completePortFutureAndOpenUI();
}
public ClientAndServer(String remoteHost, Integer remotePort, Integer... ports) {
super(new CompletableFuture());
this.mockServer = new MockServer(remotePort, remoteHost, ports);
this.completePortFutureAndOpenUI();
}
HttpResponse.withDelay(TimeUnit.MILLISECONDS, ms) // 可指定接口响应延时
节点管理
api 管理
运行
可以看出,mock-server 以及 mock-api 都存在状态,而数据库表结构未存在状态字段,主要是由于状态存在与内存,那么如何判断 mock-server 以及 mock-api 是启动状态的呢?
获取 mock-server 状态
public PageInfo<MockServerVO> findMockServer(MockServerDTO mockServerDTO, Integer pageNum, Integer pageSize) {
PageHelper.startPage(pageNum, pageSize);
PageInfo<MockServerVO> pages = new PageInfo<>(mockServerMapper.selectMockServer(mockServerDTO));
List<MockServerVO> result = pages.getList().stream().peek(page -> {
Integer port = page.getPort();
byte status = (byte) (MockServerPool.isRunning(port) ? 0 : 1);
page.setStatus(status);
}).collect(Collectors.toList());
pages.setList(result);
return pages;
}
public static boolean isRunning(Integer port) {
if (port == null) {
throw new IllegalArgumentException("端口号不能为空");
}
HashMap<String, ClientAndServer> instance = MockServerPool.getInstance();
ClientAndServer hit = instance.get(port.toString());
if (hit == null) {
return false;
} else {
return hit.getPort().equals(port);
}
}
获取 mock-api 状态。clientAndServer.retrieveActiveExpectations,返回的数组序列化能看到目前所有运行的 api
@Override
public PageInfo<MockApiListVO> findMockApiList(MockApiDTO mockApiDTO, Integer pageNum, Integer pageSize) {
PageHelper.startPage(pageNum, pageSize);
PageInfo<MockApiListVO> pages = new PageInfo<>(mockApiMapper.selectMockApiList(mockApiDTO));
List<MockApiListVO> result = pages.getList().stream().peek(page -> {
Integer port = page.getPort();
Integer apiId = page.getApiId();
String method = page.getMethod();
String url = page.getUrl();
List<MockHitPolicyVO> policies = mockHitPolicyService.findMockHitPolicyByApiId(apiId);
boolean running = MockServerPool.isRunning(port);
page.setPortRunning(running);
try {
HttpRequest httpRequest = injectionCenter.injectRequest(new HttpRequest(), method, url, policies);
page.setApiRunning(MockServerPool.apiIsRunning(port, httpRequest, url));
} catch (BusinessException e) {
page.setApiRunning(false);
}
}).collect(Collectors.toList());
pages.setList(result);
return pages;
}
public static boolean apiIsRunning(Integer port, HttpRequest request, String url) {
ArrayList<String> pathList = new ArrayList<>();
if (!isRunning(port)) {
return false;
}
try {
ClientAndServer clientAndServer = justGet(port);
Expectation[] expectations = clientAndServer.retrieveActiveExpectations(request);
JSONArray array = JSONArray.parseArray(JSON.toJSONString(expectations));
for (int i = 0; i < array.size(); i++) {
JSONObject jsonObject = array.getJSONObject(i);
JSONObject httpRequest = jsonObject.getJSONObject("httpRequest");
JSONObject path = httpRequest.getJSONObject("path");
String pathValue = path.getString("value");
pathList.add(pathValue);
}
} catch (Exception e) {
return false;
}
return pathList.contains(url);
}