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

Spring MVC 源碼深度解析:AbstractDetectingUrlHandlerMapping 初始化機制詳解

     編輯:Java 相關知識

前言

AbstractDetectingUrlHandlerMapping 是 Spring MVC 中基於掃描機制的 HandlerMapping 抽象基類,它通過掃描 Spring 容器中的 Bean,自動發現並註冊 URL 映射。本文深度解析其初始化流程、核心模板方法及子類實現差異。

類繼承體系

HandlerMapping (介面)
  ↑
AbstractHandlerMapping (抽象基類)
  ↑
AbstractUrlHandlerMapping (URL 映射基類)
  ↑
AbstractDetectingUrlHandlerMapping (掃描檢測基類)
  ↑
├── BeanNameUrlHandlerMapping
├── ControllerBeanNameHandlerMapping
├── ControllerClassNameHandlerMapping
└── (其他子類)

核心初始化流程

1. initApplicationContext 入口

@Override
public void initApplicationContext() throws ApplicationContextException {
    super.initApplicationContext(); // 初始化攔截器等
    detectHandlers();               // 核心:掃描並註冊 Handler
}

2. detectHandlers 掃描邏輯

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

3. 關鍵模板方法:determineUrlsForHandler

protected abstract String[] determineUrlsForHandler(String beanName);

留給子類實現:根據 Bean 名稱或類型,決定對應的 URL 路徑。

子類實現差異分析

1. BeanNameUrlHandlerMapping - 最簡單實現

@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 路徑。

2. AbstractControllerUrlHandlerMapping - Controller 子類專用

增加了過濾機制與 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);

3. ControllerBeanNameHandlerMapping - 基於 Bean Name

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

4. ControllerClassNameHandlerMapping - 基於類名 (已棄用)

@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 + "*"};
}

關鍵設計模式應用

1. 模板方法模式

  • detectHandlers() 定義掃描骨架
  • determineUrlsForHandler() 留給子類實現具體 URL 確定邏輯
  • buildUrlsForHandler() 進一步細分構建邏輯

2. 策略模式:ControllerTypePredicate

class ControllerTypePredicate {
    public boolean isControllerType(Class<?> beanClass) {
        return Controller.class.isAssignableFrom(beanClass);
    }
    public boolean isMultiActionControllerType(Class<?> beanClass) {
        return MultiActionController.class.isAssignableFrom(beanClass);
    }
}

將類型判斷邏輯封裝為獨立策略,支援擴展。

註冊機制:registerHandler

最終由父類 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 核心架構演進脈絡,對於維護老專案或設計類似掃描註冊框架仍有參考價值。

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