当前位置:   article > 正文

SpringMVC处理器映射器HandlerMapping详解

SpringMVC处理器映射器HandlerMapping详解

目录

一、前言        

二、initHandlerMappings

三、处理器映射器架构

策略接口

请求链

模版类

四、RequestMappingHandlerMapping的初始化

HandlerMethod映射器模版类的初始化

AbstractHandlerMethodMapping.MappingRegistry:内部类注册中心

五、RequestMappingHandlerMapping映射器模版类的调用

RequestMappingHandlerMapping调用


一、前言        

昨天在进行spring的安全升级时,遇到一个奇葩问题:原始版本为4.1.9,升级至5.3.34,由于是几年前的项目,请求路径还是带有.action的老式写法(如login.action),而有的不带(如:index),升级完后,就发现了问题,login.action能访问到,请求index时,日志爆了 no mapping for GET。

        我们来看看是怎么回事。

        在SpringMVC中会有很多请求,每个请求都需要一个HandlerAdapter处理,具体接收到一个请求之后使用哪个HandlerAdapter进行处理呢,他们的过程是什么。本文将对此问题进行讨论

二、initHandlerMappings

DispatcherServlet在初始化中,会调用其initHandlerMappings方法注册HandlerMapping对象并放到其缓存池中,其过程如下:先查询容器中是否有处理器映射器,如果有就注册到其缓存池中,如果没有就安装默认到规则创建处理器映射器,并注册到其缓存池中。

  1. private void initHandlerMappings(ApplicationContext context) {
  2. this.handlerMappings = null;
  3. //detectAllHandlerMappings默认为true
  4. //true标志检测所有handlerMapping,false只获取“handlerMapping”bean。
  5. if (this.detectAllHandlerMappings) {
  6. // 在ApplicationContext中查找所有HandlerMappings,包括祖先上下文。
  7. Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
  8. if (!matchingBeans.isEmpty()) {
  9. this.handlerMappings = new ArrayList<>(matchingBeans.values());
  10. //排序
  11. AnnotationAwareOrderComparator.sort(this.handlerMappings);
  12. }
  13. }
  14. else {
  15. try {
  16. //只获取“handlerMapping”bean
  17. HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
  18. this.handlerMappings = Collections.singletonList(hm);
  19. }
  20. catch (NoSuchBeanDefinitionException ex) {
  21. // Ignore, we'll add a default HandlerMapping later.
  22. }
  23. }
  24. //通过注册,确保我们至少有一个HandlerMapping
  25. //如果找不到其他映射,则为默认HandlerMapping。
  26. if (this.handlerMappings == null) {
  27. //从spring-webmvc下的DispatcherServlet.properties读取默认配置
  28. this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
  29. if (logger.isTraceEnabled()) {
  30. logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
  31. "': using default strategies from DispatcherServlet.properties");
  32. }
  33. }
  34. }

Spring默认HandlerMapping有BeanNameUrlHandlerMapping、RequestMappingHandlerMapping、RouterFunctionMapping

三、处理器映射器架构

策略接口

处理器映射器使用了策略模式

HandlerMapping是用来查找Handler的。在SpringMVC中会有很多请求,每个请求都需要一个Handler处理,具体接收到一个请求之后使用哪个Handler进行处理呢?这就是HandlerMapping需要做的事

HandlerMapping:负责映射用户的URL和对应的处理类Handler,HandlerMapping并没有规定这个URL与应用的处理类如何映射。所以在HandlerMapping接口中仅仅定义了根据一个URL必须返回一个由HandlerExecutionChain代表的处理链,我们可以在这个处理链中添加任意的HandlerAdapter实例来处理这个URL对应的请求(这样保证了最大的灵活性映射关系)。

请求链

模版类

处理器映射器都是实现AbstractHandlerMapping,该抽象类完成了所有的Handler以及handler里面所有的HandlerMethod的模版操作,但是怎么获取Handler,这些逻辑都是交给子类自己去实现,所以这层抽象可谓也是非常的灵活,并没有把Handler的实现方式定死。 

  1. public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
  2. implements HandlerMapping, Ordered, BeanNameAware {
  3. //默认的Handler,这边使用的Obejct,子类实现的时候,使用HandlerMethod,HandlerExecutionChain等
  4. @Nullable
  5. private Object defaultHandler;
  6. // url路径计算的辅助类、工具类
  7. private UrlPathHelper urlPathHelper = new UrlPathHelper();
  8. // Ant风格的Path匹配模式~ 解决如/books/{id}场景
  9. private PathMatcher pathMatcher = new AntPathMatcher();
  10. // 保存着拦截器们~~~
  11. private final List<Object> interceptors = new ArrayList<>();
  12. // 从interceptors中解析得到,直接添加给全部handler
  13. private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<>();
  14. // 跨域相关的配置~
  15. private CorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
  16. private CorsProcessor corsProcessor = new DefaultCorsProcessor();
  17. // 最低的顺序(default: same as non-Ordered)
  18. private int order = Ordered.LOWEST_PRECEDENCE;
  19. @Nullable
  20. private String beanName;
  21. /**
  22. * Initializes the interceptors.
  23. * @see #extendInterceptors(java.util.List)
  24. * @see #initInterceptors()
  25. */
  26. @Override
  27. protected void initApplicationContext() throws BeansException {
  28. // 给子类扩展:增加拦截器,默认为空实现.RequestMappingHandlerMapping也没有重写这个方法
  29. extendInterceptors(this.interceptors);
  30. // 找到所有MappedInterceptor(截器是)类型的bean添加到adaptedInterceptors中
  31. detectMappedInterceptors(this.adaptedInterceptors);
  32. // 将interceptors中的拦截器取出放入adaptedInterceptors
  33. // 如果是WebRequestInterceptor类型的拦截器 需要用WebRequestHandlerInterceptorAdapter进行包装适配
  34. initInterceptors();
  35. }
  36. @Override
  37. @Nullable
  38. public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
  39. //根据请求获取对应的处理器,子类实现
  40. Object handler = getHandlerInternal(request);
  41. if (handler == null) {
  42. //如果获取不到,到默认到处理器中
  43. handler = getDefaultHandler();
  44. }
  45. //如果还没有处理器,返回null
  46. if (handler == null) {
  47. return null;
  48. }
  49. // 意思是如果当前传入的handler是个String类型,那就根据其名字去容器内找这个Bean,当作一个Handler~
  50. if (handler instanceof String) {
  51. String handlerName = (String) handler;
  52. //到容器中找
  53. handler = obtainApplicationContext().getBean(handlerName);
  54. }
  55. //根据handler和request构造一个请求处理链~~
  56. HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
  57. // 4.2版本提供了对CORS跨域资源共享的支持 此处暂时略过~
  58. if (hasCorsConfigurationSource(handler)) {
  59. CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null);
  60. CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
  61. config = (config != null ? config.combine(handlerConfig) : handlerConfig);
  62. executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
  63. }
  64. return executionChain;
  65. }
  66. @Nullable
  67. protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception;
  68. }

getHandlerInternal模板方法的实现类: 

接下来最重要的就是以getHandlerInternal()方法为主线,看看其子类们的实现。它主要分为两大主线: AbstractUrlHandlerMapping 和 AbstractHandlerMethodMapping。

本文是以AbstractHandlerMethodMapping的子类RequestMappingHandlerMapping为主线

四、RequestMappingHandlerMapping的初始化

HandlerMethod映射器都是处理器映射器类型的映射器。这种类型的映射器有一个模版类AbstractHandlerMethodMapping, 所有的HandlerMethod映射器都是他的实现。

AbstractHandlerMethodMapping包括其初始化和调用过程。为了好讲解,在这里就将其初始化和调用过程代码分开说

HandlerMethod映射器模版类的初始化

  1. public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
  2. private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH =
  3. new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class, "handle"));
  4. private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration();
  5. static {
  6. ALLOW_CORS_CONFIG.addAllowedOrigin("*");
  7. ALLOW_CORS_CONFIG.addAllowedMethod("*");
  8. ALLOW_CORS_CONFIG.addAllowedHeader("*");
  9. ALLOW_CORS_CONFIG.setAllowCredentials(true);
  10. }
  11. private boolean detectHandlerMethodsInAncestorContexts = false;
  12. @Nullable
  13. private HandlerMethodMappingNamingStrategy<T> namingStrategy;
  14. //注册表,HandlerMapping在容器启动过程中初始化,把扫描到的handler放到注册表中
  15. private final MappingRegistry mappingRegistry = new MappingRegistry();
  16. @Override
  17. public void afterPropertiesSet() {
  18. initHandlerMethods();
  19. }
  20. protected void initHandlerMethods() {
  21. //循环所有的bean
  22. for (String beanName : getCandidateBeanNames()) {
  23. //如果bean名字不是以scopedTarget.开头
  24. if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
  25. processCandidateBean(beanName);
  26. }
  27. }
  28. //日志输出
  29. handlerMethodsInitialized(getHandlerMethods());
  30. }
  31. protected void processCandidateBean(String beanName) {
  32. Class<?> beanType = null;
  33. try {
  34. beanType = obtainApplicationContext().getType(beanName);
  35. }
  36. catch (Throwable ex) {
  37. if (logger.isTraceEnabled()) {
  38. logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
  39. }
  40. }
  41. //因为这里我们是研究RequestMappingHandlerMapping,所以这局代码内容如下
  42. //如果beanType不为null,且类上标注@Controller注解或者@RequestMapping注解
  43. if (beanType != null && isHandler(beanType)) {
  44. detectHandlerMethods(beanName);
  45. }
  46. }
  47. protected void detectHandlerMethods(Object handler) {
  48. Class<?> handlerType = (handler instanceof String ?
  49. obtainApplicationContext().getType((String) handler) : handler.getClass());
  50. if (handlerType != null) {
  51. Class<?> userType = ClassUtils.getUserClass(handlerType);
  52. Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
  53. (MethodIntrospector.MetadataLookup<T>) method -> {
  54. try {
  55. return getMappingForMethod(method, userType);
  56. }
  57. catch (Throwable ex) {
  58. throw new IllegalStateException("Invalid mapping on handler class [" +
  59. userType.getName() + "]: " + method, ex);
  60. }
  61. });
  62. if (logger.isTraceEnabled()) {
  63. logger.trace(formatMappings(userType, methods));
  64. }
  65. methods.forEach((method, mapping) -> {
  66. Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
  67. //底层使用了MappingRegistry的register方法
  68. registerHandlerMethod(handler, invocableMethod, mapping);
  69. });
  70. }
  71. }
  72. //忽略处理器映射器查询Handler部分代码.....
  73. }

1、循环所有的bean,如果bean名字不是以scopedTarget.开头,那么就判断他们是否是Handler(类上标注@Controller注解或者@RequestMapping注解)

2、如果是Handler,获取这个类上所有标注@RequestMapping的方法信息,以RequestMappingInfo形式

3、把他们储存到MappingRegistry中

AbstractHandlerMethodMapping.MappingRegistry:内部类注册中心

维护几个Map(键值对),用来存储映射的信息, 还有一个MappingRegistration专门保存注册信息 这个注册中心,核心是保存了多个Map映射关系,相当于缓存下来。在请求过来时需要查找的时候,可以迅速定位到处理器

  1. class MappingRegistry {
  2. //对于RequestMappingHandlerMapping来说
  3. //保存着RequestMappingInfo和MappingRegistration的对应关系~
  4. private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
  5. // 对于保存着mapping和HandlerMethod的对应关系~
  6. //对于RequestMappingHandlerMapping来说
  7. //保存着RequestMappingInfo和HandlerMethod的对应关系~
  8. private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
  9. // 这里的Map不是普通的Map,而是MultiValueMap,它是个多值Map。其实它的value是一个list类型的值
  10. // 至于为何是多值?有这么一种情况 URL都是/api/v1/hello 但是有的是get post delete等方法 所以有可能是会匹配到多个MappingInfo的
  11. //对于RequestMappingHandlerMapping来说,保存着URL和RequestMappingInfo的关系~
  12. private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
  13. //对于RequestMappingHandlerMapping来说,保存着URL和HandlerMethod的关系~
  14. private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>()
  15. // 这两个就不用解释了
  16. private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
  17. // 读写锁~~~ 读写分离 提高启动效率
  18. private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  19. //5.1版本其子类只有一个RequestMappingHandlerMapping,T就是RequestMappingInfo
  20. //handler一般情况下是处理器方法从属bean的名字
  21. //method是处理器方法
  22. public void register(T mapping, Object handler, Method method) {
  23. if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
  24. throw new IllegalStateException("Unsupported suspending handler method detected: " + method);
  25. }
  26. this.readWriteLock.writeLock().lock();
  27. try {
  28. HandlerMethod handlerMethod = createHandlerMethod(handler, method);
  29. //断言提供的映射是唯一的。
  30. validateMethodMapping(handlerMethod, mapping);
  31. this.mappingLookup.put(mapping, handlerMethod);
  32. List<String> directUrls = getDirectUrls(mapping);
  33. for (String url : directUrls) {
  34. this.urlLookup.add(url, mapping);
  35. }
  36. String name = null;
  37. if (getNamingStrategy() != null) {
  38. name = getNamingStrategy().getName(handlerMethod, mapping);
  39. addMappingName(name, handlerMethod);
  40. }
  41. //初始化跨域配置
  42. //使用的是AbstractHandlerMethodMapping的initCorsConfiguration方法,子类实现
  43. CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
  44. if (corsConfig != null) {
  45. this.corsLookup.put(handlerMethod, corsConfig);
  46. }
  47. this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
  48. }
  49. finally {
  50. this.readWriteLock.writeLock().unlock();
  51. }
  52. }

这个注册中心,核心是保存了多个Map映射关系,相当于缓存下来。在请求过来时需要查找的时候,可以迅速定位到处理器

在其初始化过程中,其主要模版化的2个方法

  1. protected CorsConfiguration initCorsConfiguration(Object handler, Method method, T mapping) {
  2. return null;
  3. }
  4. protected abstract boolean isHandler(Class<?> beanType);

五、RequestMappingHandlerMapping映射器模版类的调用

  1. public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
  2. private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH =
  3. new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class, "handle"));
  4. private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration();
  5. static {
  6. ALLOW_CORS_CONFIG.addAllowedOrigin("*");
  7. ALLOW_CORS_CONFIG.addAllowedMethod("*");
  8. ALLOW_CORS_CONFIG.addAllowedHeader("*");
  9. ALLOW_CORS_CONFIG.setAllowCredentials(true);
  10. }
  11. private boolean detectHandlerMethodsInAncestorContexts = false;
  12. @Nullable
  13. private HandlerMethodMappingNamingStrategy<T> namingStrategy;
  14. //注册表,HandlerMapping在容器启动过程中初始化,把扫描到的handler放到注册表中
  15. private final MappingRegistry mappingRegistry = new MappingRegistry();
  16. //忽略初始化部分代码.....
  17. protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
  18. //获取请求路径
  19. String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
  20. //放到请求属性中
  21. request.setAttribute(LOOKUP_PATH, lookupPath);
  22. this.mappingRegistry.acquireReadLock();
  23. try {
  24. //根据请求和路径获取对应的处理方法,注册表中取
  25. HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
  26. return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
  27. }
  28. finally {
  29. this.mappingRegistry.releaseReadLock();
  30. }
  31. }
  32. protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
  33. // Match是一个private class,内部就两个属性:T mapping和HandlerMethod handlerMethod
  34. List<Match> matches = new ArrayList<>();
  35. // 根据lookupPath去注册中心里查找RequestMappingInfo,因为一个具体的url可能匹配上多个RequestMappingInfo
  36. // 至于为何是多值?有这么一种情况 URL都是/api/v1/hello 但是有的是get post delete等方法 等不一样,都算多个的 所以有可能是会匹配到多个MappingInfo的
  37. // 所有这个里可以匹配出多个出来。比如/hello 匹配出GET、POST、PUT都成,所以size可以为3
  38. List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
  39. if (directPathMatches != null) {
  40. // 依赖于子类实现的抽象方法:getMatchingMapping() 看看到底匹不匹配,而不仅仅是URL匹配就行
  41. // 比如还有method、headers、consumes等等这些不同都代表着不同的MappingInfo的
  42. // 最终匹配上的,会new Match()放进matches里面去
  43. addMatchingMappings(directPathMatches, matches, request);
  44. }
  45. // 当还没有匹配上的时候,别无选择,只能浏览所有映射
  46. // 这里为何要浏览所有的mappings呢?而不是报错404呢?
  47. // 增加路径匹配对范围,如:/rest 匹配 /rest.ssss
  48. if (matches.isEmpty()) {
  49. addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
  50. }
  51. // 只要找到了一个匹配的 就进来这里了~~~
  52. // 请注意:因为到这里 匹配上的可能还不止一个 所以才需要继续处理~~
  53. if (!matches.isEmpty()) {
  54. Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
  55. matches.sort(comparator);
  56. //如果匹配到多个,就取第一个
  57. Match bestMatch = matches.get(0);
  58. if (matches.size() > 1) {
  59. if (logger.isTraceEnabled()) {
  60. logger.trace(matches.size() + " matching mappings: " + matches);
  61. }
  62. if (CorsUtils.isPreFlightRequest(request)) {
  63. return PREFLIGHT_AMBIGUOUS_MATCH;
  64. }
  65. Match secondBestMatch = matches.get(1);
  66. if (comparator.compare(bestMatch, secondBestMatch) == 0) {
  67. Method m1 = bestMatch.handlerMethod.getMethod();
  68. Method m2 = secondBestMatch.handlerMethod.getMethod();
  69. String uri = request.getRequestURI();
  70. throw new IllegalStateException(" ");
  71. }
  72. }
  73. request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
  74. //请求域增加一些属性,子类重写
  75. handleMatch(bestMatch.mapping, lookupPath, request);
  76. return bestMatch.handlerMethod;
  77. }
  78. else {
  79. //请求域增加一些属性,子类重写
  80. return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
  81. }
  82. }
  83. }
  84. protected void handleMatch(T mapping, String lookupPath, HttpServletRequest request) {
  85. request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath);
  86. }
  87. @Nullable
  88. protected HandlerMethod handleNoMatch(Set<T> mappings, String lookupPath, HttpServletRequest request)
  89. throws Exception {
  90. return null;
  91. }

RequestMappingHandlerMapping调用

  1. public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {
  2. private static final Method HTTP_OPTIONS_HANDLE_METHOD;
  3. /**
  4. * Expose URI template variables, matrix variables, and producible media types in the request.
  5. * @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE
  6. * @see HandlerMapping#MATRIX_VARIABLES_ATTRIBUTE
  7. * @see HandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE
  8. */
  9. @Override
  10. protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) {
  11. super.handleMatch(info, lookupPath, request);
  12. String bestPattern;
  13. Map<String, String> uriVariables;
  14. Set<String> patterns = info.getPatternsCondition().getPatterns();
  15. if (patterns.isEmpty()) {
  16. bestPattern = lookupPath;
  17. uriVariables = Collections.emptyMap();
  18. }
  19. else {
  20. bestPattern = patterns.iterator().next();
  21. uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
  22. }
  23. request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern);
  24. if (isMatrixVariableContentAvailable()) {
  25. Map<String, MultiValueMap<String, String>> matrixVars = extractMatrixVariables(request, uriVariables);
  26. request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, matrixVars);
  27. }
  28. Map<String, String> decodedUriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables);
  29. request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables);
  30. if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
  31. Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
  32. request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
  33. }
  34. }
  35. private boolean isMatrixVariableContentAvailable() {
  36. return !getUrlPathHelper().shouldRemoveSemicolonContent();
  37. }
  38. private Map<String, MultiValueMap<String, String>> extractMatrixVariables(
  39. HttpServletRequest request, Map<String, String> uriVariables) {
  40. Map<String, MultiValueMap<String, String>> result = new LinkedHashMap<>();
  41. uriVariables.forEach((uriVarKey, uriVarValue) -> {
  42. int equalsIndex = uriVarValue.indexOf('=');
  43. if (equalsIndex == -1) {
  44. return;
  45. }
  46. int semicolonIndex = uriVarValue.indexOf(';');
  47. if (semicolonIndex != -1 && semicolonIndex != 0) {
  48. uriVariables.put(uriVarKey, uriVarValue.substring(0, semicolonIndex));
  49. }
  50. String matrixVariables;
  51. if (semicolonIndex == -1 || semicolonIndex == 0 || equalsIndex < semicolonIndex) {
  52. matrixVariables = uriVarValue;
  53. }
  54. else {
  55. matrixVariables = uriVarValue.substring(semicolonIndex + 1);
  56. }
  57. MultiValueMap<String, String> vars = WebUtils.parseMatrixVariables(matrixVariables);
  58. result.put(uriVarKey, getUrlPathHelper().decodeMatrixVariables(request, vars));
  59. });
  60. return result;
  61. }
  62. /**
  63. * Iterate all RequestMappingInfo's once again, look if any match by URL at
  64. * least and raise exceptions according to what doesn't match.
  65. * @throws HttpRequestMethodNotSupportedException if there are matches by URL
  66. * but not by HTTP method
  67. * @throws HttpMediaTypeNotAcceptableException if there are matches by URL
  68. * but not by consumable/producible media types
  69. */
  70. @Override
  71. protected HandlerMethod handleNoMatch(
  72. Set<RequestMappingInfo> infos, String lookupPath, HttpServletRequest request) throws ServletException {
  73. PartialMatchHelper helper = new PartialMatchHelper(infos, request);
  74. if (helper.isEmpty()) {
  75. return null;
  76. }
  77. if (helper.hasMethodsMismatch()) {
  78. Set<String> methods = helper.getAllowedMethods();
  79. if (HttpMethod.OPTIONS.matches(request.getMethod())) {
  80. HttpOptionsHandler handler = new HttpOptionsHandler(methods);
  81. return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
  82. }
  83. throw new HttpRequestMethodNotSupportedException(request.getMethod(), methods);
  84. }
  85. if (helper.hasConsumesMismatch()) {
  86. Set<MediaType> mediaTypes = helper.getConsumableMediaTypes();
  87. MediaType contentType = null;
  88. if (StringUtils.hasLength(request.getContentType())) {
  89. try {
  90. contentType = MediaType.parseMediaType(request.getContentType());
  91. }
  92. catch (InvalidMediaTypeException ex) {
  93. throw new HttpMediaTypeNotSupportedException(ex.getMessage());
  94. }
  95. }
  96. throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(mediaTypes));
  97. }
  98. if (helper.hasProducesMismatch()) {
  99. Set<MediaType> mediaTypes = helper.getProducibleMediaTypes();
  100. throw new HttpMediaTypeNotAcceptableException(new ArrayList<>(mediaTypes));
  101. }
  102. if (helper.hasParamsMismatch()) {
  103. List<String[]> conditions = helper.getParamConditions();
  104. throw new UnsatisfiedServletRequestParameterException(conditions, request.getParameterMap());
  105. }
  106. return null;
  107. }
  108. }

RequestMappingHandlerMapping根据请求获取对应的handlerMethod过程是:

1、获取请求路径

2、根据路径到注册表中查询对应路径的RequestMappingInfo

3、如果匹配到多个,就取第一个。

4、如果匹配不到,就到注册表中查询所有RequestMappingInfo,匹配规则我们可以自定义。

Spring MVC请求URL带后缀匹配的情况,如/hello.json也能匹配/hello

RequestMappingInfoHandlerMapping 在处理http请求的时候, 如果 请求url 有后缀,如果找不到精确匹配的那个@RequestMapping方法。 那么,就把后缀去掉,然后.*去匹配,这样,一般都可以匹配,默认这个行为是被开启的。4.3以后是关闭的

比如有一个@RequestMapping("/rest"), 那么精确匹配的情况下, 只会匹配/rest请求。 但如果我前端发来一个 /rest.abcdef 这样的请求, 又没有配置 @RequestMapping("/rest.abcdef") 这样映射的情况下, 那么@RequestMapping("/rest") 就会生效。

这样会带来什么问题呢?绝大多数情况下是没有问题的,但是如果你是一个对权限要求非常严格的系统,强烈关闭此项功能,否则你会有意想不到的"收获"。本文的一开始夜抛出了这个问题

究其原因咱们可以接着上面的分析,其实就到了PatternsRequestCondition这个类上,具体实现是它的匹配逻辑来决定的。

  1. public final class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> {
  2. ...
  3. @Override
  4. @Nullable
  5. public PatternsRequestCondition getMatchingCondition(HttpServletRequest request) {
  6. // patterns表示此MappingInfo可以匹配的值们。一般对应@RequestMapping注解上的patters数组的值
  7. if (this.patterns.isEmpty()) {
  8. return this;
  9. }
  10. // 拿到待匹配的值,比如此处为"/hello.json"
  11. String lookupPath = this.pathHelper.getLookupPathForRequest(request);
  12. // 最主要就是这个方法了,它拿着这个lookupPath匹配~~~~
  13. List<String> matches = getMatchingPatterns(lookupPath);
  14. // 此处如果为empty,就返回null了~~~~
  15. return (!matches.isEmpty() ? new PatternsRequestCondition(matches, this.pathHelper, this.pathMatcher, this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions) : null);
  16. }
  17. public List<String> getMatchingPatterns(String lookupPath) {
  18. List<String> matches = new ArrayList<>();
  19. for (String pattern : this.patterns) {
  20. // 最最最重点就是在getMatchingPattern()这个方法里~~~ 拿着lookupPath和pattern看它俩合拍不~
  21. String match = getMatchingPattern(pattern, lookupPath);
  22. if (match != null) {
  23. matches.add(match);
  24. }
  25. }
  26. // 解释一下为何匹配的可能是多个。因为url匹配上了,但是还有可能@RequestMapping的其余属性匹配不上啊,所以此处需要注意 是可能匹配上多个的 最终是唯一匹配就成~
  27. if (matches.size() > 1) {
  28. matches.sort(this.pathMatcher.getPatternComparator(lookupPath));
  29. }
  30. return matches;
  31. }
  32. // // ===============url的真正匹配规则 非常重要~~~===============
  33. // 注意这个方法的取名,上面是负数,这里是单数~~~~命名规范也是有艺术感的
  34. @Nullable
  35. private String getMatchingPattern(String pattern, String lookupPath) {
  36. // 完全相等,那就不继续聊了~~~
  37. if (pattern.equals(lookupPath)) {
  38. return pattern;
  39. }
  40. // 注意了:useSuffixPatternMatch 这个属性就是我们最终要关闭后缀匹配的关键
  41. // 这个值默外部给传的true(其实内部默认值是boolean类型为false)
  42. if (this.useSuffixPatternMatch) {
  43. // 这个意思是若useSuffixPatternMatch=true我们支持后缀匹配。我们还可以配置fileExtensions让只支持我们自定义的指定的后缀匹配,而不是下面最终的.*全部支持
  44. if (!this.fileExtensions.isEmpty() && lookupPath.indexOf('.') != -1) {
  45. for (String extension : this.fileExtensions) {
  46. if (this.pathMatcher.match(pattern + extension, lookupPath)) {
  47. return pattern + extension;
  48. }
  49. }
  50. }
  51. // 若你没有配置指定后缀匹配,并且你的handler也没有.*这样匹配的,那就默认你的pattern就给你添加上后缀".*",表示匹配所有请求的url的后缀~~~
  52. else {
  53. boolean hasSuffix = pattern.indexOf('.') != -1;
  54. if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) {
  55. return pattern + ".*";
  56. }
  57. }
  58. }
  59. // 若匹配上了 直接返回此patter
  60. if (this.pathMatcher.match(pattern, lookupPath)) {
  61. return pattern;
  62. }
  63. // 这又是它支持的匹配规则。默认useTrailingSlashMatch它也是true
  64. // 这就是为何我们的/hello/也能匹配上/hello的原因
  65. // 从这可以看出,Spring MVC的宽容度是很高的,容错处理做得是非常不错的~~~~~~~
  66. if (this.useTrailingSlashMatch) {
  67. if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) {
  68. return pattern + "/";
  69. }
  70. }
  71. return null;
  72. }
  73. }

 分析了URL的匹配原因,现在肯定知道为何默认情况下"/hello.aaaa"或者"/hello.aaaa/“或者”"/hello/""能匹配上我们/hello的原因了吧~~~

Spring和SpringBoot中如何关闭此项功能呢?

为何要关闭的理由,上面其实已经说了。当我们涉及到严格的权限校验(强权限控制)的时候。特备是一些银行系统、资产系统等等,关闭后缀匹配事非常有必要的。

高版本直接默认就是关闭的

可以看到这两个属性值都直接冒泡到RequestMappingHandlerMapping这个实现类上来了,所以我们直接通过配置来改变它的默认行为就成。

  1. @Configuration
  2. @EnableWebMvc
  3. public class WebMvcConfig implements WebMvcConfigurer {
  4. // 开启后缀名匹配,开启最后一个/匹配
  5. @Override
  6. public void configurePathMatch(PathMatchConfigurer configurer) {
  7. configurer.setUseSuffixPatternMatch(true);
  8. configurer.setUseTrailingSlashMatch(true);
  9. }
  10. }

或者通过xml方式开启

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号