前後端分離開發中,後端接口未就緒時,前端需要可用的 Mock 數據。本文實現一個通用的 /mock/{module}/{action} 端點,自動讀取 classpath:mock/{module}/{action}.json 文件並返回解析後的 JSON 節點,支援熱加載、多環境隔離、優雅降級。
@RestController
@RequestMapping("/mock")
public class MockController {
private final ObjectMapper objectMapper = new ObjectMapper();
private final ResourceLoader resourceLoader;
public MockController(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@PostMapping("/{module}/{action}")
public JsonNode mockContent(@PathVariable String module,
@PathVariable String action,
HttpServletRequest request,
HttpServletResponse response) {
String resourcePath = "classpath:mock/" + module + "/" + action + ".json";
Resource resource = resourceLoader.getResource(resourcePath);
if (!resource.exists()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return objectMapper.createObjectNode()
.put("error", "Mock file not found: " + resourcePath)
.put("module", module)
.put("action", action);
}
try (InputStream is = resource.getInputStream()) {
// 支援熱加載:每次請求重新讀取,開發期修改即時生效
JsonNode jsonNode = objectMapper.readTree(is);
// 可選:注入請求上下文 (參數、Header、Session) 實現動態 Mock
// enhanceWithRequestContext(jsonNode, request);
return jsonNode;
} catch (IOException e) {
log.error("讀取 Mock 文件失敗: {}", resourcePath, e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return objectMapper.createObjectNode()
.put("error", "Failed to parse mock JSON: " + e.getMessage());
}
}
}
src/main/resources/
└── mock/
├── user/
│ ├── login.json
│ ├── profile.json
│ └── list.json
├── order/
│ ├── create.json
│ └── detail.json
└── product/
└── search.json
{
"code": 200,
"message": "success",
"data": {
"id": 1001,
"username": "mock_user",
"roles": ["ADMIN", "USER"],
"permissions": ["user:read", "user:write"]
},
"timestamp": 1699999999999
}
private void enhanceWithRequestContext(JsonNode node, HttpServletRequest request) {
if (node.isObject()) {
ObjectNode obj = (ObjectNode) node;
// 注入請求參數
Map paramMap = request.getParameterMap();
ObjectNode params = objectMapper.createObjectNode();
paramMap.forEach((k, v) -> params.putArray(k).addAll(Arrays.stream(v)
.map(objectMapper::valueToTree).collect(Collectors.toList())));
obj.set("__request_params", params);
// 注入 Header
ObjectNode headers = objectMapper.createObjectNode();
Collections.list(request.getHeaderNames()).forEach(h ->
headers.put(h, request.getHeader(h)));
obj.set("__request_headers", headers);
}
}
優雅降級策略
| 異常情況 | HTTP 狀態碼 | 響應體 |
|---|---|---|
| Mock 文件不存在 | 404 | {"error": "Mock file not found: ...", "module": "...", "action": "..."} |
| JSON 解析失敗 | 500 | {"error": "Failed to parse mock JSON: ..."} |
| IO 讀取異常 | 500 | {"error": "Failed to read mock resource: ..."} |
| 生產環境誤調用 | 403 | {"error": "Mock service disabled in production"} |
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 本文方案 (ClassPath JSON) | 零依賴、輕量、熱加載、版本控制友好 | 無動態邏輯、無狀態模擬、無契約驗證 | 前端並行開發、接口文檔驅動、輕量級項目 |
| WireMock | 獨立服務、錄製回放、狀態機、延遲/故障注入 | 需額外部署、配置複雜、Java DSL 學習成本 | 集成測試、消費者驅動契約、微服務 Mock |
| Spring Cloud Contract | 契約優先、生產者驗證、Stub 自動發布 | 學習曲線陡峭、Groovy DSL、構建流程綁定 | 微服務治理、消費者驅動契約測試 (CDC) |
{domain}/{usecase}.json,如 order/create.json「最好的 Mock 服務是『零代碼、零部署、零維護』—— 只要約定好目錄結構與命名規範,前後端協作效率可提升 30% 以上。這個 50 行的控制器,就是這種極簡哲學的具體實踐。」