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对象并放到其缓存池中,其过程如下:先查询容器中是否有处理器映射器,如果有就注册到其缓存池中,如果没有就安装默认到规则创建处理器映射器,并注册到其缓存池中。

private void initHandlerMappings(ApplicationContext context) {
		this.handlerMappings = null;
		//detectAllHandlerMappings默认为true
		//true标志检测所有handlerMapping,false只获取“handlerMapping”bean。
		if (this.detectAllHandlerMappings) {
			// 在ApplicationContext中查找所有HandlerMappings,包括祖先上下文。
			Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
			if (!matchingBeans.isEmpty()) {
				this.handlerMappings = new ArrayList<>(matchingBeans.values());
				//排序
				AnnotationAwareOrderComparator.sort(this.handlerMappings);
			}
		}
		else {
			try {
			   //只获取“handlerMapping”bean
				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.
			}
		}
		 //通过注册,确保我们至少有一个HandlerMapping
         //如果找不到其他映射,则为默认HandlerMapping。
		if (this.handlerMappings == null) {
		    //从spring-webmvc下的DispatcherServlet.properties读取默认配置
			this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
			if (logger.isTraceEnabled()) {
				logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
						"': using default strategies from DispatcherServlet.properties");
			}
		}
	}

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的实现方式定死。 

public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
		implements HandlerMapping, Ordered, BeanNameAware {
	//默认的Handler,这边使用的Obejct,子类实现的时候,使用HandlerMethod,HandlerExecutionChain等
	@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;
	/**
	 * Initializes the interceptors.
	 * @see #extendInterceptors(java.util.List)
	 * @see #initInterceptors()
	 */
	@Override
	protected void initApplicationContext() throws BeansException {
		// 给子类扩展:增加拦截器,默认为空实现.RequestMappingHandlerMapping也没有重写这个方法
		extendInterceptors(this.interceptors);
		// 找到所有MappedInterceptor(截器是)类型的bean添加到adaptedInterceptors中
		detectMappedInterceptors(this.adaptedInterceptors);
		// 将interceptors中的拦截器取出放入adaptedInterceptors
		// 如果是WebRequestInterceptor类型的拦截器  需要用WebRequestHandlerInterceptorAdapter进行包装适配
		initInterceptors();
	}
	@Override
	@Nullable
	public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
	    //根据请求获取对应的处理器,子类实现
		Object handler = getHandlerInternal(request);
		if (handler == null) {
		    //如果获取不到,到默认到处理器中
			handler = getDefaultHandler();
		}
		//如果还没有处理器,返回null
		if (handler == null) {
			return null;
		}
		// 意思是如果当前传入的handler是个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 (hasCorsConfigurationSource(handler)) {
			CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null);
			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
			config = (config != null ? config.combine(handlerConfig) : handlerConfig);
			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}
		return executionChain;
	}
	@Nullable
	protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception;
}

getHandlerInternal模板方法的实现类: 

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

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

四、RequestMappingHandlerMapping的初始化

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

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

HandlerMethod映射器模版类的初始化

public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
	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);
	}
	private boolean detectHandlerMethodsInAncestorContexts = false;
	@Nullable
	private HandlerMethodMappingNamingStrategy<T> namingStrategy;
	//注册表,HandlerMapping在容器启动过程中初始化,把扫描到的handler放到注册表中
     private final MappingRegistry mappingRegistry = new MappingRegistry();
	@Override
	public void afterPropertiesSet() {
		initHandlerMethods();
	}
	protected void initHandlerMethods() {
	    //循环所有的bean
		for (String beanName : getCandidateBeanNames()) {
		    //如果bean名字不是以scopedTarget.开头
			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
				processCandidateBean(beanName);
			}
		}
		//日志输出
		handlerMethodsInitialized(getHandlerMethods());
	}
	protected void processCandidateBean(String beanName) {
		Class<?> beanType = null;
		try {
			beanType = obtainApplicationContext().getType(beanName);
		}
		catch (Throwable ex) {
			if (logger.isTraceEnabled()) {
				logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
			}
		}
		//因为这里我们是研究RequestMappingHandlerMapping,所以这局代码内容如下
		//如果beanType不为null,且类上标注@Controller注解或者@RequestMapping注解
		if (beanType != null && isHandler(beanType)) {
			detectHandlerMethods(beanName);
		}
	}
	protected void detectHandlerMethods(Object handler) {
		Class<?> handlerType = (handler instanceof String ?
				obtainApplicationContext().getType((String) handler) : handler.getClass());
		if (handlerType != null) {
			Class<?> userType = ClassUtils.getUserClass(handlerType);
			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);
						}
					});
			if (logger.isTraceEnabled()) {
				logger.trace(formatMappings(userType, methods));
			}
			methods.forEach((method, mapping) -> {
				Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
				//底层使用了MappingRegistry的register方法
				registerHandlerMethod(handler, invocableMethod, mapping);
			});
		}
	}
    //忽略处理器映射器查询Handler部分代码.....
}

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

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

3、把他们储存到MappingRegistry中

AbstractHandlerMethodMapping.MappingRegistry:内部类注册中心

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

class MappingRegistry {
	//对于RequestMappingHandlerMapping来说
	//保存着RequestMappingInfo和MappingRegistration的对应关系~
	private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
	// 对于保存着mapping和HandlerMethod的对应关系~
	//对于RequestMappingHandlerMapping来说
	//保存着RequestMappingInfo和HandlerMethod的对应关系~
	private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
	// 这里的Map不是普通的Map,而是MultiValueMap,它是个多值Map。其实它的value是一个list类型的值
	// 至于为何是多值?有这么一种情况  URL都是/api/v1/hello  但是有的是get post delete等方法   所以有可能是会匹配到多个MappingInfo的
	//对于RequestMappingHandlerMapping来说,保存着URL和RequestMappingInfo的关系~
	private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
	//对于RequestMappingHandlerMapping来说,保存着URL和HandlerMethod的关系~
	private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>()
	// 这两个就不用解释了
	private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
	// 读写锁~~~ 读写分离  提高启动效率
	private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
	//5.1版本其子类只有一个RequestMappingHandlerMapping,T就是RequestMappingInfo
	//handler一般情况下是处理器方法从属bean的名字
	//method是处理器方法
    public void register(T mapping, Object handler, Method method) {
			if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
				throw new IllegalStateException("Unsupported suspending handler method detected: " + method);
			}
			this.readWriteLock.writeLock().lock();
			try {
				HandlerMethod handlerMethod = createHandlerMethod(handler, method);
				//断言提供的映射是唯一的。			
				validateMethodMapping(handlerMethod, mapping);
				this.mappingLookup.put(mapping, handlerMethod);
				List<String> directUrls = getDirectUrls(mapping);
				for (String url : directUrls) {
					this.urlLookup.add(url, mapping);
				}
				String name = null;
				if (getNamingStrategy() != null) {
					name = getNamingStrategy().getName(handlerMethod, mapping);
					addMappingName(name, handlerMethod);
				}
                //初始化跨域配置
                //使用的是AbstractHandlerMethodMapping的initCorsConfiguration方法,子类实现
				CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
				if (corsConfig != null) {
					this.corsLookup.put(handlerMethod, corsConfig);
				}
				this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
			}
			finally {
				this.readWriteLock.writeLock().unlock();
			}
		}

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

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

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

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

public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
	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);
	}
	private boolean detectHandlerMethodsInAncestorContexts = false;
	@Nullable
	private HandlerMethodMappingNamingStrategy<T> namingStrategy;
	//注册表,HandlerMapping在容器启动过程中初始化,把扫描到的handler放到注册表中
     private final MappingRegistry mappingRegistry = new MappingRegistry();
	 //忽略初始化部分代码.....
	protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
	   //获取请求路径
		String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
		//放到请求属性中
		request.setAttribute(LOOKUP_PATH, lookupPath);
		this.mappingRegistry.acquireReadLock();
		try {
		    //根据请求和路径获取对应的处理方法,注册表中取
			HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
			return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
		}
		finally {
			this.mappingRegistry.releaseReadLock();
		}
	}
	protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
	    // Match是一个private class,内部就两个属性:T mapping和HandlerMethod handlerMethod
		List<Match> matches = new ArrayList<>();
		// 根据lookupPath去注册中心里查找RequestMappingInfo,因为一个具体的url可能匹配上多个RequestMappingInfo
		// 至于为何是多值?有这么一种情况  URL都是/api/v1/hello  但是有的是get post delete等方法  等不一样,都算多个的  所以有可能是会匹配到多个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呢?
		// 增加路径匹配对范围,如:/rest 匹配 /rest.ssss
		if (matches.isEmpty()) {
			addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
		}
        // 只要找到了一个匹配的  就进来这里了~~~
		// 请注意:因为到这里   匹配上的可能还不止一个  所以才需要继续处理~~
		if (!matches.isEmpty()) {
			Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
			matches.sort(comparator);
			//如果匹配到多个,就取第一个
			Match bestMatch = matches.get(0);
			if (matches.size() > 1) {
				if (logger.isTraceEnabled()) {
					logger.trace(matches.size() + " matching mappings: " + matches);
				}
				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();
					String uri = request.getRequestURI();
					throw new IllegalStateException(" ");
				}
			}
			request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
			//请求域增加一些属性,子类重写
			handleMatch(bestMatch.mapping, lookupPath, request);
			return bestMatch.handlerMethod;
		}
		else {
		   //请求域增加一些属性,子类重写
			return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
		}
	}	
}
protected void handleMatch(T mapping, String lookupPath, HttpServletRequest request) {
	request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath);
}
@Nullable
protected HandlerMethod handleNoMatch(Set<T> mappings, String lookupPath, HttpServletRequest request)
	throws Exception {
return null;
}

RequestMappingHandlerMapping调用

public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {
	private static final Method HTTP_OPTIONS_HANDLE_METHOD;
	/**
	 * Expose URI template variables, matrix variables, and producible media types in the request.
	 * @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE
	 * @see HandlerMapping#MATRIX_VARIABLES_ATTRIBUTE
	 * @see HandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE
	 */
	@Override
	protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) {
		super.handleMatch(info, lookupPath, request);
		String bestPattern;
		Map<String, String> uriVariables;
		Set<String> patterns = info.getPatternsCondition().getPatterns();
		if (patterns.isEmpty()) {
			bestPattern = lookupPath;
			uriVariables = Collections.emptyMap();
		}
		else {
			bestPattern = patterns.iterator().next();
			uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
		}
		request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern);
		if (isMatrixVariableContentAvailable()) {
			Map<String, MultiValueMap<String, String>> matrixVars = extractMatrixVariables(request, uriVariables);
			request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, matrixVars);
		}
		Map<String, String> decodedUriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables);
		request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables);
		if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
			Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
			request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
		}
	}
	private boolean isMatrixVariableContentAvailable() {
		return !getUrlPathHelper().shouldRemoveSemicolonContent();
	}
	private Map<String, MultiValueMap<String, String>> extractMatrixVariables(
			HttpServletRequest request, Map<String, String> uriVariables) {
		Map<String, MultiValueMap<String, String>> result = new LinkedHashMap<>();
		uriVariables.forEach((uriVarKey, uriVarValue) -> {
			int equalsIndex = uriVarValue.indexOf('=');
			if (equalsIndex == -1) {
				return;
			}
			int semicolonIndex = uriVarValue.indexOf(';');
			if (semicolonIndex != -1 && semicolonIndex != 0) {
				uriVariables.put(uriVarKey, uriVarValue.substring(0, semicolonIndex));
			}
			String matrixVariables;
			if (semicolonIndex == -1 || semicolonIndex == 0 || equalsIndex < semicolonIndex) {
				matrixVariables = uriVarValue;
			}
			else {
				matrixVariables = uriVarValue.substring(semicolonIndex + 1);
			}
			MultiValueMap<String, String> vars = WebUtils.parseMatrixVariables(matrixVariables);
			result.put(uriVarKey, getUrlPathHelper().decodeMatrixVariables(request, vars));
		});
		return result;
	}
	/**
	 * Iterate all RequestMappingInfo's once again, look if any match by URL at
	 * least and raise exceptions according to what doesn't match.
	 * @throws HttpRequestMethodNotSupportedException if there are matches by URL
	 * but not by HTTP method
	 * @throws HttpMediaTypeNotAcceptableException if there are matches by URL
	 * but not by consumable/producible media types
	 */
	@Override
	protected HandlerMethod handleNoMatch(
			Set<RequestMappingInfo> infos, String lookupPath, HttpServletRequest request) throws ServletException {
		PartialMatchHelper helper = new PartialMatchHelper(infos, request);
		if (helper.isEmpty()) {
			return null;
		}
		if (helper.hasMethodsMismatch()) {
			Set<String> methods = helper.getAllowedMethods();
			if (HttpMethod.OPTIONS.matches(request.getMethod())) {
				HttpOptionsHandler handler = new HttpOptionsHandler(methods);
				return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
			}
			throw new HttpRequestMethodNotSupportedException(request.getMethod(), methods);
		}
		if (helper.hasConsumesMismatch()) {
			Set<MediaType> mediaTypes = helper.getConsumableMediaTypes();
			MediaType contentType = null;
			if (StringUtils.hasLength(request.getContentType())) {
				try {
					contentType = MediaType.parseMediaType(request.getContentType());
				}
				catch (InvalidMediaTypeException ex) {
					throw new HttpMediaTypeNotSupportedException(ex.getMessage());
				}
			}
			throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(mediaTypes));
		}
		if (helper.hasProducesMismatch()) {
			Set<MediaType> mediaTypes = helper.getProducibleMediaTypes();
			throw new HttpMediaTypeNotAcceptableException(new ArrayList<>(mediaTypes));
		}
		if (helper.hasParamsMismatch()) {
			List<String[]> conditions = helper.getParamConditions();
			throw new UnsatisfiedServletRequestParameterException(conditions, request.getParameterMap());
		}
		return null;
	}
}

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这个类上,具体实现是它的匹配逻辑来决定的。

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中如何关闭此项功能呢?

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

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

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

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
    // 开启后缀名匹配,开启最后一个/匹配
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.setUseSuffixPatternMatch(true);
        configurer.setUseTrailingSlashMatch(true);
    }
}

或者通过xml方式开启

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/753365.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

从菌群代谢到健康影响——认识肠道丙酸和丁酸

谷禾健康 短链脂肪酸这一词经常出现在谷禾的文章和报告中&#xff0c;那你真的了解短链脂肪酸吗&#xff1f;短链脂肪酸(SCFA)主要是肠道微生物群在结肠内通过发酵碳水化合物(包括膳食和内源性碳水化合物&#xff0c;主要是抗性淀粉和膳食纤维)和一些微生物可利用的蛋白质而产生…

一个人 三个月 干了二十万

相信很多人是被这个标题吸引进来的&#xff0c;但我并不是标题党&#xff0c;我也很讨厌标题党&#xff0c;这篇文章也不在乎流量&#xff0c;更多的是想记录下。 出来创业三个多月了&#xff0c;给大家汇报一下这段时间的业绩吧。一个人&#xff0c;三个多月&#xff0c;干了…

线性图标绘制指南:从基础到精通

图标在生活中随处可见。相比文字来说&#xff0c;图标可以让人在更短的时间内认知并了解信息&#xff0c;并且大大提升信息的视觉美观性&#xff0c;增加设计的艺术感染力。在用户界面中使用图标&#xff0c;是一种用户熟知的设计模式。而线性图标是通过提炼图形轮廓&#xff0…

TIOBE 6月榜单出炉!编程语言地位大洗牌,谁才是王?

C历史上首次超越C&#xff01;&#xff01;&#xff01; TIOBE 公布了 2024 年 6 月编程语言的排行榜&#xff1a;https://www.tiobe.com/tiobe-index/ 排行榜 以下列出的语言代表了第51至第100名。由于它们之间的差异相对较小&#xff0c;编程语言仅以字母顺序列出。 ABC, A…

ubuntu16.04上搭建qt开发环境

安装qt 下载qt在linux下的安装包qt-opensource-linux-x64-5.8.0.run&#xff1b;将安装包拷贝到ubuntu16.04上&#xff1b;执行如下命令进行安装&#xff0c;注意&#xff1a;安装前先断开虚拟机的网络&#xff0c;这样就避免了注册账户的操作 基本上一路按“next”键&#xf…

使用青否数字人直播软件有哪些优势?

使用青否数字人主播主要具有以下优势&#xff1a; 1、降低直播门槛 在垂直程度较高、专业度更强的行业&#xff0c;面对相关品牌们“专业主播难培养”的问题。数字人主播的学习技能和灵活优势尽显。通过数字人直播可以借助知识库配置与AI能力&#xff0c;快速获得技术性知识&am…

【2024最新华为OD-C/D卷试题汇总】[支持在线评测] 螺旋矩阵填数(100分) - 三语言AC题解(Python/Java/Cpp)

&#x1f36d; 大家好这里是清隆学长 &#xff0c;一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 &#x1f4bb; ACM银牌&#x1f948;| 多次AK大厂笔试 &#xff5c; 编程一对一辅导 &#x1f44f; 感谢大家的订阅➕ 和 喜欢&#x1f497; &#x1f…

c语言--指针

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文整理c语言中指针的相关知识点。 指针概念 指针存储的就是数据的地址。 直观理解: 李华家是北洋路130号1单元101 用变量处理数据: 我们去李华家拿数据。 用指针处理数据: 我们去北洋路130号1单元101拿数据…

石墨舟氮气柜的特点和使用要求介绍

石墨舟是一种在半导体、太阳能光伏等高科技产业中广泛使用的专用工具&#xff0c;主要由高纯度石墨材料制成。它的形状通常像一只船&#xff0c;因此得名“石墨舟”。石墨舟主要用于承载硅片或其他基板材料通过各种高温处理过程&#xff0c;是制造半导体器件和太阳能电池片的关…

二叉树的方法

目录 一、二叉树的定义 ​编辑 二、二叉树的创建 三、二叉树的遍历 1、前序遍历 2、中序遍历 3、后序遍历 4、层序遍历 四、二叉树遍历方法的使用 五、二叉树的操作 1、节点的个数 2、叶子节点的个数 3、第k层节点的个数 4、二叉树的高度 5、检查值为value的元素…

「2024抢先看」6款免费的ai变声器,助你开麦就变声

你是否也曾想模仿电视剧中的明星说话&#xff0c;或者想像泰勒一样有着独特的嗓音呢&#xff1f;通过免费版ai变声器&#xff0c;你可以轻松实现实时变声&#xff0c;将你的声音转换为专业且动听的声音&#xff0c;让身边的朋友对你刮目相看。在本文中&#xff0c;小编将分享20…

服务器日志事件ID4107:从自动更新 cab 中提取第三方的根目录列表失败,错误为: 已处理证书链,但是在不受信任提供程序信任的根证书中终止。

在查看Windows系统日志时&#xff0c;你是否有遇到过事件ID4107错误&#xff0c;来源CAPI2&#xff0c;详细信息在 http://www.download.windowsupdate.com/msdownload/update/v3/static/trustedr/en/authrootstl.cab 从自动更新 cab 中提取第三方的根目录列表失败&#xff0c;…

简单的本地局域网的前后端接口联调

由于项目被赶进度了&#xff0c;急于前后端联调接口&#xff0c;但是我又没钱买服务器&#xff08;主要我也不会部署&#xff09;&#xff0c;所以我这里就紧急找一个后端的大神朋友请教了一下&#xff1a;苏泽SuZe-CSDN博客 提示&#xff1a;这里不讲后端怎么写接口、前端怎么…

C#——里氏转换详情

里氏转换 里氏转换就是派生类的对象赋值给父类对象&#xff0c;反之则不行 实例 : 先创键一个类然后继承 调用

双路视频同屏显示(拼接)-基于野火Zynq7020开发板

前情提要 米联客FDMA驱动OV5640摄像头—基于野火Zynq7020开发板 本文在此基础上&#xff0c;实现了双路视频拼接。将ov5640输出的1024600的图像数据缩放为512600&#xff0c;分两路写入ddr3&#xff0c;并且显示在1024*600的RGB屏幕中。 纯FPGA也可以按此方法实现。 总体BLOC…

【C++ 初阶路】--- 类和对象(末)

目录 一、const成员1.1 取地址及const取地址操作符重载 二、再谈构造函数2.1 构造函数体赋值2.2 初始化列表2.3 explicit关键字 三、static成员3.1 概念3.2 特性 四、友元4.1 友元函数4.2 友元类 五、内部类六、匿名对象 一、const成员 将const修饰的“成员函数”称之为const成…

Qt creator实现一个简单计算器

目录 1 界面设计 2 思路简介 3 代码 目录 1 界面设计 ​2 思路简介 3 代码 3.1 widget.h 3.2 widget.c 4 完整代码 在这里主要记载了如何使用Qt creator完成一个计算器的功能。该计算器可以实现正常的加减乘除以及括号操作&#xff0c;能实现简单的计算器功能。 1 界…

北京站圆满结束!MongoDB Developer Day上海站,周六见!

上周六 MongoDB Developer Day首站北京站 80位开发者与MongoDB一起度过了充实的一天 专题讲座➕动手实操➕专家面对面交流 从数据建模、进阶查询技巧 到Atlas搜索与向量搜索 让参会伙伴们直呼“满满的技术干货&#xff01;” 全体参会者与工作人员合影 MongoDB Developer …

【unity笔记】五、UI面板TextMeshPro 添加中文字体

Unity 中 TextMeshPro不支持中文字体&#xff0c;下面为解决方法&#xff1a; 准备字体文件&#xff0c;从Windows系统文件的Fonts文件夹里拖一个.ttf文件&#xff08;C盘 > Windows > Fonts &#xff09; 准备字库文件,新建一个文本文件&#xff0c;命名为“字库”&…

Vue组件化、单文件组件以及使用vue-cli(脚手架)

文章目录 1.Vue组件化1.1 什么是组件1.2 组件的使用1.3 组件的名字1.4 嵌套组件 2.单文件组件2.1 vue 组件组成结构2.1.1 template -> 组件的模板结构2.1.2 组件的 script 节点2.1.3 组件的 style 节点 2.2 Vue组件的使用步骤2.2.1 组件之间的父子关系2.2.2 使用组件的三个步…