Spring MVC注解Controller源码流程解析--定位HandlerMethod


引言

Spring MVC注解Controller源码流程解析–映射建立

上一篇中,我们对映射建立的过程做了详细的分析,既然映射关系已经建立完毕了,那么下面就是当请求来临时,如何通过请求去映射集合中寻找出对应的HandlerMethod,然后再交给RequestMappingHandlerAdapter完成请求最终处理。

如果是通过请求路径去映射集合中通过精确匹配进行查询的话,其实实现起来就很简单了,但是因为要加入@RequestMapping中相关请求限制,包括通配符匹配和占位符匹配等等内容,会让寻找HandlerMethod的过程变的不那么简单,但是也没有那么复杂,下面我们就来看看。


定位HandlerMethod

	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		        ....
		        //检查是否是文件上传请求,如果是的话,就返回封装后的MultipartHttpServletRequest
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				//通过当前请求定位到具体处理的handler--这里是handlerMethod
				mappedHandler = getHandler(processedRequest);
				....

我们本节的重点就在getHandler方法中:

	@Nullable
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			for (HandlerMapping mapping : this.handlerMappings) {
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

getHandler方法中会遍历所有可用的HandlerMapping,然后尝试通过当前请求解析得到一个handler,如果不为空,说明找到了,否则借助下一个HandlerMapping继续寻找。

前面已经说过了,注解Controller的映射建立是通过RequestMappingHandlerMapping完成的,那么寻找映射当然也需要通过RequestMappingHandlerMapping完成,因此我们这里只关注RequestMappingHandlerMapping的getHandler流程链即可。


getHandler方法主要是由AbstractHandlerMapping顶层抽象基类提供了一个模板方法实现,具体根据request寻找handler的逻辑实现,是通过getHandlerInternal抽象方法交给子类实现的。

	public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
	    //这里调用到的是RequestMappingInfoHandlerMapping子类提供的实现
		Object handler = getHandlerInternal(request);
		//如果没找到,尝试寻找兜底的默认handler
		if (handler == null) {
			handler = getDefaultHandler();
		}
		//如果还是兜底也不管用,就返回null
		if (handler == null) {
			return null;
		}
		// 考虑到handler本身可能是一个多例bean,因此不能提前进行实例化,所以spring在handlerMapping的抽象父类AbstractHandlerMapping中
		//对这一情况进行处理,就是我们下面看到的
		//当然,考虑到handler存在多种实现,如果某种自定义实现情况下,强制要求handler为单例的,这里也支持
		//因为没有在handler不为字符串情况下抛出异常
		if (handler instanceof String) {
			String handlerName = (String) handler;
			handler = obtainApplicationContext().getBean(handlerName);
		}

		 ...
        
        //构建拦截器链
		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

		...
		//跨域处理
		if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
			...
		}

		return executionChain;
	}

RequestMappingInfoHandlerMapping提供的getHandlerInternal实现

  • RequestMappingInfoHandlerMapping主要作为RequestMappingInfo,Request和HandlerMethod三者之间沟通的桥梁,RequestMappingInfo提供请求匹配条件,判断当前Request是否应该交给当前HandlerMethod处理
	protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
		request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
		try {
		    //调用父类AbstractHandlerMethodMapping的方法
			return super.getHandlerInternal(request);
		}
		finally {
		     //把下面这个方法进行内联后,等价于 : request.removeAttribute(MEDIA_TYPES_ATTRIBUTE);
			ProducesRequestCondition.clearMediaTypesAttribute(request);
		}
	}

清除Request相关属性,主要是因为Request对象会被复用,因此使用前,需要清空上一次的数据,这也算是对象复用增加的代码复杂性吧。


AbstractHandlerMethodMapping提供的getHandlerInternal实现

RequestMappingInfoHandlerMapping重写了父类的getHandlerInternal方法,但只是对Request对象复用进行了相关数据清除工作,核心还是在AbstractHandlerMethodMapping提供的getHandlerInternal实现中。

	protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
	    //initLookupPath默认是返回Context-path后面的路径
	    //eg1: 没有设置context-path,请求路径为localhost:5200/volunteer/back/admin/pass/login,那这里返回的就是/volunteer/back/admin/pass/login
	    //eg2: 上面的例子中设置了context-path为/volunteer,那这里返回的就是/back/admin/pass/login
		String lookupPath = initLookupPath(request);
		//获取读锁
		this.mappingRegistry.acquireReadLock();
		try {
		   //通过请求路径去映射集合中寻找对应的handlerMethod
			HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
			return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
		}
		finally {
			this.mappingRegistry.releaseReadLock();
		}
	}

initLookupPath方法中默认会返回的请求路径为剥离掉context-path后的路径,并且后续拦截器中进行路径匹配时,匹配的也是剥离掉context-path后的路径,这一点切记!


根据请求路径去映射集合中寻找HandlerMethod

lookupHandlerMethod是本文的核心关注点,该方法会通过Request定位到对应的HandlerMethod后返回。

具体处理过程,又可以分为三种情况:

  • 精确匹配到一个结果
  • 需要进行最佳匹配
  • 没有匹配到任何结果

因为这部分逻辑比较复杂,因此我们对三种情况分开讨论。


精确匹配到一个结果

	@Nullable
	protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
		List<Match> matches = new ArrayList<>();
		//先通过请求路径去pathLookup集合中尝试进行精准匹配--这里的T指的是RequestMappingInfo
		List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
		//精准匹配到了结果
		if (directPathMatches != null) {
			//将结果添加进matches集合中--还会经过RequstMappingInfo的条件校验环节
			addMatchingMappings(directPathMatches, matches, request);
		}
		//如果上面精确匹配没有匹配到结果----
		if (matches.isEmpty()) {
		     //将register的keySet集合保存的所有RequestMappingInfo都加入matches集合中去
		     //然后依次遍历每个RequstMappingInfo,通过其自身提供的getMatchingCondition对当前requst请求进行条件匹配
		     //如果不满足条件,是不会加入到当前matches集合中去的
			addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
		}
		
		if (!matches.isEmpty()) {
		    //获取matches集合中第一个元素
			Match bestMatch = matches.get(0);
			//如果matches集合元素大于0,说明需要进一步进行模糊搜索
			if (matches.size() > 1) {
				...
			}
			//在request对象的属性集合中设置处理当前请求的HandlerMethod
			request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod());
			//处理当前最佳匹配
			handleMatch(bestMatch.mapping, lookupPath, request);
			return bestMatch.getHandlerMethod();
		}
		else {
		     //没有匹配结果
			return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
		}
	}

addMatchingMappings将得到的匹配结果RequestMappingInfo加入matches集合,但这个过程中还需要进行一些特殊处理,例如:

    @PostMapping({PASS+"login",PASS+"log"})

此时PostMapping会映射到两种请求路径上,此时这里需要做的就是,搞清楚到底是哪一个路径匹配上了当前请求,然后修改RequestMappingInfo对应的patterns集合,将多余的请求路径去除掉。

还有就是一个请求路径可能会映射到多个RequestMappingInfo上,例如:
在这里插入图片描述
请求路径相同,只是请求方法不同。

	private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
	    //遍历每个RequestMappinginfo
		for (T mapping : mappings) {
		     //判断当前RequestMappingInfo是否能够真正映射到当前请求上
			T match = getMatchingMapping(mapping, request);
			//如果返回值不为空,表示可以映射,否则跳过处理下一个
			if (match != null) {
				matches.add(new Match(match, this.mappingRegistry.getRegistrations().get(mapping)));
			}
		}
	}

getMatchingMapping的判断还是通过RequestMappingInfo自身提供的条件进行进行匹配的:

	@Override
	protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
		return info.getMatchingCondition(request);
	}

检查当前RequestMappingInfo 中的所有条件是否与提供的请求匹配,并返回一个新的RequestMappingInfo,其中包含针对当前请求量身定制的条件。

例如,返回的实例可能包含与当前请求匹配的 URL 模式的子集,并以最佳匹配模式在顶部进行排序。

	@Override
	@Nullable
	public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
	    //检查@RequestMapping注解提供的method请求方式是否与当前请求匹配,如果不匹配返回null
		RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
		if (methods == null) {
			return null;
		}
		//判断设置的请求参数匹配条件是否匹配
		ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
		if (params == null) {
			return null;
		}
		//请求头条件
		HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
		if (headers == null) {
			return null;
		}
		//Consume条件检查
		ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
		if (consumes == null) {
			return null;
		}
		//Produce条件检查
		ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
		if (produces == null) {
			return null;
		}
		//PathPatternsRequestCondition一般为null
		PathPatternsRequestCondition pathPatterns = null;
		if (this.pathPatternsCondition != null) {
			pathPatterns = this.pathPatternsCondition.getMatchingCondition(request);
			if (pathPatterns == null) {
				return null;
			}
		}
		//@PostMapping({PASS+"login",PASS+"log"})的情况处理 
	    //RequestMappingInfo中的patterns数组中如果存在多个请求路径,需要判断当前请求是具体映射到了那个路径上
	    //然后重新构造一个patternsCondition后返回,该patternsCondition内部包含的只有匹配当前请求路径的那个pattern
		PatternsRequestCondition patterns = null;
		if (this.patternsCondition != null) {
			patterns = this.patternsCondition.getMatchingCondition(request);
			if (patterns == null) {
				return null;
			}
		}
		//自定义请求限制
		RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
		if (custom == null) {
			return null;
		}
		//上面这些条件其中一个不通过,那么返回的结果就为null
		//最后构造一个全新的RequestMappingInfo返回,该RequestMappingInfo中包含的都是匹配上当前请求路径的信息,排除了其他非匹配上的信息
		return new RequestMappingInfo(this.name, pathPatterns, patterns,
				methods, params, headers, consumes, produces, custom, this.options);
	}

如果不清楚@ReuqestMapping注解中各个属性的作用,那么把上面每个条件判断过程看一遍就明白了。


handleMatch法主要是针对模糊匹配出来的结果进行相关处理,例如: URI template variables,matrix variables和producible media types处理等等…

上面这些名词关联的注解有: @PathVariable , @MatrixVariable ,producible media types对应的是@RequestMapping中produces设置。

	@Override
	protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) {
		super.handleMatch(info, lookupPath, request);
        //一般返回的就是@RequestMapping注解中的patterns属性,注意@RequestMapping注解可以映射到多个URL上
        //这里返回的就是patterns属性对应的patternsCondition请求匹配条件对象
		RequestCondition<?> condition = info.getActivePatternsCondition();
		//condition默认实现为patternsCondition,因此这里直接走else分支
		if (condition instanceof PathPatternsRequestCondition) {
			extractMatchDetails((PathPatternsRequestCondition) condition, lookupPath, request);
		}
		else {
		    //抽取匹配细节,该方法内部会完成对上面这些模板变量,矩阵变量的处理
			extractMatchDetails((PatternsRequestCondition) condition, lookupPath, request);
		}
        //如果我们设置了@RequestMapping注解中的produces属性,那么这里会进行处理
		if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
			Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
			//设置到request对象的属性集合中,不用想,肯定会在响应的时候用到
			request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
		}
	}

父类AbstractHandlerMethodMapping中的handleMatch方法,主要是将lookup设置到当前请求对象的属性集合中去:

	protected void handleMatch(T mapping, String lookupPath, HttpServletRequest request) {
		request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath);
	}

对模板变量和矩阵变量的抽取
	private void extractMatchDetails(
	       //传入的patternsCondition主要作用在于其内部的patterns属性集合,该集合封装了@RequestMapping注解的patterns属性值内容
			PatternsRequestCondition condition, String lookupPath, HttpServletRequest request) {

		String bestPattern;
		Map<String, String> uriVariables;
		//如果patterns属性集合为空--说明我们直接标注了一个@RequestMapping注解,但是没有指定任何属性限制
		if (condition.isEmptyPathMapping()) {
		    //那就不存在什么模糊匹配了,bestPattern 就是当前请求路径
			bestPattern = lookupPath;
			//模板变量和矩阵变量当然也就不存在了,直接一个空集合
			uriVariables = Collections.emptyMap();
		}
		//我们需要考虑是否存在相关模板变量或者矩阵变量
		else {
		    //patterns集合中第一个属性为最佳匹配--这个在addMatchingMappings中被处理完成,不清楚回头看一下
			bestPattern = condition.getPatterns().iterator().next();
			//解析模板变量,eg: "/hotels/{hotel}" and path "/hotels/1" --> 返回的map就是"hotel"->"1"
			uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
			//关于矩阵变量的处理---这里不展开,感兴趣自己debug看一下源码
			if (!getUrlPathHelper().shouldRemoveSemicolonContent()) {
				request.setAttribute(MATRIX_VARIABLES_ATTRIBUTE, extractMatrixVariables(request, uriVariables));
			}
			uriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables);
		}
		//设置最佳匹配路径和URL模板变量集合到request对象的属性集合中去
		request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern);
		request.setAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriVariables);
	}

关于模板变量和矩阵变量的解析细节这里不多展开了,感兴趣可以按照当前思路自行debug源码。


最佳匹配

	@Nullable
	protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
		List<Match> matches = new ArrayList<>();
		List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
		if (directPathMatches != null) {
			addMatchingMappings(directPathMatches, matches, request);
		}
		if (matches.isEmpty()) {
			addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
		}
		if (!matches.isEmpty()) {
			Match bestMatch = matches.get(0);
			//如果能够处理当前请求的RequestMappingInfo存在多个,下面就需要进行最佳匹配
			if (matches.size() > 1) {
				Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
				matches.sort(comparator);
				bestMatch = matches.get(0);
				if (logger.isTraceEnabled()) {
					logger.trace(matches.size() + " matching mappings: " + matches);
				}
				if (CorsUtils.isPreFlightRequest(request)) {
					for (Match match : matches) {
						if (match.hasCorsConfig()) {
							return PREFLIGHT_AMBIGUOUS_MATCH;
						}
					}
				}
				else {
					Match secondBestMatch = matches.get(1);
					if (comparator.compare(bestMatch, secondBestMatch) == 0) {
						Method m1 = bestMatch.getHandlerMethod().getMethod();
						Method m2 = secondBestMatch.getHandlerMethod().getMethod();
						String uri = request.getRequestURI();
						throw new IllegalStateException(
								"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
					}
				}
			}
			request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod());
			handleMatch(bestMatch.mapping, lookupPath, request);
			return bestMatch.getHandlerMethod();
		}
		else {
			return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
		}
	}

是如何完成最佳匹配的,这个过程本文不展开论述,感兴趣自行研究。


匹配失败

如果没有寻找当前一个RequestMappingInfo能够处理当前request,那么进入handleNoMatch阶段。

handleNoMatch会再次迭代所有 RequestMappingInfo,至少通过 URL 查看是否有任何匹配,并根据不匹配的内容引发异常。

说人话就是找出不匹配的原因,然后抛出对应的异常,告诉用户。

	@Override
	protected HandlerMethod handleNoMatch(
			Set<RequestMappingInfo> infos, String lookupPath, HttpServletRequest request) throws ServletException {
        //借助PartialMatchHelper来分析那些部分匹配的请求,是因为什么原因而无法匹配成功的
        //部分匹配的意思就是请求路径匹配上了,但是因为其他条件匹配失败了,例如: 请求头限制等
		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())) {
				Set<MediaType> mediaTypes = helper.getConsumablePatchMediaTypes();
				HttpOptionsHandler handler = new HttpOptionsHandler(methods, mediaTypes);
				return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
			}
			throw new HttpRequestMethodNotSupportedException(request.getMethod(), methods);
		}
       //consume条件不满足
		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));
		}
        //produces条件不满足
		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;
	}

PartialMatchHelper会首先获取到所有请求路径匹配成功的RequestMappingInfo:

		public PartialMatchHelper(Set<RequestMappingInfo> infos, HttpServletRequest request) {
			for (RequestMappingInfo info : infos) {
				if (info.getActivePatternsCondition().getMatchingCondition(request) != null) {
					this.partialMatches.add(new PartialMatch(info, request));
				}
			}
		}

PartialMatch的构造方法会判断当前RequestMappingInfo不匹配的原因是什么:

			public PartialMatch(RequestMappingInfo info, HttpServletRequest request) {
				this.info = info;
				this.methodsMatch = (info.getMethodsCondition().getMatchingCondition(request) != null);
				this.consumesMatch = (info.getConsumesCondition().getMatchingCondition(request) != null);
				this.producesMatch = (info.getProducesCondition().getMatchingCondition(request) != null);
				this.paramsMatch = (info.getParamsCondition().getMatchingCondition(request) != null);
			}

相关has*Mismatch就是遍历partialMatches集合,然后挨个判断是否存在对应的不匹配原因:

		public boolean hasMethodsMismatch() {
			for (PartialMatch match : this.partialMatches) {
				if (match.hasMethodsMatch()) {
					return false;
				}
			}
			return true;
		}

小结

到此为止,根据request请求去HandlerMethod注册中心寻找对应HandlerMethod的过程就分析完毕了,下一节,会对handlerMethod的调用过程进行分析。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring MVC是一种基于Java的Web应用框架,它可以帮助开发者快速构建Web应用程序。要实现一个网上书城的源码,首先需要创建一个基于Spring MVC的项目。然后,我们需要定义书城的功能和需求,比如展示书籍、购买书籍、用户登录注册等。接着,我们可以定义书籍的数据模型,比如书籍的名称、作者、价格等信息。 在Spring MVC中,我们可以使用@Controller注解来定义处理HTTP请求的控制器类。我们可以在控制器类中定义各种处理请求的方法,比如展示书籍列表的方法、展示书籍详情的方法、处理用户购买书籍的方法等。通过@RequestMapping注解,我们可以将请求映射到相应的处理方法上。 此外,我们还可以使用@Service注解来定义服务类,比如书籍服务类、用户服务类等。服务类可以实现业务逻辑,比如获取书籍列表、添加购物车、生成订单等。 在视图层,我们可以使用Thymeleaf、JSP等模板引擎来构建页面,展示书籍信息、用户信息等。而在数据持久层,我们可以选择使用Hibernate、Spring Data JPA等技术来操作数据库,管理书籍信息、用户信息等数据。 最后,我们可以使用Maven或者Gradle等工具来管理项目的依赖,构建项目的打包部署。通过以上步骤,我们就可以实现一个基于Spring MVC的网上书城源码,实现书籍的展示、购买和用户管理等功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值