專案增長時,手動維護 API 文檔極其痛苦。利用 Spring 的 RequestMappingHandlerMapping,配合 JUnit 測試,可自動掃描專案中所有 @RequestMapping 映射,輸出結構化 URL 清單,支援 Markdown 表格、JSON、文檔生成等多種格式。
RequestMappingHandlerMapping 是 Spring MVC 核心組件,啟動時掃描所有 @Controller/@RestController 的 @RequestMapping 方法,建立 RequestMappingInfo → HandlerMethod 映射表。我們只需在測試中注入該 Bean,遍歷即可獲得完整路由資訊。
src/test/resources/config/spring/simple-test.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 手動註冊 HandlerMapping 與 Adapter,避免衝突 -->
<bean id="requestMappingHandlerMapping"
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"></bean>
<bean id="requestMappingHandlerAdapter"
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"></bean>
</beans>
package com.xxx.msa.test;
import java.util.Map;
import java.util.Map.Entry;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
public class UrlMappingTest {
@Autowired
private static RequestMappingHandlerAdapter handlerAdapter;
@Autowired
private static RequestMappingHandlerMapping handlerMapping;
@BeforeClass
public static void setUp() {
// 載入完整應用上下文:主配置 + MVC 配置 + 測試配置
ApplicationContext ctx = new ClassPathXmlApplicationContext(new String[]{
"classpath*:config/spring/spring-main.xml",
"classpath*:config/spring/springmvc/springMVC.xml",
"classpath*:config/spring/simple-test.xml"
});
handlerAdapter = ctx.getBean("requestMappingHandlerAdapter", RequestMappingHandlerAdapter.class);
handlerMapping = ctx.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
}
@Test
public void printAllUrls() {
Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
System.out.println("=== 所有 Controller 映射 URL ===");
for (Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
RequestMappingInfo mapping = entry.getKey();
HandlerMethod method = entry.getValue();
String className = method.getBeanType().getSimpleName();
String methodName = method.getMethod().getName();
for (String urlPattern : mapping.getPatternsCondition().getPatterns()) {
System.out.printf("%s#%s <-- %s%n", className, methodName, urlPattern);
}
}
}
}
@Test
public void exportMarkdownTable() {
Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
StringBuilder sb = new StringBuilder();
sb.append("# MSA 系統 API 文檔\n\n");
sb.append("| 序號 | Controller | 方法 | URI | 說明 |\n");
sb.append("|:----:|:----------:|:----:|:---:|:----:|\n");
int index = 1;
for (Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
RequestMappingInfo mapping = entry.getKey();
HandlerMethod method = entry.getValue();
String className = method.getBeanType().getSimpleName();
String methodName = method.getMethod().getName();
// 可選:從方法註解獲取說明
String description = method.getMethod().getAnnotation(ApiOperation.class) != null
? method.getMethod().getAnnotation(ApiOperation.class).value()
: "";
for (String urlPattern : mapping.getPatternsCondition().getPatterns()) {
sb.append(String.format("| %d | %s | %s | %s | %s |\n",
index++, className, methodName, urlPattern, description));
}
}
System.out.println(sb.toString());
// 可選:寫入檔案
// Files.write(Paths.get("API_DOC.md"), sb.toString().getBytes(StandardCharsets.UTF_8));
}
| 序號 | Controller | 方法 | URI | 說明 |
|---|---|---|---|---|
| 1 | GageController | create | /Gage/create | |
| 2 | GageController | update | /Gage/update | |
| 3 | GageController | delete | /Gage/delete/{gageId} | |
| 4 | GageController | findByGageId | /Gage/{gageId} | |
| 5 | GrrAnalysisController | compute | /GrrAnalysis/compute | |
| 6 | LoginController | login | /Login/login | |
| 7 | IndexController | toindex | /toindex |
// 僅導出 /api/** 路徑
if (urlPattern.startsWith("/api/")) {
// 處理...
}
ApiOperation apiOp = method.getMethod().getAnnotation(ApiOperation.class); ApiImplicitParams params = method.getMethod().getAnnotation(ApiImplicitParams.class); // 解析參數、回應碼、範例等
List<Map<String, Object>> apiList = new ArrayList<>();
for (Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
Map<String, Object> api = new HashMap<>();
api.put("class", entry.getValue().getBeanType().getName());
api.put("method", entry.getValue().getMethod().getName());
api.put("urls", entry.getKey().getPatternsCondition().getPatterns());
api.put("httpMethods", entry.getKey().getMethodsCondition().getMethods());
api.put("params", entry.getKey().getParamsCondition().getExpressions());
apiList.add(api);
}
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(apiList);
Spring Boot 專案簡化版
@SpringBootTest @AutoConfigureMockMvc class UrlMappingTest { @Autowired private RequestMappingHandlerMapping handlerMapping; @Test void printUrls() { handlerMapping.getHandlerMethods().forEach((info, method) -> { info.getPatternsCondition().getPatterns().forEach(pattern -> System.out.println(method.getBeanType().getSimpleName() + "#" + method.getMethod().getName() + " <-- " + pattern)); }); } }最佳實踐建議
- 納入 CI 流程:每次構建自動生成最新 API 文檔,避免文檔滯後
- 版本控制:生成的 Markdown/JSON 納入 Git,歷史可追溯
- 權限管控:敏感內部 API 標記 internal,文檔導出時過濾
- 整合註解:強制要求 Controller 方法加
@ApiOperation,提升文檔質量- 多環境支援:區分 dev/test/prod 環境基礎路徑
常見問題
| 問題 | 解決 |
|---|---|
| 掃描不到 Controller | 確認 @ComponentScan 基礎包路徑、測試上下文載入完整配置 |
| 重複 URL 出現 | 同一方法多個 @RequestMapping、類級與方法級組合,去重處理 |
| 啟動慢 | 只載入 Web 層 (@WebMvcTest) 或使用 Spring Boot 切片測試 |
| 參數解析異常 | 複雜參數類型 (Map、自定義物件) 需自定義解析邏輯 |
利用 RequestMappingHandlerMapping 反射 Spring MVC 內部路由表,配合 JUnit 測試,零侵入實現 API 文檔自動化。這不僅解決「文檔與代碼不同步」痛點,更能作為架構治理工具:審計未授權端點、檢測路由衝突、統計 API 複雜度。建議將此能力封裝為團隊通用測試基礎設施,納入標準化交付流程。