本文將聚焦於Spring MVC中最重要的一個組件:HandlerMapping。
HandlerMapping
用來查找Handler的。在SpringMVC中會有很多請求,每個請求都需要一個Handler處理,具體接收到一個請求之后使用哪個Handler進行處理呢?這就是HandlerMapping需要做的事。
HandlerMapping:負責映射用戶的URL和對應的處理類Handler,HandlerMapping並沒有規定這個URL與應用的處理類如何映射。所以在HandlerMapping接口中僅僅定義了根據一個URL必須返回一個由HandlerExecutionChain代表的處理鏈,我們可以在這個處理鏈中添加任意的HandlerAdapter實例來處理這個URL對應的請求(這樣保證了最大的靈活性映射關系)。
public interface HandlerMapping {
//@since 4.3.21
String BEST_MATCHING_HANDLER_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingHandler";
String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() + ".pathWithinHandlerMapping";
String BEST_MATCHING_PATTERN_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingPattern";
String INTROSPECT_TYPE_LEVEL_MAPPING = HandlerMapping.class.getName() + ".introspectTypeLevelMapping";
String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables";
String MATRIX_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".matrixVariables";
String PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE = HandlerMapping.class.getName() + ".producibleMediaTypes";
// 該接口提供的唯一一個方法~~~~
@Nullable
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
看看它的繼承樹:
它有兩大繼承主線:MatchableHandlerMapping和AbstractHandlerMapping。
AbstractHandlerMapping
這是Spring的常用模式了,一言不合就先來個抽象實現。查看它的繼承圖譜:
有必要先對兩個XXXSupport進行一個非常簡單的說明~
WebApplicationObjectSupport 和 ApplicationObjectSupport
看它們的聲明,它們更像是ApplicationContextAware和ServletContextAware的適配器。
public abstract class ApplicationObjectSupport implements ApplicationContextAware { ... }
public abstract class WebApplicationObjectSupport extends ApplicationObjectSupport implements ServletContextAware { ... }
所以如果我們在繼承允許的情況下,只需要繼承此類就能自動擁有上面兩個接口的功能了。
@Service
public class HelloServiceImpl extends WebApplicationObjectSupport implements HelloService {
@Override
public Object hello() {
// 繼承自ApplicationObjectSupport就可以很方便的獲取到下面這兩個值
System.out.println(super.getApplicationContext());
System.out.println(super.obtainApplicationContext()); //@since 5.0
// MessageSourceAccessor參考:MessageSourceAware 它是對MessageSource的一個包裝 處理國際化
System.out.println(super.getMessageSourceAccessor());
// 這里需要繼承和web相關的:WebApplicationObjectSupport
System.out.println(super.getWebApplicationContext());
System.out.println(super.getServletContext());
System.out.println(super.getTempDir()); //Tomcat9_demowar\work\Catalina\localhost\demo_war_war
return "service hello";
}
@Override
protected void initApplicationContext() throws BeansException {
// 這是父類提供給子類的(父類為空實現~),子類可以自行實現,實現子類的邏輯
// 比如子類AbstractDetectingUrlHandlerMapping就復寫了此方法去detectHandlers();
super.initApplicationContext();
}
}
就這樣可以通過繼承的方式快速的實現獲取上下文等,推薦使用~~~
WebApplicationObjectSupport用於提供上下文ApplicationContext和ServletContext的功能~
很顯然如果你已經有繼承了,那就沒辦法只能選擇實現接口的方式了~
繼續來看看AbstractHandlerMapping這個抽象實現給我們做了哪些事情~
// 它自己又額外實現了BeanNameAware和Ordered排序接口
public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
implements HandlerMapping, Ordered, BeanNameAware {
//默認的Handler,這邊使用的Obejct,子類實現的時候,使用HandlerMethod,HandlerExecutionChain等
// the default handler for this handler mapping
@Nullable
private Object defaultHandler;
// url路徑計算的輔助類、工具類
private UrlPathHelper urlPathHelper = new UrlPathHelper();
// Ant風格的Path匹配模式~ 解決如/books/{id}場景
private PathMatcher pathMatcher = new AntPathMatcher();
// 保存着攔截器們~~~
private final List<Object> interceptors = new ArrayList<>();
// 從interceptors中解析得到,直接添加給全部handler
private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<>();
// 跨域相關的配置~
private CorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
private CorsProcessor corsProcessor = new DefaultCorsProcessor();
// 最低的順序(default: same as non-Ordered)
private int order = Ordered.LOWEST_PRECEDENCE;
@Nullable
private String beanName;
...
// 關於UrlPathHelper 的屬性的一些設置~~~
public void setAlwaysUseFullPath(boolean alwaysUseFullPath) {...}
public void setUrlDecode(boolean urlDecode) { ... }
public void setRemoveSemicolonContent(boolean removeSemicolonContent) { ... }
public void setUrlPathHelper(UrlPathHelper urlPathHelper) { ... } //我們也是可議自己指定一個自己的UrlPathHelper 的
...
// PathMatcher我們也可以自己指定
public void setPathMatcher(PathMatcher pathMatcher) { ... }
// Set the interceptors to apply for all handlers mapped by this handler mapping
// 可變參數:可以一次性添加多個攔截器~~~~ 這里使用的Object
public void setInterceptors(Object... interceptors) {
this.interceptors.addAll(Arrays.asList(interceptors));
}
// 設值一個UrlBasedCorsConfigurationSource Map表示它的一些屬性們~~~
public void setCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations) { ... }
// 重載方法 @since 5.1 Spring5.1之后才有的方法
public void setCorsConfigurationSource(CorsConfigurationSource corsConfigurationSource) {
Assert.notNull(corsConfigurationSource, "corsConfigurationSource must not be null");
this.corsConfigurationSource = corsConfigurationSource;
}
// Configure a custom {@link CorsProcessor} to use to apply the matched
// @since 4.2
public void setCorsProcessor(CorsProcessor corsProcessor) {
Assert.notNull(corsProcessor, "CorsProcessor must not be null");
this.corsProcessor = corsProcessor;
}
...
// 這步驟是最重要的。相當於父類setApplicationContext完成了之后,就會執行到這里~~~
// 這這步驟可議看出 這里主要處理的都是攔截器~~~相關的內容
@Override
protected void initApplicationContext() throws BeansException {
// 給子類擴展:增加攔截器,默認為空實現
extendInterceptors(this.interceptors);
// 找到所有MappedInterceptor類型的bean添加到adaptedInterceptors中
detectMappedInterceptors(this.adaptedInterceptors);
// 將interceptors中的攔截器取出放入adaptedInterceptors
// 如果是WebRequestInterceptor類型的攔截器 需要用WebRequestHandlerInterceptorAdapter進行包裝適配
initInterceptors();
}
// 去容器(含祖孫容器)內找到所有的MappedInterceptor類型的攔截器出來,添加進去 非單例的Bean也包含
// 備注MappedInterceptor為Spring MVC攔截器接口`HandlerInterceptor`的實現類 並且是個final類 Spring3.0后出來的。
protected void detectMappedInterceptors(List<HandlerInterceptor> mappedInterceptors) {
mappedInterceptors.addAll(
BeanFactoryUtils.beansOfTypeIncludingAncestors(
obtainApplicationContext(), MappedInterceptor.class, true, false).values());
}
// 它就是把調用者放進來的interceptors們,適配成HandlerInterceptor然后統一放在`adaptedInterceptors`里面裝着~~~
protected void initInterceptors() {
if (!this.interceptors.isEmpty()) {
for (int i = 0; i < this.interceptors.size(); i++) {
Object interceptor = this.interceptors.get(i);
if (interceptor == null) {
throw new IllegalArgumentException("Entry number " + i + " in interceptors array is null");
}
this.adaptedInterceptors.add(adaptInterceptor(interceptor));
}
}
}
// 適配其實也很簡單~就是支持源生的HandlerInterceptor以及WebRequestInterceptor兩種情況而已
protected HandlerInterceptor adaptInterceptor(Object interceptor) {
if (interceptor instanceof HandlerInterceptor) {
return (HandlerInterceptor) interceptor;
} else if (interceptor instanceof WebRequestInterceptor) {
// WebRequestHandlerInterceptorAdapter它就是個`HandlerInterceptor`,內部持有一個WebRequestInterceptor的引用而已
// 內部使用到了DispatcherServletWebRequest包request和response包裝成`WebRequest`等等
return new WebRequestHandlerInterceptorAdapter((WebRequestInterceptor) interceptor);
} else {
throw new IllegalArgumentException("Interceptor type not supported: " + interceptor.getClass().getName());
}
}
protected final HandlerInterceptor[] getAdaptedInterceptors() { ... }
// 它只會返回MappedInterceptor這種類型的,上面是返回adaptedInterceptors所有
protected final MappedInterceptor[] getMappedInterceptors() { ... }
// 這個方法也是一個該抽象類提供的一個非常重要的模版方法:根據request獲取到一個HandlerExecutionChain
// 也是抽象類實現接口HandlerMapping的方法~~~
@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
// 根據request獲取對應的handler 抽象方法,由具體的子類去實現~~~~
Object handler = getHandlerInternal(request);
// 若沒有匹配上處理器,那就走默認的處理器~~~ 默認的處理器也是需要由子類給賦值 否則也會null的
if (handler == null) {
handler = getDefaultHandler();
}
// 若默認的處理器都木有 那就直接返回null啦~
if (handler == null) {
return null;
}
// 意思是如果是個String類型的名稱,那就去容器內找這個Bean,當作一個Handler~
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
// 關鍵步驟:根據handler和request構造一個請求處理鏈~~
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
// 4.2版本提供了對CORS跨域資源共享的支持 此處暫時略過~
if (CorsUtils.isCorsRequest(request)) {
...
}
return executionChain;
}
// 已經找到handler了,那就根據此構造一個請求鏈
// 這里主要是吧攔截器們給糅進來~ 構成對指定請求的一個攔截器鏈
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
// 小細節:因為handler本身也許就是個Chain,所以此處需要判斷一下~
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ? (HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
// 此處就用到了urlPathHelper來解析request
// 如我的請求地址為:`http://localhost:8080/demo_war_war/api/v1/hello` 那么lookupPath=/api/v1/hello
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
if (interceptor instanceof MappedInterceptor) {
// 這里其實就能體現出MappedInterceptor的些許優勢了:也就是它只有路徑匹配上了才會攔截,沒有匹配上的就不會攔截了,處理起來確實是更加的優雅些了~~~~
// 備注:MappedInterceptor可以設置includePatterns和excludePatterns等~~~~~
MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
} else {
chain.addInterceptor(interceptor);
}
}
return chain;
}
...
}
AbstractHandlerMapping主要實現了對方法getHandler()的模版實現,它主要是對HandlerInterceptor進行了一個通用處理,最終會把他們放進HandlerExecutionChain里面去。
MappedInterceptor
一個包括includePatterns和excludePatterns字符串集合並帶有HandlerInterceptor功能的類。
很明顯,就是對於某些地址做特殊包括和排除的攔截器。
Spring3.0推出的對HandlerInterceptor的實現類,且是個final類。所以擴展它並不是像通過繼承HandlerInterceptorAdapter這樣去擴展,而是通過了類似代理的方式~~~~
// @since 3.0 它是個final類 所以不允許你直接使用繼承的方式來擴展
public final class MappedInterceptor implements HandlerInterceptor {
// 可以看到它哥倆都是可以不用指定,可以為null的
@Nullable
private final String[] includePatterns;
@Nullable
private final String[] excludePatterns;
// 持有一個interceptor的引用,類似於目標類~
private final HandlerInterceptor interceptor;
// 注意:該類允許你自己指定路徑的匹配規則。但是Spring里,不管哪個上層服務,默認使用的都是Ant風格的匹配
// 並不是正則的匹配 所以效率上還是蠻高的~
@Nullable
private PathMatcher pathMatcher;
//======構造函數:發現它不僅僅兼容HandlerInterceptor,還可以把WebRequestInterceptor轉換成此~
public MappedInterceptor(@Nullable String[] includePatterns, HandlerInterceptor interceptor) {
this(includePatterns, null, interceptor);
}
...
public MappedInterceptor(@Nullable String[] includePatterns, @Nullable String[] excludePatterns,
WebRequestInterceptor interceptor) {
// 此處使用WebRequestHandlerInterceptorAdapter這個適配器~~~
this(includePatterns, excludePatterns, new WebRequestHandlerInterceptorAdapter(interceptor));
}
// 原則:excludePatterns先執行,includePatterns后執行
// 如果excludePatterns執行完都木有匹配的,並且includePatterns是空的,那就返回true(這是個處理方式技巧~ 對這種互斥的情況 這一步判斷很關鍵~~~)
public boolean matches(String lookupPath, PathMatcher pathMatcher) { ... }
...
}
因為它不能簡單的像擴展HandlerInterceptorAdapter一樣使用,下面給出一個Demo,推薦大家以后都采用這種方案去更加優雅的處理你的攔截器:
@ComponentScan(value = "com.buqiong", useDefaultFilters = false,
includeFilters = {@Filter(type = FilterType.ANNOTATION, classes = {Controller.class, ControllerAdvice.class, RestControllerAdvice.class})}
)
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
//方式一:最源生的使用方式:直接注冊進去即可
//其實它也挺強大,支持includePatterns和exclude...
//其實它底層原理是一個依賴於`InterceptorRegistration`,它是個普通類,協助create一個`MappedInterceptor`
//由此可見最終底層還是使用的`MappedInterceptor`哦~~~~~
//@Override
//public void addInterceptors(InterceptorRegistry registry) {
// registry.addInterceptor(new HelloInterceptor())
// .addPathPatterns() // 就是includePatterns
// .excludePathPatterns();
//}
// 方式二:如果說上述方式是交給Spring去幫我自動處理,這種方式相當於自己手動來處理~~~~~
// 請務必注意: 請務必注意:此處的返回值必須是MappedInterceptor,而不能是HandlerInterceptor 否則不生效~~~
// 因為BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), MappedInterceptor.class, true, false)
// 這個方法它只去找MappedInterceptor類型,如果你是父類型,那就匹配不上了的~~~ 這個工廠方法的Bean定義信息有關~~
@Bean
public MappedInterceptor myHandlerInterceptor() {
String[] includePatterns = {"/api/v1/hello"};
MappedInterceptor handlerInterceptor = new MappedInterceptor(includePatterns, new HelloInterceptor());
return handlerInterceptor;
}
}
HelloInterceptor自己的業務攔截邏輯寫在自己里面即可。需要注意的是,若你的攔截器里想去使用Spring容器內的其它Bean,請不用使用new的方式,而是應該交給Spring管理(可用@Component)。然后此處可寫如方法入參或者通過@Autowired的方式注入進來
提供了兩種方法,但推薦使用方式一,直觀且不容易出錯
在基於XML的配置中,如下配置其實使用的就是MappedInterceptor
<mvc:interceptors>
<mvc:interceptor>
<!--攔截器mapping 符合的才會執行攔截器-->
<mvc:mapping path="/**"/>
<!--在攔截器mapping中除去下面的url -->
<mvc:exclude-mapping path="/transactional_test/*"/>
<!--執行的攔截器(其實這個Bean並沒有必要放進容器里面)-->
<ref bean="apiInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
MatchableHandlerMapping
這是HandlerMapping的另外一個分支,這是它的一個子接口。
// @since 4.3.1 出現得挺晚的接口
public interface MatchableHandlerMapping extends HandlerMapping {
// 確定給定的請求是否符合請求條件 pattern:模版
@Nullable
RequestMatchResult match(HttpServletRequest request, String pattern);
}
目前有兩個類實現了此方法RequestMappingHandlerMapping和AbstractUrlHandlerMapping,但是Spring內部並還沒有調用過此接口方法。
接下來最重要的就是以AbstractHandlerMapping為主線,看看它的真正實現類們了。它主要分為兩大主線:AbstractUrlHandlerMapping方向和AbstractHandlerMethodMapping
AbstractHandlerMethodMapping系列是當前使用得最多的,基於方法的Handler模式。
AbstractUrlHandlerMapping系列
從命名中也能看出來,它和URL有關。它的大致思路為:將url對應的Handler保存在一個Map中,在getHandlerInternal方法中使用url從Map中獲取Handler
// 雖然附加實現了MatchableHandlerMapping ,但本文並不准備詳細分析
public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping implements MatchableHandlerMapping {
// 根路徑 / 的處理器~
@Nullable
private Object rootHandler;
// 是否使用斜線/匹配 如果為true 那么`/users`它也會匹配上`/users/` 默認是false的
private boolean useTrailingSlashMatch = false;
// 設置是否延遲初始化handler。僅適用於單實例handler 默認是false表示立即實例化
private boolean lazyInitHandlers = false;
// 這個Map就是緩存下,URL對應的Handler(注意這里只是handler,而不是chain)
private final Map<String, Object> handlerMap = new LinkedHashMap<>();
...
// 這個就是父類留給子類實現的抽象方法,此抽象類相當於進行了進一步的模版實現~
@Override
@Nullable
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
// 找到URL的后半段:如`/api/v1/hello` 由此可見Spring MVC處理URL路徑匹配都是從工程名后面開始匹配的~~~~
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
// 根據url查找handler
// 1、先去handlerMap里找,若找到了那就實例化它,並且並且給chain里加入一個攔截器:`PathExposingHandlerInterceptor` 它是個private私有類的HandlerInterceptor
// 該攔截器的作用:request.setAttribute()請求域里面放置四個屬性 key見HandlerMapping的常量們~~~
// 2、否則就使用PathMatcher去匹配URL,這里面光匹配其實是比較簡單的。但是這里面還解決了一個問題:那就是匹配上多個路徑的問題
// 因此:若匹配上多個路徑了,就按照PathMatcher的排序規則排序,取值get(0)~~~最后就是同上,加上那個HandlerInterceptor即可
// 需要注意的是:若存在uriTemplateVariables,也就是路徑里都存在多個最佳的匹配的情況 比如/book/{id}和/book/{name}這兩種。
// 還有就是URI完全一樣,但是一個是get方法,一個是post方法之類的 那就再加一個攔截器`UriTemplateVariablesHandlerInterceptor` 它request.setAttribute()了一個屬性:key為 xxx.uriTemplateVariables
// 這些Attribute后續都是有用滴~~~~~~ 請注意:這里默認的兩個攔截器每次都是new出來的和Handler可議說是綁定的,所以不會存在線程安全問題~~~~
Object handler = lookupHandler(lookupPath, request);
// 若沒找到:
if (handler == null) {
// 處理跟路徑 / 和默認的Handler~~~~
Object rawHandler = null;
if ("/".equals(lookupPath)) {
rawHandler = getRootHandler();
}
if (rawHandler == null) {
rawHandler = getDefaultHandler();
}
if (rawHandler != null) {
if (rawHandler instanceof String) {
String handlerName = (String) rawHandler;
rawHandler = obtainApplicationContext().getBean(handlerName);
}
validateHandler(rawHandler, request);
// 就是注冊上面說的默認的兩個攔截器~~~~~~~ 第四個參數為null,就只會注冊一個攔截器~~~
// 然后把rawHandler轉換成chain(這個時候chain里面可能已經有兩個攔截器了,然后父類還會繼續把用戶自定義的攔截器放上去~~~~)
handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null);
}
}
return handler;
}
// =========該抽象類提供的這個方法就特別重要了:向handlerMap里面put值的唯一入口~~~ 可以批量urls
protected void registerHandler(String[] urlPaths, String beanName) throws BeansException, IllegalStateException {
Assert.notNull(urlPaths, "URL path array must not be null");
for (String urlPath : urlPaths) {
registerHandler(urlPath, beanName);
}
}
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;
// 如果是beanName,並且它是立馬加載的~~~~
if (!this.lazyInitHandlers && handler instanceof String) {
String handlerName = (String) handler;
ApplicationContext applicationContext = obtainApplicationContext();
// 並且還需要是單例的,那就立馬實例化吧~~~~
if (applicationContext.isSingleton(handlerName)) {
resolvedHandler = applicationContext.getBean(handlerName);
}
}
// 先嘗試從Map中去獲取
Object mappedHandler = this.handlerMap.get(urlPath);
if (mappedHandler != null) {
// 這個異常錯誤信息,相信我們在開發中經常碰到吧:簡單就是說就是一個URL只能映射到一個Handler上(但是一個Handler是可以處理多個URL的,這個需要注意)
// 這個校驗必不可少啊~~~~
if (mappedHandler != resolvedHandler) {
throw new IllegalStateException("Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
"]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
}
} else {
// 如果你的handler處理的路徑是根路徑,那太好了 你的這個處理器就很特殊啊~~~~
if (urlPath.equals("/")) {
setRootHandler(resolvedHandler);
}
// 這個路徑相當於處理所有 優先級是最低的 所以當作默認的處理器來使用~~~~
else if (urlPath.equals("/*")) {
setDefaultHandler(resolvedHandler);
}
// 正常的路徑了~~~
// 注意此處:好像是Spring5之后 把這句Mapped的日志級別 直接降低到trace級別了,簡直太低了有木有~~~
// 在Spring 5之前,這里的日志級別包括上面的setRoot等是info(所以我們在控制台經常能看見大片的'Mapped URL path'日志~~~~)
// 所以:自Spring5之后不再會看controller這樣的映射的日志了(除非你日志界別調低~~~)可能Spring認為這種日志多,且不認為是重要的信息吧~~~
else {
this.handlerMap.put(urlPath, resolvedHandler);
if (logger.isTraceEnabled()) {
logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler));
}
}
}
}
// 該緩存也提供了一個只讀視圖給調用者訪問~~~
public final Map<String, Object> getHandlerMap() {
return Collections.unmodifiableMap(this.handlerMap);
}
}
該抽象類提供了一個Map,緩存着URL和它對應的Handler,這是個非常重要的緩存。它提供了registerHandler()允許子類調用,向緩存里注冊url和handler的對應關系~
注意:此處肯定不能把Map放出去,讓子類直接put的。因為程序必須要高內聚,才能保證更好的隔離性以及穩定性
AbstractDetectingUrlHandlerMapping
這又是個抽象類,繼承自AbstractUrlHandlerMapping。它就越來越具有功能化了:Detecting表明它是有檢測URL的功能的~
// @since 2.5 它Spring2.5后才出來
public abstract class AbstractDetectingUrlHandlerMapping extends AbstractUrlHandlerMapping {
// 是否要去祖先容器里面檢測所有的Handlers 默認是false表示只在自己的容器里面找
// 若設置為true,相當於在父容器里的Controller也會被挖出來~~~~ 一般我並不建議這么去做
private boolean detectHandlersInAncestorContexts = false;
public void setDetectHandlersInAncestorContexts(boolean detectHandlersInAncestorContexts) {
this.detectHandlersInAncestorContexts = detectHandlersInAncestorContexts;
}
// 說白了,這里是檢測的入口 detectHandlers();
@Override
public void initApplicationContext() throws ApplicationContextException {
super.initApplicationContext();
detectHandlers();
}
protected void detectHandlers() throws BeansException {
// 這個就不解釋了:默認只會在當前容器里面去查找檢測~~~
// 注意:這里使用的Object.class 說明是把本容器內所有類型的Bean定義都拿出來了~~~~
String[] beanNames = (this.detectHandlersInAncestorContexts ? BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
getApplicationContext().getBeanNamesForType(Object.class));
// Take any bean name that we can determine URLs for.
for (String beanName : beanNames) {
// 這是個抽象方法由子類去實現。 它的作用就是看看url和bean怎么才算是匹配呢?也就是說這個handler到底能夠處理哪些URL呢?
// 注意:此處還是類級別(Bean),相當於一個類就是一個Handler哦~~~~
String[] urls = determineUrlsForHandler(beanName);
if (!ObjectUtils.isEmpty(urls)) {
// 注冊進去 緩存起來~
registerHandler(urls, beanName);
}
}
}
}
AbstractDetectingUrlHandlerMapping是通過掃描方式注冊Handler,收到請求時由AbstractUrlHandlerMapping的getHandlerInternal進行分發,看看到底是交給哪個Handler進行處理~
BeanNameUrlHandlerMapping
它是AbstractDetectingUrlHandlerMapping的唯一實現類。
說明:DefaultAnnotationHandlerMapping、BeanNameUrlHandlerMapping、AbstractControllerUrlHandlerMapping在Spring4.3的時候都被標記為過期,在Spring5以后直接就把這些類干掉了,因此本處說的唯一、源碼都是基於Spring5.以上的版本的~~~
public class BeanNameUrlHandlerMapping extends AbstractDetectingUrlHandlerMapping {
@Override
protected String[] determineUrlsForHandler(String beanName) {
List<String> urls = new ArrayList<>();
// 意思就是必須以/開頭才行~~~~~~這算是一種約定吧~~~
// 這種方式和@WebServlet方式一毛一樣~~~~~
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的名稱來匹配URL。方式同@WebServlet一模一樣。
SimpleUrlHandlerMapping
它是AbstractUrlHandlerMapping的直接實現類,也是一個基於Map的簡單實現。
public class SimpleUrlHandlerMapping extends AbstractUrlHandlerMapping {
private final Map<String, Object> urlMap = new LinkedHashMap<>();
public void setMappings(Properties mappings) {
CollectionUtils.mergePropertiesIntoMap(mappings, this.urlMap);
}
public void setUrlMap(Map<String, ?> urlMap) {
this.urlMap.putAll(urlMap);
}
@Override
public void initApplicationContext() throws BeansException {
super.initApplicationContext();
registerHandlers(this.urlMap);
}
// 這個實現簡單到令人發指
protected void registerHandlers(Map<String, Object> urlMap) throws BeansException {
if (urlMap.isEmpty()) {
logger.trace("No patterns in " + formatMappingName());
} else {
urlMap.forEach((url, handler) -> {
// 如果還沒有斜線,在前面加上斜線
if (!url.startsWith("/")) {
url = "/" + url;
}
if (handler instanceof String) {
handler = ((String) handler).trim();
}
registerHandler(url, handler);
});
}
}
}
它的實現就是把開發者指定的一個Map,然后容器啟動的時候把它注冊進去即可,非常的簡單的一個實現。當然我們自己已經知道了URL和Handler的映射關系了,然后需要進一步構造出一個HandlerMapping的時候,或許它是一個較快解決問題的選擇~~~~ 它最重要的是urlMap這個參數~
它一般用於基於XML的配置文件的形式,形如:
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property>
<map>
<entry key="/hello.do" value="myController"/>
<entry key="/my.do" value="myController"/>
</map>
</property>
</bean>
示例
寫一個控制器:
@Controller("/hello") // 注意此處BeanName必須是/開頭,否則是不會作為handler的
public class HelloController { }
這樣項目啟動的時候,就可以看到這么一句日志,證明url是映射成功了的~:
org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping.registerHandler Mapped URL path [/hello] onto handler '/hello'
但是請求后報出如下錯誤
No adapter for handler [com.fsx.controller.HelloController@5e31e1a4]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler
找不到適配器,這是屬於HandlerAdapter的內容,此處介紹怎么做:
// 實現`org.springframework.web.servlet.mvc.Controller`這個接口,才被認為是一個控制器~
@Controller("/hello")
public class HelloController extends AbstractController {
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("this is my demo");
return null;
}
}
這樣子我們再請求,就能正常的進入handleRequestInternal方法來處理請求了。
我們的Controller類必須(直接|間接)實現org.springframework.web.servlet.mvc.Controller接口,否則無法判斷具體處理方法是誰!!!
BeanNameUrlHandlerMapping和SimpleUrlHandlerMapping,屬於Spring最早期的控制器實現。完全是基於類級別的:一個類就是一個Handler。
從分析DispatcherServlet的時候發現,SpringMVC默認是給容器內注入了兩個HandlerMapping組件的:RequestMappingHandlerMapping和BeanNameUrlHandlerMapping。
分析過了HandlerMapping的一些抽象實現,以及AbstractHandlerMapping的一個主要分支:AbstractUrlHandlerMapping體系的實現原理分析:它是基於類級別的Handler實現,大體上和原生Servlet如出一轍,也還沒有脫原生Servlet的API。
接下來將介紹它的另外一個系列:AbstractHandlerMethodMapping系列,基於方法級別的Handler實現。也是當下最為主流的實現方式,更是最為常用使用方式。
AbstractHandlerMethodMapping系列
AbstractHandlerMethodMapping系列是將method作為handler來使用的,比如@RequestMapping所注釋的方法就是這種handler(當然它並不強制你一定得使用@RequestMapping這樣的注解)。
在前面我們已經知道了AbstractHandlerMethodMapping的父類AbstractHandlerMapping,其定義了抽象方法getHandlerInternal(HttpServletRequest request),那么這里主要看看它對此抽象方法的實現:
// @since 3.1 Spring3.1之后才出現,這個時候注解驅動也出來了
// 實現了initializingBean接口,其實主要的注冊操作則是通過afterPropertiesSet()接口方法來調用的
// 它是帶有泛型T的。
// T:包含HandlerMethod與傳入請求匹配所需條件的handlerMethod的映射~
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
// SCOPED_TARGET的BeanName的前綴
private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget.";
private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH = new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class, "handle"));
// 跨域相關
private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration();
static {
ALLOW_CORS_CONFIG.addAllowedOrigin("*");
ALLOW_CORS_CONFIG.addAllowedMethod("*");
ALLOW_CORS_CONFIG.addAllowedHeader("*");
ALLOW_CORS_CONFIG.setAllowCredentials(true);
}
// 默認不會去祖先容器里面找Handlers
private boolean detectHandlerMethodsInAncestorContexts = false;
// @since 4.1提供的新接口
// 為處HandlerMetho的映射分配名稱的策略接口 只有一個方法getName()
// 唯一實現為:RequestMappingInfoHandlerMethodMappingNamingStrategy
// 策略為:@RequestMapping指定了name屬性,那就以指定的為准 否則策略為:取出Controller所有的`大寫字母` + # + method.getName()
// 如:AppoloController#match方法 最終的name為:AC#match
// 當然這個你也可以自己實現這個接口,然后set進來即可(只是一般沒啥必要這么去干~~)
@Nullable
private HandlerMethodMappingNamingStrategy<T> namingStrategy;
// 內部類:負責注冊~
private final MappingRegistry mappingRegistry = new MappingRegistry();
// 此處細節:使用的是讀寫鎖 比如此處使用的是讀鎖 獲得所有的注冊進去的Handler的Map
public Map<T, HandlerMethod> getHandlerMethods() {
this.mappingRegistry.acquireReadLock();
try {
return Collections.unmodifiableMap(this.mappingRegistry.getMappings());
} finally {
this.mappingRegistry.releaseReadLock();
}
}
// 此處是根據mappingName來獲取一個Handler 此處需要注意哦~~~
@Nullable
public List<HandlerMethod> getHandlerMethodsForMappingName(String mappingName) {
return this.mappingRegistry.getHandlerMethodsByMappingName(mappingName);
}
// 最終都是委托給mappingRegistry去做了注冊的工作 此處日志級別為trace級別
public void registerMapping(T mapping, Object handler, Method method) {
if (logger.isTraceEnabled()) {
logger.trace("Register \"" + mapping + "\" to " + method.toGenericString());
}
this.mappingRegistry.register(mapping, handler, method);
}
public void unregisterMapping(T mapping) {
if (logger.isTraceEnabled()) {
logger.trace("Unregister mapping \"" + mapping + "\"");
}
this.mappingRegistry.unregister(mapping);
}
// 這個很重要,是初始化HandlerMethods的入口~~~~~
@Override
public void afterPropertiesSet() {
initHandlerMethods();
}
// 看initHandlerMethods(),觀察是如何實現加載HandlerMethod
protected void initHandlerMethods() {
// getCandidateBeanNames:Object.class相當於拿到當前容器(一般都是當前容器) 內所有的Bean定義信息
// 如果閣下容器隔離到到的話,這里一般只會拿到@Controller標注的web組件 以及其它相關web組件的 不會非常的多的~~~~
for (String beanName : getCandidateBeanNames()) {
// BeanName不是以這個打頭得 這里才會process這個BeanName~~~~
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
// 會在每個Bean里面找處理方法,HandlerMethod,然后注冊進去
processCandidateBean(beanName);
}
}
// 略:它就是輸出一句日志:debug日志或者trace日志 `7 mappings in 'requestMappingHandlerMapping'`
handlerMethodsInitialized(getHandlerMethods());
}
// 確定指定的候選bean的類型,如果標識為Handler類型,則調用DetectHandlerMethods
// isHandler(beanType):判斷這個type是否為Handler類型 它是個抽象方法,由子類去決定到底啥才叫Handler~~~~
// `RequestMappingHandlerMapping`的判斷依據為:該類上標注了@Controller注解或者@Controller注解 就算作是一個Handler
// 所以此處:@Controller起到了一個特殊的作用,不能等價於@Component的喲~~~~
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
} catch (Throwable ex) {
// 即使拋出異常 程序也不會終止~
}
if (beanType != null && isHandler(beanType)) {
// 這個和我們上篇博文講述的類似,都屬於detect探測系列~~~~
detectHandlerMethods(beanName);
}
}
// 在指定的Handler的bean中查找處理程序方法Methods 找打就注冊進去:mappingRegistry
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
// 又是非常熟悉的方法:MethodIntrospector.selectMethods
// 它在我們招@EventListener、@Scheduled等注解方法時已經遇到過多次
// 此處特別之處在於:getMappingForMethod屬於一個抽象方法,由子類去決定它的尋找規則~~~~ 什么才算作一個處理器方法
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);
}
});
// 把找到的Method 一個個遍歷,注冊進去~~~~
methods.forEach((method, mapping) -> {
// 找到這個可調用的方法(AopUtils.selectInvocableMethod)
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
}
該抽象類完成了所有的Handler以及handler里面所有的HandlerMethod的模版操作,但是決定哪些Bean是Handler類和哪些方法才是HandlerMathod,這些邏輯都是交給子類自己去實現,所以這層抽象可謂也是非常的靈活,並沒有把Handler的實現方式定死,允許不同。
這里面有個核心內容:那就是注冊handlerMethod,是交給AbstractHandlerMethodMapping的一個內部類MappingRegistry去完成的,用來專門維持所有的映射關系,並提供方法去查找方法去提供當前url映射的方法。
AbstractHandlerMethodMapping.MappingRegistry:內部類注冊中心
維護幾個Map(鍵值對),用來存儲映射的信息, 還有一個MappingRegistration專門保存注冊信息
class MappingRegistry {
// mapping對應的其MappingRegistration對象~~~
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
// 保存着mapping和HandlerMethod的對應關系~
private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
// 保存着URL與匹配條件(mapping)的對應關系 當然這里的URL是pattern式的,可以使用通配符
// 這里的Map不是普通的Map,而是MultiValueMap,它是個多值Map。其實它的value是一個list類型的值
// 至於為何是多值?有這么一種情況 URL都是/api/v1/hello 但是有的是get post delete等方法 所以有可能是會匹配到多個MappingInfo的
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
// 這個Map是Spring MVC4.1新增的(畢竟這個策略接口HandlerMethodMappingNamingStrategy在Spring4.1后才有,這里的name是它生成出來的)
// 保存着name和HandlerMethod的對應關系(一個name可以有多個HandlerMethod)
private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
// 這兩個就不用解釋了
private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
// 讀寫鎖~~~ 讀寫分離 提高啟動效率
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
... // 提供一些查找方法,都不是線程安全的
// 讀鎖提供給外部訪問,寫鎖自己放在內部即可~~~
public void acquireReadLock() {
this.readWriteLock.readLock().lock();
}
public void releaseReadLock() {
this.readWriteLock.readLock().unlock();
}
// 注冊Mapping和handler 以及Method 此處上寫鎖保證線程安全~
public void register(T mapping, Object handler, Method method) {
this.readWriteLock.writeLock().lock();
try {
// 此處注意:都是new HandlerMethod()了一個新的出來~~~~
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
// 同樣的:一個URL Mapping只能對應一個Handler
// 這里可能會出現常見的一個異常信息:Ambiguous mapping. Cannot map XXX
assertUniqueMethodMapping(handlerMethod, mapping);
// 緩存Mapping和handlerMethod的關系
this.mappingLookup.put(mapping, handlerMethod);
// 保存url和RequestMappingInfo(mapping)對應關系
// 這里注意:多個url可能對應着同一個mappingInfo呢~ 畢竟@RequestMapping的url是可以寫多個的~~~~
List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
this.urlLookup.add(url, mapping);
}
// 保存name和handlerMethod的關系 同樣也是一對多
String name = null;
if (getNamingStrategy() != null) {
name = getNamingStrategy().getName(handlerMethod, mapping);
addMappingName(name, handlerMethod);
}
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
this.corsLookup.put(handlerMethod, corsConfig);
}
// 注冊mapping和MappingRegistration的關系
this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
}
// 釋放鎖
finally {
this.readWriteLock.writeLock().unlock();
}
}
// 相當於進行一次逆向操作~
public void unregister(T mapping) { ... }
...
}
這個注冊中心,核心是保存了多個Map映射關系,相當於緩存下來。在請求過來時需要查找的時候,可以迅速定位到處理器。
下面繼續,終於來到AbstractHandlerMethodMapping它對父類抽象方法:getHandlerInternal的實現如下:
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
...
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
// 要進行匹配的 請求的URI path
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
this.mappingRegistry.acquireReadLock();
try {
//委托給方法lookupHandlerMethod() 去找到一個HandlerMethod去最終處理~
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
// Match是一個private class,內部就兩個屬性:T mapping和HandlerMethod handlerMethod
List<Match> matches = new ArrayList<>();
// 根據lookupPath去注冊中心里查找mappingInfos,因為一個具體的url可能匹配上多個MappingInfo的
// 至於為何是多值?有這么一種情況 URL都是/api/v1/hello 但是有的是get post delete等方法 當然還有可能是headers/consumes等等不一樣,都算多個的 所以有可能是會匹配到多個MappingInfo的
// 所有這個里可以匹配出多個出來。比如/hello 匹配出GET、POST、PUT都成,所以size可以為3
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
// 依賴於子類實現的抽象方法:getMatchingMapping() 看看到底匹不匹配,而不僅僅是URL匹配就行
// 比如還有method、headers、consumes等等這些不同都代表着不同的MappingInfo的
// 最終匹配上的,會new Match()放進matches里面去
addMatchingMappings(directPathMatches, matches, request);
}
// 當還沒有匹配上的時候,別無選擇,只能瀏覽所有映射
// 這里為何要瀏覽所有的mappings呢?而不是報錯404呢?這里我有點迷糊,願有知道的指明這個設計意圖~~~
if (matches.isEmpty()) {
// No choice but to go through all mappings...
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
// 單反只要找到了一個匹配的 就進來這里了~~~
// 請注意:因為到這里 匹配上的可能還不止一個 所以才需要繼續處理~~
if (!matches.isEmpty()) {
// getMappingComparator這個方法也是抽象方法由子類去實現的。
// 比如:`RequestMappingInfoHandlerMapping`的實現為先比較Method,patterns、params
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
// 排序后的最佳匹配為get(0)
Match bestMatch = matches.get(0);
// 如果總的匹配個數大於1的話
if (matches.size() > 1) {
if (CorsUtils.isPreFlightRequest(request)) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
// 次最佳匹配
Match secondBestMatch = matches.get(1);
// 如果發現次最佳匹配和最佳匹配 比較是相等的 那就報錯吧~~~~
// Ambiguous handler methods mapped for~~~
// 注意:這個是運行時的檢查,在啟動的時候是檢查不出來的~~~ 所以運行期的這個檢查也是很有必要的~~~ 否則就會出現意想不到的效果
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
// 把最最佳匹配的方法 放進request的屬性里面~~~
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
// 它也是做了一件事:request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath)
handleMatch(bestMatch.mapping, lookupPath, request);
// 最終返回的是HandlerMethod~~~
return bestMatch.handlerMethod;
}
// 一個都沒匹配上,handleNoMatch這個方法雖然不是抽象方法,protected方法子類復寫
// RequestMappingInfoHandlerMapping有復寫此方法~~~~
else {
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
...
// 因為上面說了mappings可能會有多個,比如get post put的都算~~~這里就是要進行篩選出所有match上的
private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
for (T mapping : mappings) {
// 只有RequestMappingInfoHandlerMapping 實現了一句話:return info.getMatchingCondition(request);
// 因此RequestMappingInfo#getMatchingCondition() 方法里大有文章可為~~~
// 它會對所有的methods、params、headers... 都進行匹配 但凡匹配不上的就返回null
T match = getMatchingMapping(mapping, request);
if (match != null) {
matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
}
}
}
}
// ===============RequestMappingInfo 的源碼部分講解================
public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
// 這些個匹配器都繼承自AbstractRequestCondition,會進行各自的匹配工作
// 下面會以PatternsRequestCondition為例進行示例講解~~~~~
// 他們頂級抽象接口為:RequestCondition @since 3.1 :Contract for request mapping conditions
private final PatternsRequestCondition patternsCondition;
private final RequestMethodsRequestCondition methodsCondition;
private final ParamsRequestCondition paramsCondition;
private final HeadersRequestCondition headersCondition;
private final ConsumesRequestCondition consumesCondition;
private final ProducesRequestCondition producesCondition;
private final RequestConditionHolder customConditionHolder;
// 因為類上和方法上都可能會有@RequestMapping注解,所以這里是把語意思合並 該方法來自頂層接口
@Override
public RequestMappingInfo combine(RequestMappingInfo other) {
String name = combineNames(other);
PatternsRequestCondition patterns = this.patternsCondition.combine(other.patternsCondition);
RequestMethodsRequestCondition methods = this.methodsCondition.combine(other.methodsCondition);
ParamsRequestCondition params = this.paramsCondition.combine(other.paramsCondition);
HeadersRequestCondition headers = this.headersCondition.combine(other.headersCondition);
ConsumesRequestCondition consumes = this.consumesCondition.combine(other.consumesCondition);
ProducesRequestCondition produces = this.producesCondition.combine(other.producesCondition);
RequestConditionHolder custom = this.customConditionHolder.combine(other.customConditionHolder);
return new RequestMappingInfo(name, patterns,
methods, params, headers, consumes, produces, custom.getCondition());
}
// 合並后,就開始發揮作用了,該接口來自於頂層接口~~~~
@Override
@Nullable
public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
if (methods == null) {
return null;
}
ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
if (params == null) {
return null;
}
HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
if (headers == null) {
return null;
}
ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
if (consumes == null) {
return null;
}
ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
if (produces == null) {
return null;
}
PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
if (patterns == null) {
return null;
}
RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
if (custom == null) {
return null;
}
return new RequestMappingInfo(this.name, patterns,
methods, params, headers, consumes, produces, custom.getCondition());
}
}
到這里,這個抽象類所做的工作都全部完成了。
可以看到它做的事還是非常非常多的。它用泛型來抽象Mapping關系(包括條件、屬性等),實現並不要求一定是@RequestMapping這種注解的方式,可以是任意方式,體現了它對擴展開放的設計思想~
Spring MVC請求URL帶后綴匹配的情況,如/hello.json也能匹配/hello
RequestMappingInfoHandlerMapping 在處理http請求的時候, 如果 請求url 有后綴,如果找不到精確匹配的那個@RequestMapping方法。那么,就把后綴去掉,然后.*去匹配,這樣,一般都可以匹配,默認這個行為是被開啟的。
比如有一個@RequestMapping("/rest"), 那么精確匹配的情況下, 只會匹配/rest請求。 但如果我前端發來一個 /rest.abcdef 這樣的請求, 又沒有配置 @RequestMapping("/rest.abcdef") 這樣映射的情況下, 那么@RequestMapping("/rest") 就會生效。
這樣會帶來什么問題呢?絕大多數情況下是沒有問題的,但是如果你是一個對權限要求非常嚴格的系統,強烈關閉此項功能,否則你會有意想不到的"收獲"。
究其原因咱們可以接着上面的分析,其實就到了PatternsRequestCondition這個類上,具體實現是它的匹配邏輯來決定的。
public final class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> {
...
@Override
@Nullable
public PatternsRequestCondition getMatchingCondition(HttpServletRequest request) {
// patterns表示此MappingInfo可以匹配的值們。一般對應@RequestMapping注解上的patters數組的值
if (this.patterns.isEmpty()) {
return this;
}
// 拿到待匹配的值,比如此處為"/hello.json"
String lookupPath = this.pathHelper.getLookupPathForRequest(request);
// 最主要就是這個方法了,它拿着這個lookupPath匹配~~~~
List<String> matches = getMatchingPatterns(lookupPath);
// 此處如果為empty,就返回null了~~~~
return (!matches.isEmpty() ? new PatternsRequestCondition(matches, this.pathHelper, this.pathMatcher, this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions) : null);
}
public List<String> getMatchingPatterns(String lookupPath) {
List<String> matches = new ArrayList<>();
for (String pattern : this.patterns) {
// 最最最重點就是在getMatchingPattern()這個方法里~~~ 拿着lookupPath和pattern看它倆合拍不~
String match = getMatchingPattern(pattern, lookupPath);
if (match != null) {
matches.add(match);
}
}
// 解釋一下為何匹配的可能是多個。因為url匹配上了,但是還有可能@RequestMapping的其余屬性匹配不上啊,所以此處需要注意 是可能匹配上多個的 最終是唯一匹配就成~
if (matches.size() > 1) {
matches.sort(this.pathMatcher.getPatternComparator(lookupPath));
}
return matches;
}
// // ===============url的真正匹配規則 非常重要~~~===============
// 注意這個方法的取名,上面是負數,這里是單數~~~~命名規范也是有藝術感的
@Nullable
private String getMatchingPattern(String pattern, String lookupPath) {
// 完全相等,那就不繼續聊了~~~
if (pattern.equals(lookupPath)) {
return pattern;
}
// 注意了:useSuffixPatternMatch 這個屬性就是我們最終要關閉后綴匹配的關鍵
// 這個值默外部給傳的true(其實內部默認值是boolean類型為false)
if (this.useSuffixPatternMatch) {
// 這個意思是若useSuffixPatternMatch=true我們支持后綴匹配。我們還可以配置fileExtensions讓只支持我們自定義的指定的后綴匹配,而不是下面最終的.*全部支持
if (!this.fileExtensions.isEmpty() && lookupPath.indexOf('.') != -1) {
for (String extension : this.fileExtensions) {
if (this.pathMatcher.match(pattern + extension, lookupPath)) {
return pattern + extension;
}
}
}
// 若你沒有配置指定后綴匹配,並且你的handler也沒有.*這樣匹配的,那就默認你的pattern就給你添加上后綴".*",表示匹配所有請求的url的后綴~~~
else {
boolean hasSuffix = pattern.indexOf('.') != -1;
if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) {
return pattern + ".*";
}
}
}
// 若匹配上了 直接返回此patter
if (this.pathMatcher.match(pattern, lookupPath)) {
return pattern;
}
// 這又是它支持的匹配規則。默認useTrailingSlashMatch它也是true
// 這就是為何我們的/hello/也能匹配上/hello的原因
// 從這可以看出,Spring MVC的寬容度是很高的,容錯處理做得是非常不錯的~~~~~~~
if (this.useTrailingSlashMatch) {
if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) {
return pattern + "/";
}
}
return null;
}
}
分析了URL的匹配原因,現在肯定知道為何默認情況下"/hello.aaaa"或者"/hello.aaaa/“或者”"/hello/""能匹配上我們/hello的原因了吧~~~
Spring和SpringBoot中如何關閉此項功能呢?
為何要關閉的理由,上面其實已經說了。當我們涉及到嚴格的權限校驗(強權限控制)的時候。特別是一些銀行系統、資產系統等等,關閉后綴匹配事非常有必要的。
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping implements MatchableHandlerMapping, EmbeddedValueResolverAware {
private boolean useSuffixPatternMatch = true;
private boolean useTrailingSlashMatch = true;
}
可以看到這兩個屬性值都直接冒泡到RequestMappingHandlerMapping這個實現類上來了,所以我們直接通過配置來改變它的默認行為就成。
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
// 關閉后綴名匹配,關閉最后一個/匹配
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(false);
configurer.setUseTrailingSlashMatch(false);
}
}
就這么一下,我們的URL就安全了,再也不能后綴名任意匹配了。再想用后綴匹配,直接就給你404:
RequestMappingInfoHandlerMapping
提供匹配條件RequestMappingInfo的解析處理。
// @since 3.1 此處泛型為:RequestMappingInfo 用這個類來表示mapping映射關系、參數、條件等
public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {
// 專門處理Http的Options方法的HandlerMethod
private static final Method HTTP_OPTIONS_HANDLE_METHOD;
static {
try {
HTTP_OPTIONS_HANDLE_METHOD = HttpOptionsHandler.class.getMethod("handle");
} catch (NoSuchMethodException ex) {
throw new IllegalStateException("Failed to retrieve internal handler method for HTTP OPTIONS", ex);
}
}
// 構造函數:給set了一個HandlerMethodMappingNamingStrategy
protected RequestMappingInfoHandlerMapping() {
setHandlerMethodMappingNamingStrategy(new RequestMappingInfoHandlerMethodMappingNamingStrategy());
}
// 復寫父類的抽象方法:獲取mappings里面的patters們~~~
@Override
protected Set<String> getMappingPathPatterns(RequestMappingInfo info) {
return info.getPatternsCondition().getPatterns();
}
// 校驗看看這個Mapping是否能匹配上這個request,若能匹配上就返回一個RequestMappingInfo
@Override
protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
return info.getMatchingCondition(request);
}
@Override
protected Comparator<RequestMappingInfo> getMappingComparator(final HttpServletRequest request) {
return (info1, info2) -> info1.compareTo(info2, request);
}
...
}
它主要做的事就是確定了泛型類型為:RequestMappingInfo,然后很多方法都依托它來完成判定邏輯,比如上面三個@Override方法就是對父類抽象方法的實現。委托給RequestMappingInfo去實現的~
而RequestMappingInfo的構建工作,Spring MVC理論上是可以允許有多種方案。Spring MVC給出的唯一實現類為RequestMappingHandlerMapping。
下面就介紹Spring MVC目前的唯一構造方案:通過@RequestMapping來構造一個RequestMappingInfo。
RequestMappingHandlerMapping 唯一實現類
根據@RequestMapping注解生成RequestMappingInfo,同時提供isHandler實現。
直到這個具體實現類,才與具體的實現方式@RequestMapping做了強綁定了
有了三層抽象的實現,其實留給本類需要實現的功能已經不是非常的多了~
// @since 3.1 Spring3.1才提供的這種注解掃描的方式的支持~~~ 它也實現了MatchableHandlerMapping分支的接口
// EmbeddedValueResolverAware接口:說明要支持解析Spring的表達式~
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
implements MatchableHandlerMapping, EmbeddedValueResolverAware {
...
private Map<String, Predicate<Class<?>>> pathPrefixes = new LinkedHashMap<>();
// 配置要應用於控制器方法的路徑前綴
// @since 5.1:Spring5.1才出來的新特性,其實有時候還是很好的使的 下面給出使用的Demo
// 前綴用於enrich每個@RequestMapping方法的映射,至於匹不匹配由Predicate來決定 有種前綴分類的效果~~~~
// 推薦使用Spring5.1提供的類:org.springframework.web.method.HandlerTypePredicate
public void setPathPrefixes(Map<String, Predicate<Class<?>>> prefixes) {
this.pathPrefixes = Collections.unmodifiableMap(new LinkedHashMap<>(prefixes));
}
// @since 5.1 注意pathPrefixes是只讀的~~~因為上面Collections.unmodifiableMap了 有可能只是個空Map
public Map<String, Predicate<Class<?>>> getPathPrefixes() {
return this.pathPrefixes;
}
public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) {
this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch;
this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch);
}
// If enabled a method mapped to "/users" also matches to "/users/".
public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) {
this.useTrailingSlashMatch = useTrailingSlashMatch;
}
@Override
public void afterPropertiesSet() {
// 對RequestMappingInfo的配置進行初始化 賦值
this.config = new RequestMappingInfo.BuilderConfiguration();
this.config.setUrlPathHelper(getUrlPathHelper()); // 設置urlPathHelper默認為UrlPathHelper.class
this.config.setPathMatcher(getPathMatcher()); //默認為AntPathMatcher,路徑匹配校驗器
this.config.setSuffixPatternMatch(this.useSuffixPatternMatch); // 是否支持后綴補充,默認為true
this.config.setTrailingSlashMatch(this.useTrailingSlashMatch); // 是否添加"/"后綴,默認為true
this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch); // 是否采用mediaType匹配模式,比如.json/.xml模式的匹配,默認為false
this.config.setContentNegotiationManager(getContentNegotiationManager()); //mediaType處理類:ContentNegotiationManager
// 此處 必須還是要調用父類的方法的
super.afterPropertiesSet();
}
...
// 判斷該類,是否是一個handler(此處就體現出@Controller注解的特殊性了)
// 這也是為何我們的XXXController用@Bean申明是無效的原因(前提是類上木有@RequestMapping注解,否則也是闊儀的哦~~~)
// 因此我個人建議:為了普適性,類上的@RequestMapping也統一要求加上,即使你不寫@Value也木關系,這樣是最好的
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
// 還記得父類:AbstractHandlerMethodMapping#detectHandlerMethods的時候,回去該類里面找所有的指定的方法
// 而什么叫指定的呢?就是靠這個來判定方法是否符合條件的~~~~~
@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
// 第一步:先拿到方法上的info
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
// 方法上有。在第二步:拿到類上的info
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
// 倘若類上面也有,那就combine把兩者結合
// combile的邏輯基如下:
// names:name1+#+name2
// path:路徑拼接起來作為全路徑(容錯了方法里沒有/的情況)
// method、params、headers:取並集
// consumes、produces:以方法的為准,沒有指定再取類上的
// custom:誰有取誰的。若都有:那就看custom具體實現的.combine方法去決定把 簡單的說就是交給調用者了~~~
info = typeInfo.combine(info);
}
// 在Spring5.1之后還要處理這個前綴匹配~~~
// 根據這個類,去找看有沒有前綴 getPathPrefix():entry.getValue().test(handlerType) = true算是hi匹配上了
// 備注:也支持${os.name}這樣的語法拿值,可以把前綴也寫在專門的配置文件里面~~~~
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
// RequestMappingInfo.paths(prefix) 相當於統一在前面加上這個前綴~
info = RequestMappingInfo.paths(prefix).build().combine(info);
}
}
return info;
}
// 根據此方法/類,創建一個RequestMappingInfo
@Nullable
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
// 注意:此處使用的是findMergedAnnotation 這也就是為什么雖然@RequestMapping它並不具有繼承的特性,但是你子類仍然有繼承的效果的原因~~~~
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
// 請注意:這里進行了區分處理 如果是Class的話 如果是Method的話
// 這里返回的是一個condition 也就是看看要不要處理這個請求的條件~~~~
RequestCondition<?> condition = (element instanceof Class ?
getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
// 這個createRequestMappingInfo就是根據一個@RequestMapping以及一個condition創建一個
// 顯然如果沒有找到此注解,這里就返回null了,表面這個方法啥的就不是一個info~~~~
return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}
// 他倆都是返回的null。protected方法留給子類復寫,子類可以據此自己定義一套自己的規則來限制匹配
// Provide a custom method-level request condition.
// 它相當於在Spring MVC默認的規則的基礎上,用戶還可以自定義條件進行處理~~~~
@Nullable
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
return null;
}
@Nullable
protected RequestCondition<?> getCustomMethodCondition(Method method) {
return null;
}
// 根據@RequestMapping 創建一個RequestMappingInfo
protected RequestMappingInfo createRequestMappingInfo(RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {
RequestMappingInfo.Builder builder = RequestMappingInfo
// 強大的地方在此處:path里竟然還支持/api/v1/${os.name}/hello 這樣形式動態的獲取值
// 也就是說URL還可以從配置文件里面讀取 Spring考慮很周到啊~~~
// @GetMapping("/${os.name}/hello") // 支持從配置文件里讀取此值 Windows 10
.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
.methods(requestMapping.method())
.params(requestMapping.params())
.headers(requestMapping.headers())
.consumes(requestMapping.consumes())
.produces(requestMapping.produces())
.mappingName(requestMapping.name());
// 調用者自定義的條件~~~
if (customCondition != null) {
builder.customCondition(customCondition);
}
// 注意此處:把當前的config設置進去了~~~~
return builder.options(this.config).build();
}
@Override
public RequestMatchResult match(HttpServletRequest request, String pattern) { ... }
// 支持了@CrossOrigin注解 Spring4.2提供的注解
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) { ... }
}
至此RequestMappingHandlerMapping的初始化完成了。像pathPrefixes這種配置,可以全局統一配置來控制每個Controller,如常用的/api/v1前綴~
如何配置呢?下面給個示例:
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
//configurer.setUseSuffixPatternMatch(false); //關閉后綴名匹配,關閉最后一個/匹配
//configurer.setUseTrailingSlashMatch(false);
// 這樣HelloController上的方法自動就會有此前綴了,而別的controller上是不會有的
// 注意:這是Spring5.1后才支持的新特性
configurer.addPathPrefix("/api/v1", clazz -> clazz.isAssignableFrom(HelloController.class));
// 使用Spring提供的HandlerTypePredicate,更加的強大
HandlerTypePredicate predicate = HandlerTypePredicate.forBasePackage("com.buqiong");
//HandlerTypePredicate predicate = HandlerTypePredicate.forBasePackageClass(HelloController.class);
//HandlerTypePredicate predicate = HandlerTypePredicate.forAssignableType(...);
//HandlerTypePredicate predicate = HandlerTypePredicate.forAnnotation(...);
//HandlerTypePredicate predicate = HandlerTypePredicate.builder()
// .basePackage()
// .basePackageClass()
// .build();
configurer.addPathPrefix("/api/v2", predicate);
}
}
細節注意:若添加了兩prefix都可以作用在某個Controller上,那么會按照放入的順序(因為它是LinkedHashMap)以先匹配上的為准,可參考RequestMappingHandlerMapping#getPathPrefix方法~
RequestMappingHandlerMapping 向容器中注冊的時候,檢測到實現了 InitializingBean接口,容器去執行afterPropertiesSet(),在afterPropertiesSet中完成Controller中完成方法的映射。
以上就是Spring MVC在容器啟動過程中,完成URL到Handler映射的所有內容~
@RequestMapping屬性詳解
使用@RequestMapping 來映射URL 到控制器類,或者是到Controller 控制器的處理方法上。
當@RequestMapping 標記在Controller 類上的時候,里面使用@RequestMapping 標記的方法的請求地址都是相對於類上的@RequestMapping 而言的;當Controller 類上沒有標記@RequestMapping 注解時,方法上的@RequestMapping 都是絕對路徑。
這種絕對路徑和相對路徑所組合成的最終路徑都是相對於根路徑“/ ”而言的。
這個注解的屬性眾多,下面逐個解釋一下:
// @since 2.5 用於將Web請求映射到具有靈活方法簽名的請求處理類中的方法的注釋 Both Spring MVC and `Spring WebFlux` support this annotation
// @Mapping這個注解是@since 3.0 但它目前還只有這個地方使用到了~~~ 我感覺是多余的
@Target({ElementType.METHOD, ElementType.TYPE}) // 能夠用到類上和方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
//給這個Mapping取一個名字。若不填寫,就用HandlerMethodMappingNamingStrategy去按規則生成
String name() default "";
// 路徑 數組形式 可以寫多個。 一般都是按照Ant風格進行書寫~
@AliasFor("path")
String[] value() default {};
@AliasFor("value")
String[] path() default {};
// 請求方法:GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
// 顯然可以指定多個方法。如果不指定,表示適配所有方法類型~~
// 同時還有類似的枚舉類:org.springframework.http.HttpMethod
RequestMethod[] method() default {};
// 指定request中必須包含某些參數值時,才讓該方法處理
// 使用 params 元素,你可以讓多個處理方法處理到同一個URL 的請求, 而這些請求的參數是不一樣的
// 如:@RequestMapping(value = "/fetch", params = {"personId=10"} 和 @RequestMapping(value = "/fetch", params = {"personId=20"}
// 這兩個方法都處理請求`/fetch`,但是參數不一樣,進入的方法也不一樣~~~~
// 支持!myParam和myParam!=myValue這種~~~
String[] params() default {};
// 指定request中必須包含某些指定的header值,才能讓該方法處理請求
// @RequestMapping(value = "/head", headers = {"content-type=text/plain"}
String[] headers() default {};
// 指定處理請求request的**提交內容類型**(Content-Type),例如application/json、text/html等
// 相當於只有指定的這些Content-Type的才處理
// @RequestMapping(value = "/cons", consumes = {"application/json", "application/XML"}
// 不指定表示處理所有~~ 取值參見枚舉類:org.springframework.http.MediaType
// 它可以使用!text/plain形如這樣非的表達方式
String[] consumes() default {};
// 指定返回的內容類型,返回的內容類型必須是request請求頭(Accept)中所包含的類型
// 僅當request請求頭中的(Accept)類型中包含該指定類型才返回;
// 參見枚舉類:org.springframework.http.MediaType
// 它可以使用!text/plain形如這樣非的表達方式
String[] produces() default {};
}
Spring4.3之后提供了組合注解5枚:
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping
consumes 與 headers 區別
consumes produces params headers四個屬性都是用來縮小請求范圍。
consumes只能指定 content-Type 的內容類型,但是headers可以指定所有。
所以可以認為:headers是更為強大的(所有需要指定key和value嘛),而consumes和produces是專用的,頭的key是固定的,所以只需要寫value值即可,使用起來也更加的方便~。
推薦一個類:org.springframework.http.HttpHeaders,它里面有常量:幾乎所有的請求頭的key,以及我們可以很方便的構建一個HttpHeader,平時可以作為參考使用
Spring MVC默認使用的HandlerMapping是什么?
Spring對這塊的設計也是很靈活的,允許你自己配置,也允許你啥都不做,使用Spring默認的配置。處理代碼在:DispatcherServlet#initHandlerMappings
public class DispatcherServlet extends FrameworkServlet {
// 為此DispatcherServlet 初始化HandlerMappings
// 備注:DispatcherServlet是允許你有多個的~~~~
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
//detectAllHandlerMappings該屬性默認為true,表示會去容器內找所有的HandlerMapping類型的定義信息
// 若想改為false,請調用它的setDetectAllHandlerMappings() 自行設置值(絕大部分情況下沒啥必要)
if (this.detectAllHandlerMappings) {
// 這里注意:若你沒有標注注解`@EnableWebMvc`,那么這里找的結果是空的
// 若你標注了此注解,這個注解就會默認向容器內注入兩個HandlerMapping:RequestMappingHandlerMapping和BeanNameUrlHandlerMapping
Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// 多個的話 還需要進行一次排序~~~
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
// 不全部查找,那就只找一個名字為`handlerMapping`的HandlerMapping 實現精准控制
// 絕大多數情況下 我們並不需要這么做~
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
} catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}
// 若一個都沒找到自定義的,回滾到Spring的兜底策略,它會想容器注冊兩個:RequestMappingHandlerMapping和BeanNameUrlHandlerMapping
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
// 輸出trace日志:表示使用了兜底策略~
// 兜底策略配置文件:DispatcherServlet.properties
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
}
通過這段代碼,我們能夠很清晰的看到。絕大部分情況下,我們容器內會有這兩個HandlerMapping Bean:RequestMappingHandlerMapping和BeanNameUrlHandlerMapping
換句話說,默認情況下@RequestMapping和BeanNameUrl的方式都是被支持的~
請注意,使用@EnableWebMvc和不使用它有一個非常重要的區別:
使用@EnableWebMvc原來是依托於這個WebMvcConfigurationSupport config類向容器中注入了對應的Bean,所以它們都是交給了Spring管理的(所以你可以@Autowired它們)。但是,若是走了Spring它自己去讀取配置文件走默認值,它的Bean是沒有交給Spring管理的,沒有交給Spring管理的。它是這樣創建的:context.getAutowireCapableBeanFactory().createBean(clazz) 它創建出來的Bean都不會交給Spring管理。
在Spring5以下,DispatcherServlet.properties這個配置文件里寫的是這樣的:
相當於最底層默認使用的是DefaultAnnotationHandlerMapping,而在Spring5之后,改成了RequestMappingHandlerMapping。DefaultAnnotationHandlerMapping是Spring2.5用來處理@RequestMapping注解的,自從Spring3.2后已被標記為:@Deprecated。
需要注意的是:純Spring MVC環境下我們都會開啟@EnableWebMvc,所以我們實際使用的還是RequestMappingHandlerMapping的。而在SpringBoot環境下,雖然我們一般不建議標注@EnableWebMvc,但是Boot它默認也會注冊RequestMappingHandlerMapping的。
DefaultAnnotationHandlerMapping的一個小坑
在功能上DefaultAnnotationHandlerMapping和RequestMappingHandlerMapping絕大多數是等價的。但是因為DefaultAnnotationHandlerMapping過於古老了,它並不支持像@GetMapping(Spring4.3后提供)這樣的組合注解的。 從源碼角度理由如下:
比如Handler這么寫的:
@ResponseBody
@GetMapping("/hello/test")
public Object test(String userName) {
System.out.println(userName);
return null;
}
DefaultAnnotationHandlerMapping處理代碼為:
...
RequestMapping mapping = AnnotationUtils.findAnnotation(method, RequestMapping.class);
...
值如下:
發現我們的URL並沒有獲取到。
但是RequestMappingHandlerMapping的獲取代碼為:
...
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
...
可以發現使用AnnotatedElementUtils.findMergedAnnotation是支持這個組合注解的。但是AnnotatedElementUtils整個工具類是Spring4.0后才有,而DefaultAnnotationHandlerMapping早在Spring3.2后就被標記為廢棄了,因為就無需Spring繼續維護了~~~~
所以若你是純Spring MVC環境,為確保萬無一失,請開啟SpringMVC:@EnableWebMvc
若使用非組合注解如@RequestMapping,兩者大體一樣。但既然人家都廢棄了,所以非常不建議再繼續使用~~~
其實在Spring5.以后,就直接把這個兩個類拿掉了,所以也就沒有后顧之憂了。(DispatcherServlet.properties這個配置文件也做了對應的修改)
總結
Spring MVC在啟動時會掃描所有的@RequestMapping並封裝成對應的RequestMapingInfo。
一個請求過來會與RequestMapingInfo進行逐個比較,找到最適合的那個RequestMapingInfo。
Spring MVC通過HandlerMapping建立起了Url Pattern和Handler的對應關系,這樣任何一個URL請求過來時,就可以快速定位一個唯一的Handler,然后交給其進行處理了~
當然這里面還有很多實現細節,其中還有一個非常重要的一塊:HandlerAdapter,接下來我們會繼續討論。
參考: |