AbstractDetectingUrlHandlerMapping 是 Spring MVC 中基於掃描機制的 HandlerMapping 抽象基類,它通過掃描 Spring 容器中的 Bean,自動發現並註冊 URL 映射。本文深度解析其初始化流程、核心模板方法及子類實現差異。
HandlerMapping (介面) ↑ AbstractHandlerMapping (抽象基類) ↑ AbstractUrlHandlerMapping (URL 映射基類) ↑ AbstractDetectingUrlHandlerMapping (掃描檢測基類) ↑ ├── BeanNameUrlHandlerMapping ├── ControllerBeanNameHandlerMapping ├── ControllerClassNameHandlerMapping └── (其他子類)
@Override
public void initApplicationContext() throws ApplicationContextException {
super.initApplicationContext(); // 初始化攔截器等
detectHandlers(); // 核心:掃描並註冊 Handler
}
protected void detectHandlers() throws BeansException {
// 1. 獲取所有 Bean 名稱 (支援祖先上下文)
String[] beanNames = (this.detectHandlersInAncestorContexts
? BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class)
: getApplicationContext().getBeanNamesForType(Object.class));
// 2. 遍歷每個 Bean,確定其 URL 映射
for (String beanName : beanNames) {
String[] urls = determineUrlsForHandler(beanName);
if (!ObjectUtils.isEmpty(urls)) {
// 3. 註冊映射關係
registerHandler(urls, beanName);
} else {
logger.debug("Rejected bean '{}': no URL paths identified", beanName);
}
}
}
protected abstract String[] determineUrlsForHandler(String beanName);
留給子類實現:根據 Bean 名稱或類型,決定對應的 URL 路徑。
@Override
protected String[] determineUrlsForHandler(String beanName) {
List<String> urls = new ArrayList<>();
// Bean 名稱以 / 開頭視為 URL
if (beanName.startsWith("/")) {
urls.add(beanName);
}
// 別名也以 / 開頭則加入
String[] aliases = getApplicationContext().getAliases(beanName);
for (String alias : aliases) {
if (alias.startsWith("/")) {
urls.add(alias);
}
}
return StringUtils.toStringArray(urls);
}
特點:約定優於配置,Bean 名稱即 URL 路徑。
增加了過濾機制與 URL 構建模板:
@Override
protected String[] determineUrlsForHandler(String beanName) {
Class<?> beanClass = getApplicationContext().getType(beanName);
// 1. 過濾:排除包/類、必須是 Controller 子類
if (isEligibleForMapping(beanName, beanClass)) {
// 2. 委託子類構建 URL
return buildUrlsForHandler(beanName, beanClass);
}
return null;
}
protected boolean isEligibleForMapping(String beanName, Class<?> beanClass) {
// 排除配置的類/包
if (excludedClasses.contains(beanClass)) return false;
for (String pkg : excludedPackages) {
if (beanClass.getName().startsWith(pkg)) return false;
}
// 必須是 Controller 類型
return isControllerType(beanClass);
}
protected abstract String[] buildUrlsForHandler(String beanName, Class<?> beanClass);
@Override
protected String[] buildUrlsForHandler(String beanName, Class<?> beanClass) {
List<String> urls = new ArrayList<>();
urls.add(generatePathMapping(beanName));
// 支援別名
for (String alias : getApplicationContext().getAliases(beanName)) {
urls.add(generatePathMapping(alias));
}
return StringUtils.toStringArray(urls);
}
protected String generatePathMapping(String name) {
// 確保以 / 開頭
String path = name.startsWith("/") ? name : "/" + name;
// 添加前綴/後綴
StringBuilder sb = new StringBuilder();
if (!path.startsWith(urlPrefix)) sb.append(urlPrefix);
sb.append(path);
if (!path.endsWith(urlSuffix)) sb.append(urlSuffix);
return sb.toString();
}
@Override
protected String[] buildUrlsForHandler(String beanName, Class<?> beanClass) {
return generatePathMappings(beanClass);
}
protected String[] generatePathMappings(Class<?> beanClass) {
StringBuilder pathMapping = buildPathPrefix(beanClass);
String className = ClassUtils.getShortName(beanClass);
// 移除 Controller 後綴
String path = className.endsWith("Controller")
? className.substring(0, className.length() - "Controller".length())
: className;
// 首字母小寫
if (path.length() > 0) {
pathMapping.append(Character.toLowerCase(path.charAt(0)))
.append(path.substring(1));
}
// MultiActionController 支援 /* 模式
if (isMultiActionControllerType(beanClass)) {
return new String[] {pathMapping.toString(), pathMapping + "/*"};
}
return new String[] {pathMapping + "*"};
}
detectHandlers() 定義掃描骨架determineUrlsForHandler() 留給子類實現具體 URL 確定邏輯buildUrlsForHandler() 進一步細分構建邏輯class ControllerTypePredicate {
public boolean isControllerType(Class<?> beanClass) {
return Controller.class.isAssignableFrom(beanClass);
}
public boolean isMultiActionControllerType(Class<?> beanClass) {
return MultiActionController.class.isAssignableFrom(beanClass);
}
}
將類型判斷邏輯封裝為獨立策略,支援擴展。
最終由父類 AbstractUrlHandlerMapping 實現:
protected void registerHandler(String[] urlPaths, String beanName) {
for (String urlPath : urlPaths) {
registerHandler(urlPath, beanName);
}
}
protected void registerHandler(String urlPath, Object handler) {
// 存入 handlerMap: urlPath -> handler (beanName 或 handler 物件)
// 處理重複註冊衝突
}
| 特性 | AbstractDetectingUrlHandlerMapping 系 | RequestMappingHandlerMapping |
|---|---|---|
| 映射來源 | Bean 名稱/類名/配置 | @RequestMapping 註解 |
| 靈活度 | 低 (約定式) | 高 (聲明式) |
| 支援方法級映射 | 否 | 是 |
| Spring 版本狀態 | 維護模式/棄用 | 主流推薦 |
AbstractDetectingUrlHandlerMapping 體現了 Spring 早期「約定優於配置」的設計哲學,通過模板方法模式將掃描流程標準化,將 URL 確定邏輯委託給子類。雖然現代 Spring MVC 主流使用 @RequestMapping 註解驅動,但理解其初始化機制有助於掌握 Spring MVC 核心架構演進脈絡,對於維護老專案或設計類似掃描註冊框架仍有參考價值。