【深刻淺出spring】Spring MVC 流程解析 -- HanndlerMapping

前言

本文基於以前的文章【深刻淺出spring】Spring MVC 流程解析 繼續從源碼角度分析 spring MVC 的原理。Spring MVC 的運行流程在上面的文章中已經介紹了,這裏詳細介紹 url --> handler 的映射過程,即 hanndler mappinghtml

總流程

回顧下上文介紹的流程1:git

首先用戶發送請求,DispatcherServlet實現了Servlet接口,整個請求處理流:HttpServlet.service -> FrameworkServlet.doGet -> FrameworkServlet.processRequest -> DispatcherServlet.doService -> DispatcherServlet.doDispatchweb

相關源碼以下:spring

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HandlerExecutionChain mappedHandler = null;
        ......
        mappedHandler = getHandler(processedRequest);
        ......
    }
    
    @Nullable
    protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        if (this.handlerMappings != null) {
            for (HandlerMapping hm : this.handlerMappings) {
                if (logger.isTraceEnabled()) {
                    logger.trace(
                            "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
                }
                HandlerExecutionChain handler = hm.getHandler(request);
                if (handler != null) {
                    return handler;
                }
            }
        }
        return null;
    }

    public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        Object handler = getHandlerInternal(request);
        if (handler == null) {
            handler = getDefaultHandler();
        }
        if (handler == null) {
            return null;
        }
        // Bean name or resolved handler?
        if (handler instanceof String) {
            String handlerName = (String) handler;
            handler = obtainApplicationContext().getBean(handlerName);
        }

        HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
        if (CorsUtils.isCorsRequest(request)) {
            CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request);
            CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
            CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
            executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
        }
        return executionChain;
    }

拆解doDispatch中相關步驟,和 handler mapping 相關的流程可慨括爲DispatcherServlet.doDispatch --> DispatcherServlet.getHandler --> AbstractHandlerMapping.getHandler --> xxx.getHandlerInternal,這裏的xxxAbstractHandlerMapping的子類或實現類,核心方法getHandlerInternal是抽象方法,具體實現由其子類實現。segmentfault

spring mvc中默認的HandlerMapping有4種(即此處 handlerMappings 列表中的成員):mvc

  • RequestMappingHandlerMapping
  • BeanNameUrlHandlerMapping
  • SimpleUrlHandlerMapping
  • WelcomePageHandlerMapping

下面依次分析每一個HandlerMapping的原理。附錄有具體的類關係圖app

RequestMappingHandlerMapping

處理器獲取

由附錄中的類關係圖可知,此類繼承自AbstractHandlerMethodMapping,如上節分析 DispatcherServlet 中的 getHandler 方法最終回調用子類的 getHandlerInternal 方法,RequestMappingHandlerMapping沒有重寫getHandlerInternal,故這裏直接調用AbstractHandlerMethodMapping.getHandlerInternal,源碼以下:cors

protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
        String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
        if (logger.isDebugEnabled()) {
            logger.debug("Looking up handler method for path " + lookupPath);
        }
        this.mappingRegistry.acquireReadLock();
        try {
            HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
            if (logger.isDebugEnabled()) {
                if (handlerMethod != null) {
                    logger.debug("Returning handler method [" + handlerMethod + "]");
                }
                else {
                    logger.debug("Did not find handler method for [" + lookupPath + "]");
                }
            }
            return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
        }
        finally {
            this.mappingRegistry.releaseReadLock();
        }
    }

    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
        List<Match> matches = new ArrayList<>();
        List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
        if (directPathMatches != null) {
            addMatchingMappings(directPathMatches, matches, request);
        }
        ......
    }

上述方法是根據請求url獲取對應處理方法,而後封裝成 HandlerExecutionChain, 即HandlerExecutionChain 中的 handler 是 HandlerMethod 類型。 這裏不得不提一下 MappingRegistry,是 RequestMappingHandlerMapping 中重要的類,lookupHandlerMethod 的核心邏輯就是從 MappingRegistry 中獲取 url 對應的 HandlerMethod。ide

class MappingRegistry {

        private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

        private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();

        private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();

        private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();

        private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();

        private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
}

對於 RequestMappingHandlerMapping,這裏的 T = RequestMappingInfo。聯結 urlLookup 和 mappingLookup 便可實現從 url --> HandlerMethod 的映射,實現獲取對應請求的處理方法。
不難發現,這裏的 url --> HandlerMethod 的尋找過程本質就是 Map 的 key-value,那麼這些 map,即 MappingRegistry 的實例是何時初始化的呢?spring-boot

映射關係寫入

RequestMappingHandlerMapping 關係圖中能夠發現RequestMappingHandlerMapping實現的接口InitializingBeanInitializingBean的做用見Spring Bean 初始化之InitializingBean, init-method 和 PostConstruct,我的感受仍是講的很清楚了。跟蹤源代碼能夠發現,MappingRegistry的初始化入口是AbstractHandlerMethodMapping.initHandlerMethods,源碼以下:

protected void initHandlerMethods() {
        if (logger.isDebugEnabled()) {
            logger.debug("Looking for request mappings in application context: " + getApplicationContext());
        }
        String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
                BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
                obtainApplicationContext().getBeanNamesForType(Object.class));

        for (String beanName : beanNames) {
            if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
                Class<?> beanType = null;
                try {
                    beanType = obtainApplicationContext().getType(beanName);
                }
                catch (Throwable ex) {
                    // An unresolvable bean type, probably from a lazy bean - let's ignore it.
                    if (logger.isDebugEnabled()) {
                        logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
                    }
                }
                if (beanType != null && isHandler(beanType)) {
                    detectHandlerMethods(beanName);
                }
            }
        }
        handlerMethodsInitialized(getHandlerMethods());
    }

能夠看到其中過濾 bean 使用了RequestMappingHandlerMapping.isHandler,對註解了@Controller, @RequestMapping的 bean 纔會進行後續操做。即核心方法detectHandlerMethods,經過反射的方式,將@RequestMapping中的url和對應的處理方法註冊到mappingRegistry中,從而實現了上面所屬的映射查找。

@Override
    protected boolean isHandler(Class<?> beanType) {
        return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
                AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
    }
    
    protected void detectHandlerMethods(final Object handler) {
        Class<?> handlerType = (handler instanceof String ?
                obtainApplicationContext().getType((String) handler) : handler.getClass());

        if (handlerType != null) {
            final Class<?> userType = ClassUtils.getUserClass(handlerType);
            Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
                    (MethodIntrospector.MetadataLookup<T>) method -> {
                        try {
                            return getMappingForMethod(method, userType);
                        }
                        catch (Throwable ex) {
                            throw new IllegalStateException("Invalid mapping on handler class [" +
                                    userType.getName() + "]: " + method, ex);
                        }
                    });
            if (logger.isDebugEnabled()) {
                logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods);
            }
            for (Map.Entry<Method, T> entry : methods.entrySet()) {
                Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType);
                T mapping = entry.getValue();
                // 註冊到mappingRegistry中
                registerHandlerMethod(handler, invocableMethod, mapping);
            }
        }
    }

總結

  • RequestMappingHandlerMapping 沒有重寫 getHandlerInternal,直接使用父類 AbstractHandlerMethodMappinggetHandlerInternal
  • RequestMappingHandlerMapping 會將 @Controller, @RequestMapping 註解的類方法註冊爲 handler,每一個請求url能夠對應一個 handler method 來處理
  • RequestMappingHandlerMapping 的映射關係由 MappingRegistry 維護,經過多個map聯結,便可找到請求對應的處理器
  • RequestMappingHandlerMapping 的映射關係初始化入口是 afterProperties 方法,由於其實現了接口 InitializingBean

SimpleUrlHandlerMapping

處理器獲取

此類的getHandlerInternal邏輯由其父類AbstractUrlHandlerMapping實現

protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
        String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
        Object handler = lookupHandler(lookupPath, request);
        if (handler == null) {
            // We need to care for the default handler directly, since we need to
            // expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well.
            Object rawHandler = null;
            if ("/".equals(lookupPath)) {
                rawHandler = getRootHandler();
            }
            if (rawHandler == null) {
                rawHandler = getDefaultHandler();
            }
            if (rawHandler != null) {
                // Bean name or resolved handler?
                if (rawHandler instanceof String) {
                    String handlerName = (String) rawHandler;
                    rawHandler = obtainApplicationContext().getBean(handlerName);
                }
                validateHandler(rawHandler, request);
                handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null);
            }
        }
        if (handler != null && logger.isDebugEnabled()) {
            logger.debug("Mapping [" + lookupPath + "] to " + handler);
        }
        else if (handler == null && logger.isTraceEnabled()) {
            logger.trace("No handler mapping found for [" + lookupPath + "]");
        }
        return handler;
    }

其中核心邏輯是Object lookupHandler(String urlPath, HttpServletRequest request),這個方法的尋找邏輯根據優先級簡單歸納爲以下幾步:

  1. 直接從Map<String, Object> handlerMap中根據請求 url 獲取對應的處理方法
  2. 經過AntPathMatcherhandlerMap中獲取匹配最好的url,並獲取對應的處理方法

一樣,這裏只是一個獲取查找的過場,核心點是handlerMap的初始化邏輯。

映射關係寫入

從類圖關係中能夠發現,SimpleUrlHandlerMapping實現了ApplicationContextAware接口,從ApplicationContextAwareProcessor.invokeAwareInterfaces能夠發現,此接口的setApplicationContext會在bean初始化的時候被調用

private void invokeAwareInterfaces(Object bean) {
        if (bean instanceof Aware) {
            if (bean instanceof EnvironmentAware) {
                ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
            }
            if (bean instanceof EmbeddedValueResolverAware) {
                ((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver);
            }
            if (bean instanceof ResourceLoaderAware) {
                ((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext);
            }
            if (bean instanceof ApplicationEventPublisherAware) {
                ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);
            }
            if (bean instanceof MessageSourceAware) {
                ((MessageSourceAware) bean).setMessageSource(this.applicationContext);
            }
            if (bean instanceof ApplicationContextAware) {
                ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
            }
        }
    }
    
    
    // SimpleUrlHandlerMapping的具體實現
    public void initApplicationContext() throws BeansException {
        super.initApplicationContext();
        registerHandlers(this.urlMap);
    }
    
    protected void registerHandlers(Map<String, Object> urlMap) throws BeansException {
        if (urlMap.isEmpty()) {
            logger.warn("Neither 'urlMap' nor 'mappings' set on SimpleUrlHandlerMapping");
        }
        else {
            urlMap.forEach((url, handler) -> {
                // Prepend with slash if not already present.
                if (!url.startsWith("/")) {
                    url = "/" + url;
                }
                // Remove whitespace from handler bean name.
                if (handler instanceof String) {
                    handler = ((String) handler).trim();
                }
                registerHandler(url, handler);
            });
        }
    }

總結

  • 經過ApplicationContextAware.setApplicationContext註冊 url - handler 的映射,這裏的 handler 對應了一個處理類,調用方法handleRequest處理,而不像RequestMappingHandlerMapping,是類中的一個方法。
  • SimpleUrlHandlerMapping處理的 url 好比以/開頭
  • SimpleUrlHandlerMapping的 url - handler 映射來源來自成員urlMap,故須要經過配置注入,例子以下:
<bean id="helloController" class="com.raistudies.ui.controller.HelloController"/>

  <bean id="urlHandler" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="urlMap">
            <map>
                <entry key="/hello.htm" value-ref="helloController"/>
                <entry key="/sayHello*" value-ref="helloController"/>
                <entry key="/welcome.html" value-ref="helloController"/>
                <entry key="/welcomeUser*" value-ref="helloController"/>
            </map>
        </property>
    </bean>
  • SimpleUrlHandlerMapping的初始化再WebMvcAutoConfiguration中完成:
@Bean
    public SimpleUrlHandlerMapping faviconHandlerMapping() {
        SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
        mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
        mapping.setUrlMap(Collections.singletonMap("**/favicon.ico", faviconRequestHandler()));
        return mapping;
    }

WelcomePageHandlerMapping

獲取處理器

首先來看下getHandlerInternal方法

public Object getHandlerInternal(HttpServletRequest request) throws Exception {
        for (MediaType mediaType : getAcceptedMediaTypes(request)) {
            if (mediaType.includes(MediaType.TEXT_HTML)) {
                return super.getHandlerInternal(request);
            }
        }
        return null;
    }

能夠看到,邏輯比較簡單,請求的 media type 需爲 text/html, 而後執行父類的方法AbstractUrlHandlerMapping.getHandlerInternal

protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
        String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
        Object handler = lookupHandler(lookupPath, request);
        if (handler == null) {
            // We need to care for the default handler directly, since we need to
            // expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well.
            Object rawHandler = null;
            if ("/".equals(lookupPath)) {
                rawHandler = getRootHandler();
            }
            if (rawHandler == null) {
                rawHandler = getDefaultHandler();
            }
            if (rawHandler != null) {
                // Bean name or resolved handler?
                if (rawHandler instanceof String) {
                    String handlerName = (String) rawHandler;
                    rawHandler = obtainApplicationContext().getBean(handlerName);
                }
                validateHandler(rawHandler, request);
                handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null);
            }
        }
        if (handler != null && logger.isDebugEnabled()) {
            logger.debug("Mapping [" + lookupPath + "] to " + handler);
        }
        else if (handler == null && logger.isTraceEnabled()) {
            logger.trace("No handler mapping found for [" + lookupPath + "]");
        }
        return handler;
    }

從源碼中能夠看到,先經過lookupHandler尋找處理方法,若找不到且請求爲/,則返回rootHandler

映射關係寫入

再看下WebMvcAutoConfigurationWelcomePageHandlerMapping的初始化源碼,這裏的getWelcomPage會在以下路徑下尋找:

  • classpath:/META-INF/resources/index.html
  • classpath:/resources/index.html
  • classpath:/static/index.html
  • classpath:/public/index.html
@Bean
        public WelcomePageHandlerMapping welcomePageHandlerMapping(
                ApplicationContext applicationContext) {
            return new WelcomePageHandlerMapping(
                    new TemplateAvailabilityProviders(applicationContext),
                    applicationContext, getWelcomePage(),
                    this.mvcProperties.getStaticPathPattern());
        }

總結

  • WelcomePageHandlerMapping 默認經過 WebMvcAutoConfiguration 初始化,將處理器直接註冊爲rootHandler,默認處理請求/,默認返回規定路徑下的index.html

BeanNameUrlHandlerMapping

獲取處理器

一樣的先看getHandlerInternal方法,BeanNameUrlHandlerMapping和其父類AbstractDetectingUrlHandlerMapping都沒有重寫,故getHandlerInternal執行的是父類AbstractUrlHandlerMapping的方法,邏輯同SimpleUrlHandlerMapping

映射關係寫入

不一樣點是handlerMap的初始化。經過源碼閱讀,發現AbstractDetectingUrlHandlerMapping重寫了initApplicationContext,具體邏輯以下:

@Override
    public void initApplicationContext() throws ApplicationContextException {
        super.initApplicationContext();
        detectHandlers();
    }
    
    protected void detectHandlers() throws BeansException {
        ApplicationContext applicationContext = obtainApplicationContext();
        if (logger.isDebugEnabled()) {
            logger.debug("Looking for URL mappings in application context: " + applicationContext);
        }
        String[] beanNames = (this.detectHandlersInAncestorContexts ?
                BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, Object.class) :
                applicationContext.getBeanNamesForType(Object.class));

        // Take any bean name that we can determine URLs for.
        for (String beanName : beanNames) {
            String[] urls = determineUrlsForHandler(beanName);
            if (!ObjectUtils.isEmpty(urls)) {
                // URL paths found: Let's consider it a handler.
                registerHandler(urls, beanName);
            }
            else {
                if (logger.isDebugEnabled()) {
                    logger.debug("Rejected bean name '" + beanName + "': no URL paths identified");
                }
            }
        }
    }
    
    // BeanNameUrlHandlerMapping
    @Override
    protected String[] determineUrlsForHandler(String beanName) {
        List<String> urls = new ArrayList<>();
        if (beanName.startsWith("/")) {
            urls.add(beanName);
        }
        String[] aliases = obtainApplicationContext().getAliases(beanName);
        for (String alias : aliases) {
            if (alias.startsWith("/")) {
                urls.add(alias);
            }
        }
        return StringUtils.toStringArray(urls);
    }

根據源碼不難發現,對於名稱以/開頭的bean,會被註冊到handlerMap中,key即爲bean name,value即爲對應的bean,處理請求的時候會調用bean的handleRequest方法.

配置示例:

bean name="/hello.htm" class="com.raistudies.ui.comtroller.HelloController"/>
<bean name="/sayHello*" class="com.raistudies.ui.comtroller.HelloController"/>
<bean id="urlHandler" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>

總結

  • 默認的handlerMapping有4類:RequestMappingHandlerMapping, BeanNameUrlHandlerMapping, SimpleUrlHandlerMapping, WelcomePageHandlerMapping
  • RequestMappingHandlerMapping的映射緯度是請求 --> 方法,另外3個都是請求 --> 類RequestMappingHandlerMapping的應用場景豐富,處理動態請求的最佳實踐,另外3個基本上用來處理靜態資源的
  • BeanNameUrlHandlerMapping, SimpleUrlHandlerMapping 原理很接近,最大的區別是BeanNameUrlHandlerMapping的bean名稱就是url,必須以/開頭,所以相比SimpleUrlHandlerMapping少了一步 url --> handler實例 的配置
  • 當容器中有多個handlerMapping實例時,會根據實例的優先級從高到底依次遍歷,一旦找到對應 handler 後就會結束。優先級經過mapping的order屬性設置,值越小,優先級越高。若不顯示設置,默認就是max,即優先級最低。範圍 Ordered.HIGHEST_PRECEDENCE -- Ordered.LOWEST_PRECEDENCE

示例

  • 代碼下載地址:handler_mapping_sample
  • WelcomePageHandlerMapping示例請求:http://localhost:8080/,會渲染 static 目錄下的 index.html
  • BeanNameUrlHandlerMapping示例請求:http://localhost:8080/beanNameUrl.html,對應的處理器BeanNameController
  • SimpleUrlHandlerMapping示例請求:http://localhost:8080/simpleUrl.html,對應的處理器SimpleUrlController,配置ControllerConfig
  • RequestMappingHandlerMapping示例請求:http://localhost:8080/data/model,對應的處理方法DemoController.getModel

附錄

HanddlerMapping 關係圖

clipboard.png

RequestMappingHandlerMapping 關係圖

clipboard.png

BeanNameUrlHandlerMapping 關係圖

clipboard.png

SimpleUrlHandlerMapping 關係圖

clipboard.png

WelcomePageHandlerMapping 關係圖

clipboard.png

相關文章
相關標籤/搜索