后端 Mock 实战:Spring Boot 3 实现入站 & 出站接口模拟
😄生命不息,写作不止
🔥 继续踏上学习之路,学之分享笔记
👊 总有一天我也能像各位大佬一样
🏆 博客首页 @怒放吧德德 To记录领地 @一个有梦有戏的人
🌝分享学习心得,欢迎指正,大家一起学习成长!
转发请携带作者信息 @怒放吧德德(掘金) @一个有梦有戏的人(CSDN)

前言
一般来说 mock 只有前端上拥有,能够模拟 api 返回参数自行测试,那么后端是否也需要有这么一个功能呢?由于我们公司需要对接比较多的外部系统,并且上线时机不同,但是返回数据可以提前定制,那么就很需要这么一个 Backend Mock System,用来管理进站与出站的 mock 功能。所以我通过 kiro 设计了一款 mock 功能组件,目前只有 demo 阶段:基于Spring Boot 3和DDD架构的后端Mock功能系统,用于在开发和测试环境中模拟HTTP接口响应。仓库位于:gitee.com/liyongde/ja...
1 Mock 设计
1.1 核心功能
- 出站请求Mock(Outbound Mock):封装HTTP客户端,拦截本系统调用外部系统的请求
- 入站请求Mock(Inbound Mock):通过拦截器机制,拦截外部系统调用本系统的请求
- 数据库配置管理:通过数据库动态配置Mock规则,支持CRUD操作
- 智能缓存机制:内存缓存减少数据库访问,提升性能
- 灵活的启用/禁用:支持全局开关和单个配置的启用/禁用
1.2 技术特点
- DDD架构:清晰的领域驱动设计,分层明确
- Spring Boot 3:基于最新的Spring Boot框架
- Java 17:使用现代Java特性
- MyBatis-Plus:强大的持久层框架,提供灵活的SQL控制和优秀的性能
- Hutool工具库:简化HTTP请求处理
- TestContainers:容器化测试环境,确保测试环境与生产环境一致
1.3 SQL 设计
mock_config 这张表主要用来存放定义的 mock 数据。
sql
CREATE TABLE `mock_config` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`api_path` varchar(500) NOT NULL COMMENT '接口路径,如:/api/user/info',
`api_method` varchar(10) NOT NULL COMMENT '请求方法:GET, POST, PUT, DELETE',
`response_json` longtext COMMENT '返回的JSON数据',
`is_enabled` tinyint(1) DEFAULT '1' COMMENT '是否启用:true-启用,false-禁用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_api_method_path` (`api_path`,`api_method`) COMMENT '路径和方法的唯一约束',
KEY `idx_is_enabled` (`is_enabled`) COMMENT '启用状态索引',
KEY `idx_create_time` (`create_time`) COMMENT '创建时间索引'
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Mock配置表';
1.4 开发设计
基础 CRUD 不讲解,只介绍主要部分。
本次的设计分为了两种:
- 1 出战(通过定制 http 请求完成)
- 2 入站(通过拦截请求进行获取数据返回)
案例代码采用 DDD 分层架构
plain
Trial3-Mock-Design/
├── domain/ # 领域层
│ ├── model/ # 领域模型(实体)
│ └── repository/ # 仓储接口
├── application/ # 应用层
│ ├── service/ # 应用服务
│ └── dto/ # 数据传输对象
├── infrastructure/ # 基础设施层
│ ├── persistence/ # 持久化实现
│ └── config/ # 配置类
└── interfaces/ # 接口层
├── controller/ # REST控制器
├── interceptor/ # 拦截器
├── client/ # HTTP客户端封装
└── exception/ # 异常处理
2 Mock 开发
2.1 基础功能
MockConfigProperties.java 配置属性类
用来设置全局是否开启 mock 功能,以及缓存时间和日志开关。
java
/**
* Mock配置属性类
* 使用@ConfigurationProperties从application.yml中绑定mock配置
*
* 配置示例:
* mock:
* enabled: true
* cache-expiration-seconds: 300
* log-mock-usage: true
*/
@Component
@ConfigurationProperties(prefix = "mock")
@Data
public class MockConfigProperties {
/**
* 全局Mock功能开关
* 当设置为false时,系统将绕过所有Mock逻辑,直接执行实际请求
* 默认值:true
*/
private boolean enabled = true;
/**
* 缓存过期时间(秒)
* Mock配置在缓存中的存活时间,超过此时间后将从数据库重新加载
* 默认值:300秒(5分钟)
*/
private long cacheExpirationSeconds = 300;
/**
* 是否记录Mock使用日志
* 当设置为true时,系统将记录每次使用Mock数据的详细信息
* 默认值:true
*/
private boolean logMockUsage = true;
}
MockCacheService.java 缓存服务
java
/**
* Mock配置缓存服务
* 使用ConcurrentHashMap实现线程安全的内存缓存,提升Mock配置查询性能
* 支持缓存过期机制,确保数据的时效性
*/
@Service
@Slf4j
public class MockCacheService {
private final ConcurrentHashMap<String, CacheEntry<MockConfig>> cache;
private final long cacheExpirationMs;
/**
* 构造函数,初始化缓存和过期时间
*
* @param properties Mock配置属性,包含缓存过期时间配置
*/
public MockCacheService(MockConfigProperties properties) {
this.cache = new ConcurrentHashMap<>();
this.cacheExpirationMs = properties.getCacheExpirationSeconds() * 1000;
log.info("MockCacheService initialized with expiration time: {} ms", cacheExpirationMs);
}
/**
* 从缓存中获取Mock配置
* 如果缓存条目已过期,将自动移除并返回空
*
* @param cacheKey 缓存键,格式为 "apiPath:apiMethod"
* @return Optional包装的MockConfig,如果不存在或已过期则返回空
*/
public Optional<MockConfig> get(String cacheKey) {
CacheEntry<MockConfig> entry = cache.get(cacheKey);
if (entry == null) {
log.debug("Cache miss for key: {}", cacheKey);
return Optional.empty();
}
if (entry.isExpired(cacheExpirationMs)) {
log.debug("Cache entry expired for key: {}, removing from cache", cacheKey);
cache.remove(cacheKey);
return Optional.empty();
}
log.debug("Cache hit for key: {}", cacheKey);
return Optional.of(entry.getValue());
}
/**
* 将Mock配置放入缓存
*
* @param cacheKey 缓存键,格式为 "apiPath:apiMethod"
* @param config 要缓存的MockConfig对象
*/
public void put(String cacheKey, MockConfig config) {
CacheEntry<MockConfig> entry = new CacheEntry<>(config);
cache.put(cacheKey, entry);
log.debug("Cache updated for key: {}", cacheKey);
}
/**
* 使指定缓存条目失效(移除)
* 通常在Mock配置被更新或删除时调用
*
* @param cacheKey 要失效的缓存键
*/
public void invalidate(String cacheKey) {
cache.remove(cacheKey);
log.debug("Cache invalidated for key: {}", cacheKey);
}
/**
* 清空所有缓存条目
* 通常在需要强制刷新所有缓存时调用
*/
public void clear() {
int size = cache.size();
cache.clear();
log.info("Cache cleared, removed {} entries", size);
}
/**
* 缓存条目内部类
* 封装缓存值和时间戳,用于实现过期检查
*
* @param <T> 缓存值的类型
*/
private static class CacheEntry<T> {
private final T value;
private final long timestamp;
/**
* 构造函数,创建缓存条目并记录当前时间戳
*
* @param value 要缓存的值
*/
public CacheEntry(T value) {
this.value = value;
this.timestamp = System.currentTimeMillis();
}
/**
* 获取缓存的值
*
* @return 缓存的值
*/
public T getValue() {
return value;
}
/**
* 检查缓存条目是否已过期
*
* @param expirationMs 过期时间(毫秒)
* @return true如果已过期,否则返回false
*/
public boolean isExpired(long expirationMs) {
return System.currentTimeMillis() - timestamp > expirationMs;
}
}
}
2.2 封装 http 请求(出站)
通过封装 http 请求客户端,当每次需要调用外部系统的时候,通过此封装工具,将对应的 api 和参数传递过去,如果是需要 mock 返回,则不会调用 http,反之放行调用。
java
/**
* HTTP客户端封装器
* 封装Hutool的HTTP请求方法,提供出站Mock功能
*
* 工作流程:
* 1. 检查全局Mock开关
* 2. 从URL提取路径
* 3. 查询Mock配置
* 4. 如果Mock启用,返回Mock数据;否则发起实际HTTP请求
*
* 支持的HTTP方法:GET, POST, PUT, DELETE
*/
@Component
@Slf4j
public class HttpClientWrapper {
private final MockService mockService;
private final MockConfigProperties properties;
/**
* 构造函数,注入依赖
*
* @param mockService Mock服务
* @param properties Mock配置属性
*/
public HttpClientWrapper(MockService mockService, MockConfigProperties properties) {
this.mockService = mockService;
this.properties = properties;
}
/**
* 发起GET请求
*
* @param url 目标URL
* @return 响应内容
*/
public String get(String url) {
return executeRequest(url, "GET", null);
}
/**
* 发起POST请求
*
* @param url 目标URL
* @param body 请求体
* @return 响应内容
*/
public String post(String url, String body) {
return executeRequest(url, "POST", body);
}
/**
* 发起PUT请求
*
* @param url 目标URL
* @param body 请求体
* @return 响应内容
*/
public String put(String url, String body) {
return executeRequest(url, "PUT", body);
}
/**
* 发起DELETE请求
*
* @param url 目标URL
* @return 响应内容
*/
public String delete(String url) {
return executeRequest(url, "DELETE", null);
}
/**
* 执行HTTP请求的核心逻辑
*
* 流程:
* 1. 检查全局Mock开关,如果禁用则直接发起实际请求
* 2. 从URL提取路径
* 3. 查询Mock配置
* 4. 如果Mock配置存在且启用,返回Mock数据
* 5. 否则发起实际HTTP请求
*
* @param url 目标URL
* @param method HTTP方法
* @param body 请求体(可为null)
* @return 响应内容
*/
private String executeRequest(String url, String method, String body) {
// 检查全局Mock开关
if (!properties.isEnabled()) {
log.debug("Mock is globally disabled, making real request to {} {}", method, url);
return makeRealRequest(url, method, body);
}
try {
// 从URL提取路径
String path = extractPath(url);
// 查询Mock配置
Optional<MockConfig> mockConfig = mockService.getMockConfig(path, method);
// 如果Mock配置存在且启用,返回Mock数据
if (mockConfig.isPresent() && mockConfig.get().isActive()) {
if (properties.isLogMockUsage()) {
log.info("Mock response used for outbound {} {}", method, url);
}
return mockConfig.get().getResponseJson();
}
// Mock未启用或不存在,发起实际请求
log.debug("No active mock config found for {} {}, making real request", method, path);
return makeRealRequest(url, method, body);
} catch (Exception e) {
log.error("Error during mock check for {} {}, falling back to real request: {}",
method, url, e.getMessage());
return makeRealRequest(url, method, body);
}
}
/**
* 发起实际的HTTP请求
* 使用Hutool的HttpRequest工具类
*
* @param url 目标URL
* @param method HTTP方法
* @param body 请求体(可为null)
* @return 响应内容
* @throws IllegalArgumentException 如果HTTP方法不支持
*/
private String makeRealRequest(String url, String method, String body) {
log.debug("Making real HTTP request: {} {}", method, url);
try {
String response;
switch (method.toUpperCase()) {
case "GET":
response = HttpRequest.get(url).execute().body();
break;
case "POST":
response = HttpRequest.post(url).body(body).execute().body();
break;
case "PUT":
response = HttpRequest.put(url).body(body).execute().body();
break;
case "DELETE":
response = HttpRequest.delete(url).execute().body();
break;
default:
log.error("Unsupported HTTP method: {}", method);
throw new IllegalArgumentException("Unsupported HTTP method: " + method);
}
log.debug("Real HTTP request completed: {} {}, response length: {}",
method, url, response != null ? response.length() : 0);
return response;
} catch (Exception e) {
log.error("Error making real HTTP request to {} {}: {}", method, url, e.getMessage(), e);
throw new RuntimeException("Failed to make HTTP request: " + e.getMessage(), e);
}
}
/**
* 从完整URL中提取路径部分
*
* 例如:
* - "http://example.com/api/user/info" -> "/api/user/info"
* - "https://example.com:8080/api/data?id=1" -> "/api/data"
*
* @param url 完整URL
* @return URL的路径部分
* @throws IllegalArgumentException 如果URL格式无效
*/
private String extractPath(String url) {
try {
URI uri = new URI(url);
String path = uri.getPath();
if (path == null || path.isEmpty()) {
log.warn("URL has no path component: {}, using root path '/'", url);
return "/";
}
log.debug("Extracted path '{}' from URL '{}'", path, url);
return path;
} catch (URISyntaxException e) {
log.error("Invalid URL format: {}", url, e);
throw new IllegalArgumentException("Invalid URL: " + url, e);
}
}
}
调用 mock 服务,主要是以下代码
java
// 从URL提取路径
String path = extractPath(url);
// 查询Mock配置
Optional<MockConfig> mockConfig = mockService.getMockConfig(path, method);
通过 mock 配置的判断获取对应数据,先查缓存,在查数据库,一定程度上优化。
java
/**
* 获取Mock配置(集成缓存查询)
* 优先从缓存获取,缓存未命中时从数据库查询并更新缓存
*
* @param apiPath API路径
* @param apiMethod HTTP方法
* @return Optional包装的MockConfig
*/
public Optional<MockConfig> getMockConfig(String apiPath, String apiMethod) {
String cacheKey = apiPath + ":" + apiMethod;
// 先查缓存
Optional<MockConfig> cached = cacheService.get(cacheKey);
if (cached.isPresent()) {
log.debug("Mock config found in cache for {} {}", apiMethod, apiPath);
return cached;
}
// 缓存未命中,查数据库
Optional<MockConfig> config = mockConfigRepository.findByApiPathAndApiMethod(apiPath, apiMethod);
// 如果找到,更新缓存
config.ifPresent(c -> {
cacheService.put(cacheKey, c);
log.debug("Mock config loaded from database and cached for {} {}", apiMethod, apiPath);
});
if (config.isEmpty()) {
log.debug("Mock config not found for {} {}", apiMethod, apiPath);
}
return config;
}
2.3 Mock 拦截器(出站)
定义一个拦截器,没被过滤的 api 请求将会到这里进行拦截 mock 处理
java
/**
* Mock拦截器
* 用于拦截入站HTTP请求,根据Mock配置返回Mock数据
* 实现HandlerInterceptor接口,在Controller方法执行前进行拦截
*
* 工作流程:
* 1. 检查全局Mock开关是否启用
* 2. 提取请求路径和HTTP方法
* 3. 查询Mock配置(优先从缓存获取)
* 4. 如果Mock配置存在且启用,直接返回Mock数据
* 5. 否则,继续执行Controller方法
*/
@Component
@Slf4j
public class MockInterceptor implements HandlerInterceptor {
private final MockService mockService;
private final MockConfigProperties properties;
/**
* 构造函数,注入依赖
*
* @param mockService Mock服务
* @param properties Mock配置属性
*/
public MockInterceptor(MockService mockService, MockConfigProperties properties) {
this.mockService = mockService;
this.properties = properties;
}
/**
* 在Controller方法执行前拦截请求
*
* @param request HTTP请求
* @param response HTTP响应
* @param handler 处理器
* @return true表示继续执行Controller,false表示拦截并返回Mock数据
* @throws Exception 处理过程中的异常
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 检查全局Mock开关
if (!properties.isEnabled()) {
log.debug("Mock functionality is globally disabled, continuing to controller");
return true; // Mock功能禁用,继续执行Controller
}
// 提取请求路径和方法
String path = request.getRequestURI();
String method = request.getMethod();
log.debug("Intercepting request: {} {}", method, path);
// 查询Mock配置
Optional<MockConfig> mockConfig = mockService.getMockConfig(path, method);
// 检查Mock配置是否存在且启用
if (mockConfig.isPresent() && mockConfig.get().isActive()) {
MockConfig config = mockConfig.get();
// 设置响应头
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.OK.value());
// 写入Mock响应数据
response.getWriter().write(config.getResponseJson());
response.getWriter().flush();
// 记录Mock使用日志
if (properties.isLogMockUsage()) {
log.info("Mock response returned for {} {} (config id: {})",
method, path, config.getId());
}
return false; // 拦截请求,不继续执行Controller
}
// Mock配置不存在或未启用,继续执行Controller
log.debug("No active mock config found for {} {}, continuing to controller", method, path);
return true;
}
}
主要也是调用了 Optional<MockConfig> mockConfig = mockService.getMockConfig(path, method);mock 服务。
通过 mvc 拦截,记得将 mock 的 crud 排除,也可以制作白名单数组,将不需要的 api 直接过滤掉。
java
/**
* Web MVC配置类
* 用于注册拦截器和配置拦截路径
*
* 拦截器配置:
* - 拦截所有路径(/**)
* - 排除Mock配置管理接口(/api/mock-config/**),避免管理接口被Mock拦截
*/
@Configuration
@Slf4j
public class WebMvcConfig implements WebMvcConfigurer {
private final MockInterceptor mockInterceptor;
/**
* 构造函数,注入MockInterceptor
*
* @param mockInterceptor Mock拦截器
*/
public WebMvcConfig(MockInterceptor mockInterceptor) {
this.mockInterceptor = mockInterceptor;
}
/**
* 注册拦截器
* 配置拦截路径和排除路径
*
* @param registry 拦截器注册器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
log.info("Registering MockInterceptor");
registry.addInterceptor(mockInterceptor)
.addPathPatterns("/**") // 拦截所有路径
.excludePathPatterns("/api/mock-config/**"); // 排除Mock配置管理接口
log.info("MockInterceptor registered successfully");
}
}
3 Mock 功能测试
代码仓库里面设有入站与出站的测试类,可以自行调试。
出站测试
java
@Test
void testOutboundMock_WhenMockConfigExistsAndEnabled_ReturnsMockData() {
// Given: 创建并保存Mock配置
MockConfig config = new MockConfig();
config.setApiPath("/api/user/info");
config.setApiMethod("GET");
config.setResponseJson("{\"id\":1,\"name\":\"Test User\"}");
config.setIsEnabled(true);
config.setCreateTime(LocalDateTime.now());
config.setUpdateTime(LocalDateTime.now());
repository.save(config);
// When: 通过HttpClientWrapper发起GET请求
String response = httpClientWrapper.get("http://example.com/api/user/info");
System.out.println(response);
// Then: 应该返回Mock数据
assertThat(response).isEqualTo("{\"id\":1,\"name\":\"Test User\"}");
}
入站测试
java
@Test
void testInboundMock_WhenMockConfigExistsAndEnabled_ReturnsInterceptedMockData() throws Exception {
// Given: 创建并保存Mock配置
MockConfig config = new MockConfig();
config.setApiPath("/api/test/endpoint");
config.setApiMethod("GET");
config.setResponseJson("{\"intercepted\":true,\"message\":\"Mock response\"}");
config.setIsEnabled(true);
config.setCreateTime(LocalDateTime.now());
config.setUpdateTime(LocalDateTime.now());
repository.save(config);
// When & Then: 发起GET请求,应该被拦截器拦截并返回Mock数据
mockMvc.perform(get("/api/test/endpoint"))
.andExpect(status().isOk())
.andExpect(content().contentType("application/json;charset=UTF-8"))
.andExpect(content().json("{\"intercepted\":true,\"message\":\"Mock response\"}"));
}
4 总结
本文是介绍作者基于Spring Boot 3和DDD架构的后端Mock功能系统,用于在开发和测试环境中模拟HTTP接口响应的一次尝试,这只是一个案例,可以根据里面的代码集成到各自的项目中,后面我在写一篇集成到后台管理系统中的案例过程。也可以先看我的仓库:gitee.com/liyongde/ja...
转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人
持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。转载请携带链接,转载到微信公众号请勿选择原创,谢谢!
👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍
谢谢支持!