1. 程式人生 > 實用技巧 >SpringMVC的URL對映器之SimpleUrlHandlerMapping

SpringMVC的URL對映器之SimpleUrlHandlerMapping

寫在前面

本文原始碼專案中用到的依賴有 spring-webmvcjavax.servlet-api,測試依賴有 junitspring-test

本文不對 web.xml 中的配置做過多闡述。更多參考文件:

本文不去細究 <property> 標籤內的子標籤是如何變成 setXXX 的引數物件的,但是會關心 setXXX 方法內做了什麼。

主要的四類 “Handler”:

接下來我們準備把實現了 ControllerHttpRequestHandler

以及繼承了 HttpServlet 的類加入到 SimpleUrlHandlerMapping

注意:本文中沒有實現 HandlerMethod 的對映,另外,這裡的 Controller 指的是 org.springframework.web.servlet.mvc.Controller,而不是我們常用的註解 @Controller。

對映配置

SimpleUrlHandlerMapping 可以通過setMappings(Properties mappings)setUrlMap(Map<String, ?> urlMap)設定 url 和 “Handler” 的對映

setMappings

方式一:使用 <props> 填入多個 <prop>

spring-mvc.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="welcomeController" class="coderead.springframework.mvc.WelcomeController" />
    <bean id="helloGuestController" class="coderead.springframework.mvc.HelloGuestController" />
    <bean id="helloLuBanController" class="coderead.springframework.mvc.HelloLuBanHttpRequestHandler" />
    <bean id="loginHttpServlet" class="coderead.springframework.mvc.LoginHttpServlet" />

    <!-- Fixed problem : javax.servlet.ServletException: No adapter for handler [coderead.springframework.mvc.LoginHttpServlet@2f2f30df]-->
    <bean class="org.springframework.web.servlet.handler.SimpleServletHandlerAdapter"/>
    <bean class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter" />
    <bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" />

    <bean id="simpleUrlHandlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <!-- 方式一: props 中填入 prop 列表-->
            <!-- key 是 url,屬性值是 Bean 的 id。-->
            <props>
                <prop key="/welcome">welcomeController</prop>
                <prop key="/hello">helloGuestController</prop>
                <prop key="/hi">helloLuBanController</prop>
                <prop key="/login">loginHttpServlet</prop>
            </props>
        </property>
    </bean>
</beans>

方式二:使用 <value> 填入配置檔案文字

<!-- 方式二: value 中填入配置檔案內容 -->
<!-- 等號左邊是URL模式,等號右邊是 Bean 的 id。 -->
<property name="mappings">
	<value>
		/welcome=welcomeController
		/hello=helloGuestController
		/hi=helloLuBanController
		/login=loginHttpServlet
	</value>
</property>

用這段程式碼代替方式一中的 <property name="mappings"/>

方式三:<map> 填入 <entry> 鍵值對

<!-- 方式三: map -->
<!-- entry: key 儲存 URL 模式,value 儲存 Bean id。 -->
<property name="mappings">
    <map>
        <entry key="/welcome" value="welcomeController" />
        <entry key="/hello" value="helloGuestController" />
        <entry key="/hi" value="helloLuBanController" />
        <entry key="/login" value="loginHttpServlet" />
    </map>
</property>

用這段程式碼代替方式一中的 <property name="mappings"/>

方式四:使用 PropertiesFactoryBean 和 properties 檔案

<!-- 方式四:properties 配置檔案 -->
<property name="mappings">
	<bean class="org.springframework.beans.factory.config.PropertiesFactoryBean">
		<property name="location" value="classpath:spring/url-mapping.properties"/>
	</bean>
</property>

url-mapping.properties

/welcome=welcomeController
/hello=helloGuestController
/hi=helloLuBanController
/login=loginHttpServlet

setUrlMap

<property name="mappings"/> 替換為 <property name="urlMap"/>經過我的測試,上面的四種方式都依然奏效! 我的測試方法參考了 SpringMVC 測試 mockMVC 這篇部落格,基於 MockMvc 進行了測試。

SimpleUrlHandlerMapping 原始碼分析

填充 urlMap

SimpleUrlHandlerMapping 只有 urlMap 成員變數

private final Map<String, Object> urlMap = new HashMap();

因此,setUrlMapsetMapping 無論呼叫那個,最終都會為 urlMap 增加鍵值對。

setUrlMap

// 其中 setUrlMap 比較簡單,沒什麼好說的
public void setUrlMap(Map<String, ?> urlMap) {
      this.urlMap.putAll(urlMap);
}

setMapping

public void setMappings(Properties mappings) {
      // 顧名思義,把 mappings 中的鍵值對合併到 urlMap 中
      CollectionUtils.mergePropertiesIntoMap(mappings, this.urlMap);
}

org.springframework.util.CollectionUtils.mergePropertiesIntoMap

public static <K, V> void mergePropertiesIntoMap(@Nullable Properties props, Map<K, V> map) {
    String key;
    Object value;
    if (props != null) {
        // 遍歷 props 的鍵,並在迴圈體執行完畢後,將鍵值對存入 map
        for(Enumeration en = props.propertyNames(); en.hasMoreElements(); map.put(key, value)) {
            key = (String)en.nextElement();
            value = props.get(key);
            if (value == null) {
                value = props.getProperty(key);
            }
        }
    }
}

這個方法就是把 Properties 的 key=value 取出來,再放入目標 map 中。

填充 handlerMap

AbstractUrlHandlerMapping 是 SimpleUrlHandlerMapping 的父類,其中一個成員變數 Map<String, Object> handlerMap 儲存 key 是 url patterns,value 就是 “Handler” 物件

SimpleUrlHandlerMapping#registerHandlers

protected void registerHandlers(Map<String, Object> urlMap) throws BeansException {
	if (urlMap.isEmpty()) {
		logger.trace("No patterns in " + formatMappingName());
	}
	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);
		});
                // 這段 if 程式碼沒有實質性作用,僅僅是為了列印一段日誌,可以忽略
		if (logger.isDebugEnabled()) {
			List<String> patterns = new ArrayList<>();
			if (getRootHandler() != null) {
				patterns.add("/");
			}
			if (getDefaultHandler() != null) {
				patterns.add("/**");
			}
			patterns.addAll(getHandlerMap().keySet());
			logger.debug("Patterns " + patterns + " in " + formatMappingName());
		}
	}
}

細節一:urlMap.forEach 是 Java8 Lambda 表示式的寫法,等同於下面這段 foreach 程式碼。

for (Map.Entry<String, Object> entry : urlMap.entrySet()) {
      String url = entry.getKey();
      Object handler = entry.getValue();
}

細節二:對 url 字串的前置處理,確保 url 以 / 開頭,並且開頭和結尾沒有空格符。

AbstractUrlHandlerMapping#registerHandler

protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
	Assert.notNull(urlPath, "URL path must not be null");
	Assert.notNull(handler, "Handler object must not be null");
	Object resolvedHandler = handler;
	// Eagerly resolve handler if referencing singleton via name.
	if (!this.lazyInitHandlers && handler instanceof String) {
		String handlerName = (String) handler;
		ApplicationContext applicationContext = obtainApplicationContext();
		if (applicationContext.isSingleton(handlerName)) {
			resolvedHandler = applicationContext.getBean(handlerName);
		}
	}
	Object mappedHandler = this.handlerMap.get(urlPath);
	if (mappedHandler != null) {
		if (mappedHandler != resolvedHandler) {
			throw new IllegalStateException(
					"Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
					"]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
		}
	}
	else {
		if (urlPath.equals("/")) {
			if (logger.isTraceEnabled()) {
				logger.trace("Root mapping to " + getHandlerDescription(handler));
			}
			setRootHandler(resolvedHandler);
		}
		else if (urlPath.equals("/*")) {
			if (logger.isTraceEnabled()) {
				logger.trace("Default mapping to " + getHandlerDescription(handler));
			}
			setDefaultHandler(resolvedHandler);
		}
		else {
			this.handlerMap.put(urlPath, resolvedHandler);
			if (logger.isTraceEnabled()) {
				logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler));
			}
		}
	}
}

細節一:根據字串型別的 handler,生成 Bean。

resolvedHandler = applicationContext.getBean(handlerName);

細節二:obtainApplicationContext()
obtainApplicationContext() 是父類 ApplicationObjectSupport 的方法,該類實現了 ApplicationContextAware 介面。

Spring容器會在上下文建立完成後,主動回撥 void setApplicationContext(ApplicationContext ctx) 方法,該方法會呼叫 protected void initApplicationContext()

public final void setApplicationContext(@Nullable ApplicationContext context) throws BeansException {
    if (context == null && !this.isContextRequired()) {
        this.applicationContext = null;
        this.messageSourceAccessor = null;
    } else if (this.applicationContext == null) {
        if (!this.requiredContextClass().isInstance(context)) {
            throw new ApplicationContextException("Invalid application context: needs to be of type [" + this.requiredContextClass().getName() + "]");
        }
        this.applicationContext = context;
        this.messageSourceAccessor = new MessageSourceAccessor(context);
        this.initApplicationContext(context);
    } else if (this.applicationContext != context) {
        throw new ApplicationContextException("Cannot reinitialize with different application context: current one is [" + this.applicationContext + "], passed-in one is [" + context + "]");
    }

}

細節三:SimpleUrlHandlerMapping#initApplicationContext() 何時觸發?

ApplicationContext 例項建立完成,回撥 ApplicationContextAware#setApplicationContext(ApplicationContext ctx) 之後。

獲取原始碼

獲取專案原始碼:

git clone https://gitee.com/kendoziyu/coderead-spring-mvc-parent.git

其中 url-handler-mapping 專案就是本文的示例程式碼。你可以執行專案進行訪問,也可以直接執行測試。

mvn jetty:run

通過 jetty:run 命令直接啟動專案,如果你使用的是 IDEA,那你可以參考這篇文章:使用maven-Jetty9-plugin外掛執行第一個Servlet

mvn test

通過 mvn test 命令執行測試用例,測試請求是否可以正常返回。這個測試用例主要是方便你修改 spring-mvc.xml 後,檢驗基本功能是否正常。

參考文獻

SpringMVC 測試 mockMVC

Springframework MockMVC Docs

springMvc四種處理器對映器之二:SimpleUrlHandlerMapping