首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Spring MVC 實戰:JUnit 自動掃描並輸出所有 Controller 映射 URL

     編輯:Java 綜合教學

Spring MVC 實戰:JUnit 自動掃描並輸出所有 Controller 映射 URL

專案增長時,手動維護 API 文檔極其痛苦。利用 Spring 的 RequestMappingHandlerMapping,配合 JUnit 測試,可自動掃描專案中所有 @RequestMapping 映射,輸出結構化 URL 清單,支援 Markdown 表格、JSON、文檔生成等多種格式。

核心原理

RequestMappingHandlerMapping 是 Spring MVC 核心組件,啟動時掃描所有 @Controller/@RestController 的 @RequestMapping 方法,建立 RequestMappingInfo → HandlerMethod 映射表。我們只需在測試中注入該 Bean,遍歷即可獲得完整路由資訊。

步驟一:測試專用 Spring 配置

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>

步驟二:JUnit 測試類實現

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);
            }
        }
    }
}

步驟三:進階輸出 - Markdown 表格文檔

@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));
}

輸出範例 (Markdown 渲染後)

序號Controller方法URI說明
1GageControllercreate/Gage/create
2GageControllerupdate/Gage/update
3GageControllerdelete/Gage/delete/{gageId}
4GageControllerfindByGageId/Gage/{gageId}
5GrrAnalysisControllercompute/GrrAnalysis/compute
6LoginControllerlogin/Login/login
7IndexControllertoindex/toindex

進階擴展功能

1. 過濾特定模組/前綴

// 僅導出 /api/** 路徑
if (urlPattern.startsWith("/api/")) {
    // 處理...
}

2. 整合 Swagger 註解豐富文檔

ApiOperation apiOp = method.getMethod().getAnnotation(ApiOperation.class);
ApiImplicitParams params = method.getMethod().getAnnotation(ApiImplicitParams.class);
// 解析參數、回應碼、範例等

3. 輸出 JSON 供前端/工具消費

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);

4. 持久化到 Confluence / GitBook / 有道雲筆記


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));
        });
    }
}

最佳實踐建議

  1. 納入 CI 流程:每次構建自動生成最新 API 文檔,避免文檔滯後
  2. 版本控制:生成的 Markdown/JSON 納入 Git,歷史可追溯
  3. 權限管控:敏感內部 API 標記 internal,文檔導出時過濾
  4. 整合註解:強制要求 Controller 方法加 @ApiOperation,提升文檔質量
  5. 多環境支援:區分 dev/test/prod 環境基礎路徑

常見問題

問題解決
掃描不到 Controller確認 @ComponentScan 基礎包路徑、測試上下文載入完整配置
重複 URL 出現同一方法多個 @RequestMapping、類級與方法級組合,去重處理
啟動慢只載入 Web 層 (@WebMvcTest) 或使用 Spring Boot 切片測試
參數解析異常複雜參數類型 (Map、自定義物件) 需自定義解析邏輯

總結

利用 RequestMappingHandlerMapping 反射 Spring MVC 內部路由表,配合 JUnit 測試,零侵入實現 API 文檔自動化。這不僅解決「文檔與代碼不同步」痛點,更能作為架構治理工具:審計未授權端點、檢測路由衝突、統計 API 複雜度。建議將此能力封裝為團隊通用測試基礎設施,納入標準化交付流程。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved