Spring源碼之請求路徑匹配路由方式
請求路徑匹配路由
在spring中,當(dāng)一個請求過來的時候會做路徑匹配,下面我們就從源碼層面分析一下路徑匹配。
示例:
@RequestMapping(value = "/user/{aid}/online/**", method = RequestMethod.GET)
我們一起看看這個方法是如何尋找的,和一些相應(yīng)的工具類
入口
我的項目使用的是自動配置的RequestMappingHandlerMapping類,在getHandlerInternal()方法中:
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
上面這行是根據(jù)你的請求path和request去查找合適的method了。在項目啟動的時候,Spring就把路徑和對應(yīng)的方法加載到了內(nèi)存中。
進(jìn)入上面方法
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// No choice but to go through all mappings...
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
可以看到如果根據(jù)lookupPath直接匹配上了,走第一個方法,如果沒有,則需要根據(jù)規(guī)則匹配,走第二個方法。
mappingRegistry.getMappings().keySer()這個方法獲取的類型為RequestMappingInfo類型,后面進(jìn)入了RequestMappingInfo的getMatchingCondition()方法:
public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
if (methods == null || params == null || headers == null || consumes == null || produces == null) {
return null;
}
PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
if (patterns == null) {
return null;
}
可以看到代碼里面會查看各種條件是否匹配,包括,請求方法methods,參數(shù)params,請求頭headers,還出入?yún)㈩愋偷认嚓P(guān)的consumers,produces等,最后一行就是我們要找的路徑匹配patternsCondition.getMatchingCondition(request)。
這個方法會走到PatternRequestCondition的getMatchingPattern方法,然后調(diào)用如下方法,獲取pattern:
if (this.pathMatcher.match(pattern, lookupPath)) {
return pattern;
}
上面這個pathMatcher的類型就是AntPathMatcher類,就是通過調(diào)用AntPathMatcher類的match方法,查看是否匹配,然后返回pattern。
SpringMVC 將請求找到匹配的處理
在SpringMVC的模式下,瀏覽器的一個請求是如何映射到指定的controller的呢?
初始化映射關(guān)系
在web服務(wù)器啟動時,Spring容器中會保存一個map的數(shù)據(jù)結(jié)構(gòu),里邊記錄這controller和url請求中的對應(yīng)關(guān)系。那么這個map中的數(shù)據(jù)是如何來的呢?
首先來看AbstractHandlerMethodMapping的initHandlerMethods方法(至于為什么直接找到這個方法,我也是網(wǎng)上搜索的,之前的調(diào)用鏈沒去糾結(jié))
protected void initHandlerMethods() {
if (logger.isDebugEnabled()) {
logger.debug("Looking for request mappings in application context: " + getApplicationContext());
}
//獲取Spring容器裝配的所有bean的名稱
String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
getApplicationContext().getBeanNamesForType(Object.class));
//遍歷
for (String beanName : beanNames) {
//判斷該bean是否有@controller或者@RequestMapping注解
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX) &&
isHandler(getApplicationContext().getType(beanName))){
//如果有上述注解,則需要保存對應(yīng)關(guān)系
detectHandlerMethods(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}
protected void detectHandlerMethods(final Object handler) {
//獲取傳過來handler的類信息
Class<?> handlerType =
(handler instanceof String ? getApplicationContext().getType((String) handler) : handler.getClass());
// Avoid repeated calls to getMappingForMethod which would rebuild RequestMappingInfo instances
//初始化一個保存映射信息的map
final Map<Method, T> mappings = new IdentityHashMap<Method, T>();
final Class<?> userType = ClassUtils.getUserClass(handlerType);
Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() {
@Override
public boolean matches(Method method) {
//獲取該類里所有方法的映射信息 T為RequestMappingInfo
//mapping值的形式為{[/test/test1],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}
T mapping = getMappingForMethod(method, userType);
if (mapping != null) {
//將信息加入map
mappings.put(method, mapping);
return true;
}
else {
return false;
}
}
});
for (Method method : methods) {
//注冊HandlerMethod,在里邊進(jìn)行一些重復(fù)驗證等
registerHandlerMethod(handler, method, mappings.get(method));
}
}
上述方法中調(diào)用了一個比較重要的方法,getMappingForMethod,通過這個方法生成后續(xù)我們一直會用到的一個RequestMappingInfo對象。具體方法如下:
@Override
//該方法接收兩個參數(shù),一個是具體方法,一個是方法所在的類
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = null;
//找到方法的@RequestMapping注解
RequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
if (methodAnnotation != null) {
//這個方法返回null
RequestCondition<?> methodCondition = getCustomMethodCondition(method);
//創(chuàng)建RequestMappingInfo對象
info = createRequestMappingInfo(methodAnnotation, methodCondition);
//找到類的@RequestMapping注解
RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class);
if (typeAnnotation != null) {
//該方法也返回一個null
RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
//如果類和方法都有@RequestMapping注解,則進(jìn)行combine操作
info = createRequestMappingInfo(typeAnnotation, typeCondition).combine(info);
}
}
return info;
}
那么上述方法中調(diào)用的createRequestMappingInfo方法有事如何真正的創(chuàng)建出一個RequestMappingInfo對象的呢?
protected RequestMappingInfo createRequestMappingInfo(RequestMapping annotation, RequestCondition<?> customCondition) {
//拿到@RequestMapping注解上的value值
String[] patterns = resolveEmbeddedValuesInPatterns(annotation.value());
//創(chuàng)建一個RequestMappingInfo,參數(shù)為一堆condition,出了PatternsRequestCondition,其余全部使用@RequestMapping注解上的值
return new RequestMappingInfo(
annotation.name(),
new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(),
this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions),
new RequestMethodsRequestCondition(annotation.method()),
new ParamsRequestCondition(annotation.params()),
new HeadersRequestCondition(annotation.headers()),
new ConsumesRequestCondition(annotation.consumes(), annotation.headers()),
new ProducesRequestCondition(annotation.produces(), annotation.headers(), this.contentNegotiationManager),
customCondition);
}
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
//handler此處為帶有@controller或者@RequestMapping的類的名稱
//初始化一個HandlerMethod,包含一些類的名稱和方法等信息
HandlerMethod newHandlerMethod = createHandlerMethod(handler, method);
HandlerMethod oldHandlerMethod = this.handlerMethods.get(mapping);
//判斷是否有handlerMethods是否有重復(fù)數(shù)據(jù),有則拋異常,沒有則將其加入handlerMethods map中
if (oldHandlerMethod != null && !oldHandlerMethod.equals(newHandlerMethod)) {
throw new IllegalStateException("Ambiguous mapping found. Cannot map '" + newHandlerMethod.getBean() +
"' bean method \n" + newHandlerMethod + "\nto " + mapping + ": There is already '" +
oldHandlerMethod.getBean() + "' bean method\n" + oldHandlerMethod + " mapped.");
}
this.handlerMethods.put(mapping, newHandlerMethod);
if (logger.isInfoEnabled()) {
logger.info("Mapped \"" + mapping + "\" onto " + newHandlerMethod);
}
//將沒有*號和問好的pattern加入到urlMap中
Set<String> patterns = getMappingPathPatterns(mapping);
for (String pattern : patterns) {
if (!getPathMatcher().isPattern(pattern)) {
this.urlMap.add(pattern, mapping);
}
}
//維護一個nameMap,key為名字,格式為congroller類大寫字母+#+方法名
//比如TestBank類的getBank方法,可以為TB#getBank
if (this.namingStrategy != null) {
String name = this.namingStrategy.getName(newHandlerMethod, mapping);
updateNameMap(name, newHandlerMethod);
}
}
由上述registerHandlerMethod方法我們可以看出,該方法共維護了三個map分別是:
handlermethods: key為RequestMappingInfo value為HandlerMethodurlMap: key為沒有*和?的pattern(比如/test/test1)value為RequestMappingInfonameMap: key為名字,格式為congroller類大寫字母+#+方法名,比如TestBank類的getBank方法,key為TB#getBank
上述三個map在后續(xù)匹配瀏覽器請求用哪個方法來處理時會重點用到。
從映射關(guān)系中尋找匹配方法
那么DispatcherServlet是如何處理一個請求的呢?
我們從DispatcherServlet的doService方法來看起,該方法中,最終會調(diào)用到AbstractHandlerMethodMapping類的lookupHandlerMethod方法來確定這個請求應(yīng)該由哪個方法處理,代碼如下:
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<Match>();
//從urlMap中尋找能匹配的處理方法
List<T> directPathMatches = this.urlMap.get(lookupPath);
//如果從urlMap中找到匹配的處理方法,則調(diào)用addMatchingMappings方法,將匹配的方法放入matches集合
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
//如果urlMap中沒有找到直接匹配的方法
if (matches.isEmpty()) {
// No choice but to go through all mappings...
addMatchingMappings(this.handlerMethods.keySet(), matches, request);
}
if (!matches.isEmpty()) {
//如果找到了匹配的方法,先獲取一個比較器
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
//將匹配到的方法按照比較器排序
Collections.sort(matches, comparator);
if (logger.isTraceEnabled()) {
logger.trace("Found " + matches.size() + " matching mapping(s) for [" + lookupPath + "] : " + matches);
}
//如果成功匹配的方法只有一個,拿這個方法返回。如果匹配到多個方法,取最匹配的前兩個進(jìn)行比較。
//如果比較結(jié)果為0,則拋出沒有找到唯一合適處理方法的異常
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
throw new IllegalStateException(
"Ambiguous handler methods mapped for HTTP path '" + request.getRequestURL() + "': {" +
m1 + ", " + m2 + "}");
}
}
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
//沒有找到匹配的則返回null
return handleNoMatch(handlerMethods.keySet(), lookupPath, request);
}
}
從上述代碼可以看出,程序會先從this.urlMap中尋找是否有匹配的方法,那么這個urlMap中的數(shù)據(jù)是從什么時候加載的呢?我們網(wǎng)上翻看registerHandlerMethod方法,在web服務(wù)器啟動時,該方法初始化了urlMap中的數(shù)據(jù)。
通過上述分析,大致可以了解到Spring容器是如何維護url和方法之間的映射關(guān)系,以及當(dāng)收到請求時又是如何將請求匹配到正確的方法的。
至于沒有分析到的當(dāng)類和方法都有@RequestMapping注解時觸發(fā)的combine操作究竟做了什么,當(dāng)找到多個匹配方法是又是如何通過比較器進(jìn)行排序的,我們下次再分析。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
java中 spring 定時任務(wù) 實現(xiàn)代碼
java中 spring 定時任務(wù) 實現(xiàn)代碼,需要的朋友可以參考一下2013-03-03
Docker容器使用宿主機上的mongod/redis等服務(wù)詳解
這篇文章主要介紹了Docker容器使用宿主機上的mongod/redis等服務(wù)詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11
深入學(xué)習(xí)Spring Cloud-Ribbon
這篇文章主要介紹了Spring Cloud-Ribbon的相關(guān)知識,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友一起看看吧2021-03-03
SpringBoot+MQTT+apollo實現(xiàn)訂閱發(fā)布功能的示例
這篇文章主要介紹了SpringBoot+MQTT+apollo實現(xiàn)訂閱發(fā)布功能的示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
POST方法給@RequestBody傳參數(shù)失敗的解決及原因分析
這篇文章主要介紹了POST方法給@RequestBody傳參數(shù)失敗的解決及原因分析,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10
springMVC+ajax實現(xiàn)文件上傳且?guī)нM(jìn)度條實例
本篇文章主要介紹了springMVC+ajax實現(xiàn)文件上傳且?guī)нM(jìn)度條實例,具有一定的參考價值,有興趣的可以了解一下。2017-01-01

