Spring MVC学习教程之RequestMappingHandlerMapping匹配
前言
对于RequestMappingHandlerMapping,使用Spring的同学基本都不会陌生,该类的作用有两个:
- 通过request查找对应的HandlerMethod,即当前request具体是由Controller中的哪个方法进行处理;
- 查找当前系统中的Interceptor,将其与HandlerMethod封装为一个HandlerExecutionChain。
本文主要讲解RequestMappingHandlerMapping是如何获取HandlerMethod和Interceptor,并且将其封装为HandlerExecutionChain的。
下面话不多说了,来一起看看详细的介绍吧
1.整体封装结构
RequestMappingHandlerMapping实现了HandlerMapping接口,该接口的主要方法如下:
publicinterfaceHandlerMapping{ //通过request获取HandlerExecutionChain对象 HandlerExecutionChaingetHandler(HttpServletRequestrequest)throwsException; }
这里我们直接看RequestMappingHandlerMapping是如何实现该接口的:
@Override @Nullable publicfinalHandlerExecutionChaingetHandler(HttpServletRequestrequest) throwsException{ //通过request获取具体的处理bean,这里handler可能有两种类型:HandlerMethod和String。 //如果是String类型,那么就在BeanFactory中查找该String类型的bean,需要注意的是,返回的 //bean如果是需要使用RequestMappingHandlerAdapter处理,那么也必须是HandlerMethod类型的 Objecthandler=getHandlerInternal(request); if(handler==null){ //如果找不到处理方法,则获取自定义的默认handler handler=getDefaultHandler(); } if(handler==null){ returnnull; } if(handlerinstanceofString){ //如果获取的handler是String类型的,则在当前BeanFactory中获取该名称的bean, //并将其作为handler返回 StringhandlerName=(String)handler; handler=obtainApplicationContext().getBean(handlerName); } //获取当前系统中配置的Interceptor,将其与handler一起封装为一个HandlerExecutionChain HandlerExecutionChainexecutionChain=getHandlerExecutionChain(handler,request); //这里CorsUtils.isCorsRequest()方法判断的是当前请求是否为一个跨域的请求,如果是一个跨域的请求, //则将跨域相关的配置也一并封装到HandlerExecutionChain中 if(CorsUtils.isCorsRequest(request)){ CorsConfigurationglobalConfig= this.globalCorsConfigSource.getCorsConfiguration(request); CorsConfigurationhandlerConfig=getCorsConfiguration(handler,request); CorsConfigurationconfig=(globalConfig!=null? globalConfig.combine(handlerConfig):handlerConfig); executionChain=getCorsHandlerExecutionChain(request,executionChain,config); } returnexecutionChain; }
从上面的代码可以看出,对于HandlerExecutionChain的获取,RequestMappingHandlerMapping首先会获取当前request对应的handler,然后将其与Interceptor一起封装为一个HandlerExecutionChain对象。这里在进行封装的时候,Spring会对当前request是否为跨域请求进行判断,如果是跨域请求,则将相关的跨域配置封装到HandlerExecutionChain中,关于跨域请求,读者可以阅读跨域资源共享CORS详解。
2.获取HandlerMethod
关于RequestMappingHandlerMapping是如何获取handler的,其主要在getHandlerInternal()方法中,如下是该方法的源码:
@Override protectedHandlerMethodgetHandlerInternal(HttpServletRequestrequest)throwsException{ //获取当前request的URI StringlookupPath=getUrlPathHelper().getLookupPathForRequest(request); if(logger.isDebugEnabled()){ logger.debug("Lookinguphandlermethodforpath"+lookupPath); } //获取注册的Mapping的读锁 this.mappingRegistry.acquireReadLock(); try{ //通过path和request查找具体的HandlerMethod HandlerMethodhandlerMethod=lookupHandlerMethod(lookupPath,request); if(logger.isDebugEnabled()){ if(handlerMethod!=null){ logger.debug("Returninghandlermethod["+handlerMethod+"]"); }else{ logger.debug("Didnotfindhandlermethodfor["+lookupPath+"]"); } } //如果获取到的bean是一个String类型的,则在BeanFactory中查找该bean, //并将其封装为一个HandlerMethod对象 return(handlerMethod!=null?handlerMethod.createWithResolvedBean():null); }finally{ //释放当前注册的Mapping的读锁 this.mappingRegistry.releaseReadLock(); } }
上述方法中,其首先会获取当前request的uri,然后通过uri查找HandlerMethod,并且在最后,会判断获取到的HandlerMethod中的bean是否为String类型的,如果是,则在当前BeanFactory中查找该名称的bean,并且将其封装为HandlerMethod对象。这里我们直接阅读lookupHandlerMethod()方法:
@Nullable protectedHandlerMethodlookupHandlerMethod(StringlookupPath, HttpServletRequestrequest)throwsException{ Listmatches=newArrayList<>(); //通过uri直接在注册的RequestMapping中获取对应的RequestMappingInfo列表,需要注意的是, //这里进行查找的方式只是通过url进行查找,但是具体哪些RequestMappingInfo是匹配的,还需要进一步过滤 List directPathMatches=this.mappingRegistry.getMappingsByUrl(lookupPath); if(directPathMatches!=null){ //对获取到的RequestMappingInfo进行进一步过滤,并且将过滤结果封装为一个Match列表 addMatchingMappings(directPathMatches,matches,request); } if(matches.isEmpty()){ //如果无法通过uri进行直接匹配,则对所有的注册的RequestMapping进行匹配,这里无法通过uri //匹配的情况主要有三种: //①在RequestMapping中定义的是PathVariable,如/user/detail/{id}; //②在RequestMapping中定义了问号表达式,如/user/?etail; //③在RequestMapping中定义了*或**匹配,如/user/detail/** addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches,request); } if(!matches.isEmpty()){ //对匹配的结果进行排序,获取相似度最高的一个作为结果返回,这里对相似度的判断时, //会判断前两个是否相似度是一样的,如果是一样的,则直接抛出异常,如果不相同, //则直接返回最高的一个 Comparator comparator=newMatchComparator(getMappingComparator(request)); matches.sort(comparator); if(logger.isTraceEnabled()){ logger.trace("Found"+matches.size() +"matchingmapping(s)for["+lookupPath+"]:"+matches); } //获取匹配程度最高的一个匹配结果 MatchbestMatch=matches.get(0); if(matches.size()>1){ //如果匹配结果不止一个,首先会判断是否是跨域请求,如果是, //则返回PREFLIGHT_AMBIGUOUS_MATCH,如果不是,则会判断前两个匹配程度是否相同, //如果相同则抛出异常 if(CorsUtils.isPreFlightRequest(request)){ returnPREFLIGHT_AMBIGUOUS_MATCH; } MatchsecondBestMatch=matches.get(1); if(comparator.compare(bestMatch,secondBestMatch)==0){ Methodm1=bestMatch.handlerMethod.getMethod(); Methodm2=secondBestMatch.handlerMethod.getMethod(); thrownewIllegalStateException("Ambiguoushandlermethodsmappedfor" +"HTTPpath'"+request.getRequestURL()+"':{"+m1 +","+m2+"}"); } } //这里主要是对匹配结果的一个处理,主要包含对传入参数和返回的MediaType的处理 handleMatch(bestMatch.mapping,lookupPath,request); returnbestMatch.handlerMethod; }else{ //如果匹配结果是空的,则对所有注册的Mapping进行遍历,判断当前request具体是哪种情况导致 //的无法匹配:①RequestMethod无法匹配;②Consumes无法匹配;③Produces无法匹配; //④Params无法匹配 returnhandleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath,request); } }
这里对于结果的匹配,首先会通过uri进行直接匹配,如果能匹配到,则在匹配结果中尝试进行RequestMethod,Consumes和Produces等配置的匹配;如果通过uri不能匹配到,则直接对所有定义的RequestMapping进行匹配,这里主要是进行正则匹配,如果能匹配到。如果能够匹配到,则对匹配结果按照相似度进行排序,并且对前两个结果相似度进行比较,如果相似度一样,则抛出异常,如果不一样,则返回相似度最高的一个匹配结果。如果无法获取到匹配结果,则对所有的匹配结果进行遍历,判断当前request具体是哪一部分参数无法匹配到结果。对于匹配结果的获取,主要在addMatchingMappings()方法中,这里我们继续阅读该方法的源码:
privatevoidaddMatchingMappings(Collectionmappings,List matches, HttpServletRequestrequest){ for(Tmapping:mappings){ Tmatch=getMatchingMapping(mapping,request); if(match!=null){ matches.add(newMatch(match, this.mappingRegistry.getMappings().get(mapping))); } } }
对于RequestMapping的匹配,这里逻辑比较简单,就是对所有的RequestMappingInfo进行遍历,然后将request分别于每个RequestMappingInfo进行匹配,如果匹配上了,其返回值就不为空,最后将所有的匹配结果返回。如下是getMatchingMapping()方法的源码(其最终调用的是RequestMappingInfo.getMatchingCondition()方法):
@Override @Nullable publicRequestMappingInfogetMatchingCondition(HttpServletRequestrequest){ //判断request请求的类型是否与当前RequestMethod匹配 RequestMethodsRequestConditionmethods= this.methodsCondition.getMatchingCondition(request); //判断request请求的参数是否与RequestMapping中params参数配置的一致 ParamsRequestConditionparams=this.paramsCondition.getMatchingCondition(request); //判断request请求的headers是否与RequestMapping中headers参数配置的一致 HeadersRequestConditionheaders=this.headersCondition.getMatchingCondition(request); //判断request的请求体类型是否与RequestMapping中配置的consumes参数配置的一致 ConsumesRequestConditionconsumes= this.consumesCondition.getMatchingCondition(request); //判断当前RequestMapping将要返回的请求体类型是否与request中Accept的header指定的一致 ProducesRequestConditionproduces= this.producesCondition.getMatchingCondition(request); //对于上述几个判断,如果匹配上了,那么其返回值都不会为空,因而这里会对每个返回值都进行判断, //如果有任意一个为空,则说明没匹配上,那么就返回null if(methods==null||params==null||headers==null ||consumes==null||produces==null){ returnnull; } //对于前面的匹配,都是一些静态属性的匹配,其中最重要的uri的匹配,主要是正则匹配, //就是在下面这个方法中进行的 PatternsRequestConditionpatterns= this.patternsCondition.getMatchingCondition(request); //如果URI没匹配上,则返回null if(patterns==null){ returnnull; } //这里主要是对用户自定义的匹配条件进行匹配 RequestConditionHoldercustom= this.customConditionHolder.getMatchingCondition(request); if(custom==null){ returnnull; } //如果上述所有条件都匹配上了,那么就将匹配结果封装为一个RequestMappingInfo返回 returnnewRequestMappingInfo(this.name,patterns,methods,params,headers, consumes,produces,custom.getCondition()); }
可以看到,对于一个RequestMapping的匹配,主要包括:RequestMethod,Params,Headers,Consumes,Produces,Uri和自定义条件的匹配,如果这几个条件都匹配上了,才能表明当前RequestMapping与request匹配上了。
3.Interceptor的封装
关于Inteceptor的封装,由前述第一点可以看出,其主要在getHandlerExecutionChain()方法中,如下是该方法的源码:
protectedHandlerExecutionChaingetHandlerExecutionChain(Objecthandler, HttpServletRequestrequest){ //将当前handler封装到HandlerExecutionChain对象中 HandlerExecutionChainchain=(handlerinstanceofHandlerExecutionChain? (HandlerExecutionChain)handler:newHandlerExecutionChain(handler)); //获取当前request的URI,用于MappedInterceptor的匹配 StringlookupPath=this.urlPathHelper.getLookupPathForRequest(request); //对当前所有注册的Interceptor进行遍历,如果其是MappedInterceptor类型,则调用其matches() //方法,判断当前Interceptor是否能够应用于该request,如果可以,则添加到HandlerExecutionChain中 for(HandlerInterceptorinterceptor:this.adaptedInterceptors){ if(interceptorinstanceofMappedInterceptor){ MappedInterceptormappedInterceptor=(MappedInterceptor)interceptor; if(mappedInterceptor.matches(lookupPath,this.pathMatcher)){ chain.addInterceptor(mappedInterceptor.getInterceptor()); } }else{ //如果当前Interceptor不是MappedInterceptor类型,则直接将其添加到 //HandlerExecutionChain中 chain.addInterceptor(interceptor); } } returnchain; }
对于拦截器,理论上,Spring是会将所有的拦截器都进行一次调用,对于是否需要进行拦截,都是用户自定义实现的。这里如果对于URI有特殊的匹配,可以使用MappedInterceptor,然后实现其matches()方法,用于判断当前MappedInterceptor是否能够应用于当前request。
4.小结
本文首先讲解了Spring是如何通过request进行匹配,从而找到具体处理当前请求的RequestMapping的,然后讲解了Spring是如何封装Interceptor,将HandlerMethod和Interceptor封装为一个HandlerExecutionChain的。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。