百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Spring MVC之RequestMappingHandlerMapping匹配

wxin55 2024-10-27 15:57 11 浏览 0 评论

对于RequestMappingHandlerMapping,使用Spring的同学基本都不会陌生,该类的作用有两个:

  • 通过request查找对应的HandlerMethod,即当前request具体是由Controller中的哪个方法进行处理;
  • 查找当前系统中的Interceptor,将其与HandlerMethod封装为一个HandlerExecutionChain。

本文主要讲解RequestMappingHandlerMapping是如何获取HandlerMethod和Interceptor,并且将其封装为HandlerExecutionChain的。

1.整体封装结构

RequestMappingHandlerMapping实现了HandlerMapping接口,该接口的主要方法如下:

public interface HandlerMapping {
 // 通过request获取HandlerExecutionChain对象
	HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

这里我们直接看RequestMappingHandlerMapping是如何实现该接口的:

@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) 
 throws Exception {
 // 通过request获取具体的处理bean,这里handler可能有两种类型:HandlerMethod和String。
 // 如果是String类型,那么就在BeanFactory中查找该String类型的bean,需要注意的是,返回的
 // bean如果是需要使用RequestMappingHandlerAdapter处理,那么也必须是HandlerMethod类型的
 Object handler = getHandlerInternal(request);
 if (handler == null) {
 // 如果找不到处理方法,则获取自定义的默认handler
 handler = getDefaultHandler();
 }
 if (handler == null) {
 return null;
 }
 if (handler instanceof String) {
 // 如果获取的handler是String类型的,则在当前BeanFactory中获取该名称的bean,
 // 并将其作为handler返回
 String handlerName = (String) handler;
 handler = obtainApplicationContext().getBean(handlerName);
 }
 // 获取当前系统中配置的Interceptor,将其与handler一起封装为一个HandlerExecutionChain
 HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
 // 这里CorsUtils.isCorsRequest()方法判断的是当前请求是否为一个跨域的请求,如果是一个跨域的请求,
 // 则将跨域相关的配置也一并封装到HandlerExecutionChain中
 if (CorsUtils.isCorsRequest(request)) {
 CorsConfiguration globalConfig = 
 this.globalCorsConfigSource.getCorsConfiguration(request);
 CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
 CorsConfiguration config = (globalConfig != null ? 
 globalConfig.combine(handlerConfig) : handlerConfig);
 executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
 }
 return executionChain;
}

从上面的代码可以看出,对于HandlerExecutionChain的获取,RequestMappingHandlerMapping首先会获取当前request对应的handler,然后将其与Interceptor一起封装为一个HandlerExecutionChain对象。这里在进行封装的时候,Spring会对当前request是否为跨域请求进行判断,如果是跨域请求,则将相关的跨域配置封装到HandlerExecutionChain中,关于跨域请求,读者可以阅读跨域资源共享 CORS 详解。

2. 获取HandlerMethod

关于RequestMappingHandlerMapping是如何获取handler的,其主要在getHandlerInternal()方法中,如下是该方法的源码:

@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
 // 获取当前request的URI
 String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
 if (logger.isDebugEnabled()) {
 logger.debug("Looking up handler method for path " + lookupPath);
 }
 // 获取注册的Mapping的读锁
 this.mappingRegistry.acquireReadLock();
 try {
 // 通过path和request查找具体的HandlerMethod
 HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
 if (logger.isDebugEnabled()) {
 if (handlerMethod != null) {
 logger.debug("Returning handler method [" + handlerMethod + "]");
 } else {
 logger.debug("Did not find handler method for [" + 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
protected HandlerMethod lookupHandlerMethod(String lookupPath, 
 HttpServletRequest request) throws Exception {
 List<Match> matches = new ArrayList<>();
 // 通过uri直接在注册的RequestMapping中获取对应的RequestMappingInfo列表,需要注意的是,
 // 这里进行查找的方式只是通过url进行查找,但是具体哪些RequestMappingInfo是匹配的,还需要进一步过滤
 List<T> 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<Match> comparator = new MatchComparator(getMappingComparator(request));
 matches.sort(comparator);
 if (logger.isTraceEnabled()) {
 logger.trace("Found " + matches.size() 
 + " matching mapping(s) for [" + lookupPath + "] : " + matches);
 }
 // 获取匹配程度最高的一个匹配结果
 Match bestMatch = matches.get(0);
 if (matches.size() > 1) {
 // 如果匹配结果不止一个,首先会判断是否是跨域请求,如果是,
 // 则返回PREFLIGHT_AMBIGUOUS_MATCH,如果不是,则会判断前两个匹配程度是否相同,
 // 如果相同则抛出异常
 if (CorsUtils.isPreFlightRequest(request)) {
 return PREFLIGHT_AMBIGUOUS_MATCH;
 }
 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 + "}");
 }
 }
 // 这里主要是对匹配结果的一个处理,主要包含对传入参数和返回的MediaType的处理
 handleMatch(bestMatch.mapping, lookupPath, request);
 return bestMatch.handlerMethod;
 } else {
 // 如果匹配结果是空的,则对所有注册的Mapping进行遍历,判断当前request具体是哪种情况导致
 // 的无法匹配:①RequestMethod无法匹配;②Consumes无法匹配;③Produces无法匹配;
 // ④Params无法匹配
 return handleNoMatch(this.mappingRegistry.getMappings().keySet(), 
 lookupPath, request);
 }
}

这里对于结果的匹配,首先会通过uri进行直接匹配,如果能匹配到,则在匹配结果中尝试进行RequestMethod,Consumes和Produces等配置的匹配;如果通过uri不能匹配到,则直接对所有定义的RequestMapping进行匹配,这里主要是进行正则匹配,如果能匹配到。如果能够匹配到,则对匹配结果按照相似度进行排序,并且对前两个结果相似度进行比较,如果相似度一样,则抛出异常,如果不一样,则返回相似度最高的一个匹配结果。如果无法获取到匹配结果,则对所有的匹配结果进行遍历,判断当前request具体是哪一部分参数无法匹配到结果。对于匹配结果的获取,主要在addMatchingMappings()方法中,这里我们继续阅读该方法的源码:

private void addMatchingMappings(Collection<T> mappings, List<Match> matches, 
 HttpServletRequest request) {
 for (T mapping : mappings) {
 T match = getMatchingMapping(mapping, request);
 if (match != null) {
 matches.add(new Match(match, 
 this.mappingRegistry.getMappings().get(mapping)));
 }
 }
}

对于RequestMapping的匹配,这里逻辑比较简单,就是对所有的RequestMappingInfo进行遍历,然后将request分别于每个RequestMappingInfo进行匹配,如果匹配上了,其返回值就不为空,最后将所有的匹配结果返回。如下是getMatchingMapping()方法的源码(其最终调用的是RequestMappingInfo.getMatchingCondition()方法):

@Override
@Nullable
public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
 // 判断request请求的类型是否与当前RequestMethod匹配
 RequestMethodsRequestCondition methods = 
 this.methodsCondition.getMatchingCondition(request);
 // 判断request请求的参数是否与RequestMapping中params参数配置的一致
 ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
 // 判断request请求的headers是否与RequestMapping中headers参数配置的一致
 HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
 // 判断request的请求体类型是否与RequestMapping中配置的consumes参数配置的一致
 ConsumesRequestCondition consumes = 
 this.consumesCondition.getMatchingCondition(request);
 // 判断当前RequestMapping将要返回的请求体类型是否与request中Accept的header指定的一致
 ProducesRequestCondition produces = 
 this.producesCondition.getMatchingCondition(request);
 // 对于上述几个判断,如果匹配上了,那么其返回值都不会为空,因而这里会对每个返回值都进行判断,
 // 如果有任意一个为空,则说明没匹配上,那么就返回null
 if (methods == null || params == null || headers == null 
 || consumes == null || produces == null) {
 return null;
 }
 // 对于前面的匹配,都是一些静态属性的匹配,其中最重要的uri的匹配,主要是正则匹配,
 // 就是在下面这个方法中进行的
 PatternsRequestCondition patterns = 
 this.patternsCondition.getMatchingCondition(request);
 // 如果URI没匹配上,则返回null
 if (patterns == null) {
 return null;
 }
 // 这里主要是对用户自定义的匹配条件进行匹配
 RequestConditionHolder custom = 
 this.customConditionHolder.getMatchingCondition(request);
 if (custom == null) {
 return null;
 }
 // 如果上述所有条件都匹配上了,那么就将匹配结果封装为一个RequestMappingInfo返回
 return new RequestMappingInfo(this.name, patterns, methods, params, headers, 
 consumes, produces, custom.getCondition());
}

可以看到,对于一个RequestMapping的匹配,主要包括:RequestMethod,Params,Headers,Consumes,Produces,Uri和自定义条件的匹配,如果这几个条件都匹配上了,才能表明当前RequestMapping与request匹配上了。

3. Interceptor的封装

关于Inteceptor的封装,由前述第一点可以看出,其主要在getHandlerExecutionChain()方法中,如下是该方法的源码:

protected HandlerExecutionChain getHandlerExecutionChain(Object handler, 
 HttpServletRequest request) {
 // 将当前handler封装到HandlerExecutionChain对象中
 HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
 (HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
 // 获取当前request的URI,用于MappedInterceptor的匹配
 String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
 // 对当前所有注册的Interceptor进行遍历,如果其是MappedInterceptor类型,则调用其matches()
 // 方法,判断当前Interceptor是否能够应用于该request,如果可以,则添加到HandlerExecutionChain中
 for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
 if (interceptor instanceof MappedInterceptor) {
 MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
 if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
 chain.addInterceptor(mappedInterceptor.getInterceptor());
 }
 } else {
 // 如果当前Interceptor不是MappedInterceptor类型,则直接将其添加到
 // HandlerExecutionChain中
 chain.addInterceptor(interceptor);
 }
 }
 return chain;
}

对于拦截器,理论上,Spring是会将所有的拦截器都进行一次调用,对于是否需要进行拦截,都是用户自定义实现的。这里如果对于URI有特殊的匹配,可以使用MappedInterceptor,然后实现其matches()方法,用于判断当前MappedInterceptor是否能够应用于当前request。

4. 小结

本文首先讲解了Spring是如何通过request进行匹配,从而找到具体处理当前请求的RequestMapping的,然后讲解了Spring是如何封装Interceptor,将HandlerMethod和Interceptor封装为一个HandlerExecutionChain的。

相关推荐

黑客工具sqlmap,带你了解什么师sql注入

1、sqlmap介绍sqlmap是一款支持MySQL,Oracle,PostgreSQL,MicrosoftSQLServer,MicrosoftAccess,IBMDB2,SQL...

Web网络安全漏洞分析,SQL注入原理详解

本文主要为大家介绍了Web网络安全漏洞分析SQL注入的原理详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪一、SQL注入的基础1.1介绍SQL注入SQL注入就是指We...

sql注入、文件上传、文件包含(sql注入数据提交的类型)

在owasp年度top10安全问题中,注入高居榜首。SQL注入攻击指的是通过构建特殊的输入作为参数传入Web应用程序,而这些输入大都是SQL语法里的一些组合,通过执行SQL语句进而执行攻击者所要...

比较常见类型漏洞讲解(三):SQL注入(一)

比较常见类型漏洞讲解(一)比较常见类型漏洞讲解(二):文件上传这里继续介绍比较容易找到的不同类型的漏洞。演示准备目标主机:Metasploitable2攻击目标:目标主机的dvwa系统攻击机:Kali...

警惕SQL注入:检测与防御的实战指南

在当今数字化的时代,软件系统的安全性至关重要。SQL注入作为一种常见且危害巨大的安全漏洞,给企业和用户带来了潜在的风险。对于测试人员来说,掌握发现和解决SQL注入漏洞的方法是保障软件质量和安全的...

SQL注入的原理和危害(sql注入的原理及危害)

什么是SQL注入SQL注入是发生在web端的安全漏洞,实现非法操作,欺骗服务器执行非法查询,他的危害有会恶意获取,甚至篡改数据库信息,绕过登录验证,原理是针对程序员编写时的疏忽,通过执行SQL语句,实...

科普基础 | 最全的SQL注入总结(sql注入是干嘛的)

0x01SQL注入原理当客户端提交的数据未作处理或转义直接带入数据库,就造成了sql注入。攻击者通过构造不同的sql语句来实现对数据库的任意操作。0x02SQL注入的分类按变量类型分:数字型和字符...

产品经理必备IT技术知识之“什么是SQL注入攻击?

不少优秀的产品经理都会写一些简单的SQL语句进行数据查询的操作,但是会写SQL语句不代表能写好SQL语句。SQL语句写得不好,就会引发SQL注入攻击。SQL注入攻击是Web开发中最常见的一种安全问题,...

通过sql注入获取用户名和密码(联通光猫超级用户名超级密码获取)

1.啥是sql注入sql注入是一种通过操作输入来修改后台sql语句以达到利用代码进行攻击目的的技术2.生成sql注入漏洞的原因总结一句话:对前台传过来的参数没有进行有效的过滤,太相信前端传过来的参数...

什么是SQL注入攻击(SQL Injection)

1,定义:在构建数据库SQL语句时,攻击者在参数请求中注入恶意的SQL代码,并在在数据库中执行,操控数据库执行意图之外的操作。2,目的:窃取数据、修改数据、删除数据、绕过身份验证、权限提升,执...

浅谈SQL注入(sql注入的理解)

在关于SQL注入,百度的解释是这样的:SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知...

sql注入(sql注入攻击是什么意思)

SQL注入分类1.数字型注入当输入的参数为整型时,则有可能存在数字型注入漏洞。假设存在一条URL为:HTTP://www.aaa.com/test.php?id=1可以对后台的SQL语句猜...

SQL注入详解(sql注入总结)

现在大多数系统都使用B/S架构,出于安全考虑需要过滤从页面传递过来的字符。通常,用户可以通过以下接口调用数据库的内容:URL地址栏、登陆界面、留言板、搜索框等。这往往会出现安全隐患,为了更好的保护数据...

什么是sql注入,这些坑得避开(什么是sql注入,编程时如何避免)

1、sql注入是什么sql注入就是用户通过输入的参数,拼接到原先的sql中,成为sql的一部分,从而影响sql的功能和执行结果2、sql注入破坏力-小兵破坏力比如原先sql如下s...

金母鸡量化教学场:pandas—数据挖掘的Python库

如果你想充分发挥Python的强大作用,如果你想成为一名好的Python量化师,你应该先学好Pandas。今天就来了解什么是Pandas。与numpy易于处理同种类型数据不同,pandas更加的适合...

取消回复欢迎 发表评论: