SpringMVC的URL對映器之SimpleUrlHandlerMapping
寫在前面
本文原始碼專案中用到的依賴有 spring-webmvc,javax.servlet-api,測試依賴有 junit 和 spring-test。
本文不對 web.xml 中的配置做過多闡述。更多參考文件:
本文不去細究 <property> 標籤內的子標籤是如何變成 setXXX 的引數物件的,但是會關心 setXXX 方法內做了什麼。
主要的四類 “Handler”:
接下來我們準備把實現了 Controller 和 HttpRequestHandler
注意:本文中沒有實現 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();
因此,setUrlMap 和 setMapping 無論呼叫那個,最終都會為 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 後,檢驗基本功能是否正常。