后端 Mock 实战:Spring Boot 3 实现入站 & 出站接口模拟

后端 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...


转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人

持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。转载请携带链接,转载到微信公众号请勿选择原创,谢谢!

👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍

谢谢支持!

相关推荐
biyezuopinvip3 小时前
基于Spring Boot的企业网盘的设计与实现(任务书)
java·spring boot·后端·vue·ssm·任务书·企业网盘的设计与实现
UrbanJazzerati3 小时前
Python编程基础:类(class)和构造函数
后端·面试
脸大是真的好~3 小时前
EasyExcel的使用
java·excel
小宋10213 小时前
Java 项目结构 vs Python 项目结构:如何快速搭一个可跑项目
java·开发语言·python
楚兴3 小时前
MacBook M1 安装 OpenClaw 完整指南
人工智能·后端
JavaGuide3 小时前
一款悄然崛起的国产规则引擎,让业务编排效率提升 10 倍!
java·spring boot
吃虫子的人4 小时前
记录使用Arthas修改线上源码重新加载的一次过程
java·arthas
Java编程爱好者4 小时前
2026版Java面试八股文总结(春招+秋招+社招),建议收藏。
后端