寫在前面
本文源碼項目中用到的依賴有 spring-webmvc,javax.servlet-api,測試依賴有 junit 和 spring-test。
本文不對 web.xml 中的配置做過多闡述。更多參考文檔:
本文不去細究 <property> 標簽內的子標簽是如何變成 setXXX 的參數對象的,但是會關心 setXXX 方法內做了什么。
主要的四類 “Handler”:
接下來我們准備把實現了 Controller 和 HttpRequestHandler 以及繼承了 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();
因此,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 后,檢驗基本功能是否正常。