学习@RequestBody注解解析请求参数流程

一、背景

研究对象是Springboot的一个后台Web系统。
想了解,在SpringMVC对@RequestBody的参数进行注入之前,执行了request.getInputStream()/request.getReader()或者request.getParameter()方法,会不会对参数的获取造成影响。也就是@RequestBody是如何获取到Http请求体中的参数的。

二、Controller中Handler的注册

留意到每次系统启动的时候,Spring会打印这类日志

Mapped "{[/xxx/yyyy/aaa],methods=[POST]}" onto ......
因此,对于Controller中对RequestMapping的解析,就从此处的日志开始。看看在解析RequestMapping的时候,有没有对@RequestBody注解的参数进行处理。

  1. 找到打印这行日志的类以及行数, RequestMappingHandlerMapping:547.
    发现这个类中总共都没有547行,那么就去它继承的父类中去找,结果在AbstractHandlerMethodMapping这个类中找到了对应的行和方法。
public void register(T mapping, Object handler, Method method) {
  // 省略
}

从方法名以及参数上来看,肯定是将Controller中每个RequestMapping对应的Method注册起对应关系。但是入参到底是啥也不清楚,那么就看哪些地方调用了这个方法。
发现了如下的调用链路:

AbstractHandlerMethodMapping#initHandlerMethods
AbstractHandlerMethodMapping#detectHandlerMethods
AbstractHandlerMethodMapping#registerHandlerMethod
AbstractHandlerMethodMapping#register

  1. 那么从initHandlerMethods开始看
protected void initHandlerMethods() {
    // 省略,获取所有beanNames
		for (String beanName : beanNames) {
			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
				Class<?> beanType = null;
				try {
					beanType = obtainApplicationContext().getType(beanName);
				}
				catch (Throwable ex) {
					// An unresolvable bean type, probably from a lazy bean - let's ignore it.
					if (logger.isDebugEnabled()) {
						logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
					}
				}
                                // 关键代码处
				if (beanType != null && isHandler(beanType)) {
					detectHandlerMethods(beanName);
				}
			}
		}
		handlerMethodsInitialized(getHandlerMethods());
	}

在这个方法中的关键代码处可以看到,会对bean进行判断isHandler, 如果是Handler,那么就去解析里面的Methods

@Override
	protected boolean isHandler(Class<?> beanType) {
		return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
				AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
	}

可以看到,只要是有@Controller或者@RequestMapping的Bean,都是Handler。
也可以看到所谓的Handler就是我们日常说的身为Controller的Bean。
3. 继续进入detectHandlerMethods方法中

Class<?> handlerType = (handler instanceof String ?
				obtainApplicationContext().getType((String) handler) : handler.getClass());

		if (handlerType != null) {
			final Class<?> userType = ClassUtils.getUserClass(handlerType);
			Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
					(MethodIntrospector.MetadataLookup<T>) method -> {
						try {
							return getMappingForMethod(method, userType);  //关键处1
						}
						catch (Throwable ex) {
							throw new IllegalStateException("Invalid mapping on handler class [" +
									userType.getName() + "]: " + method, ex);
						}
					});
			if (logger.isDebugEnabled()) {
				logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods);
			}
			methods.forEach((method, mapping) -> {
				Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
				registerHandlerMethod(handler, invocableMethod, mapping);  //关键处2
			});
		}

这个方法就是获取Controller中的所有方法,并一个个去解析
在关键处1,里面所做的事情就是先判断这个方法是否有@RequestMapping注解,其次获取注解里面的信息(请求头、请求方法,请求参数等)并记录到RequestMappingInfo对象中。 并且假如Controller上也有RequestMapping注解,那就还要进行一些合并操作。都做完了就返回一个整体的RequestMappingInfo对象

在关键处2,就是建立Mapping与Method的映射关系,等到实际调用的时候,根据请求的地址解析得到Mapping,取出相应的Method进行调用。
当然这里面是有代理的,具体细节就没有去详细看。因为我的目的是先了解流程。
在这个解析过程中,好像并没有对@RequestBody的处理,那么就看看在实际调用的时候,是怎么处理@RequestBody的

二、@RequestBody参数解析

一个Http请求,必然从Servlet的doService开始的(抛开拦截器与过滤器),那么就从SpringMVC的DispatcherServlet开始入手。顺着doService看到doDispatch,然后doDispatch中可以看到一行:

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

那么实际的接口处理流程就应该是在这里面了,跟进可以看到RequestMappingHandlerAdapter#handleInternal方法中,

mav = invokeHandlerMethod(request, response, handlerMethod);

从方法名就可以看出是进行Controller中的Method调用的,继续跟进去,会发现RequestMappingHandlerAdapter有一个成员变量是argumentResolvers,那么从名称来看,很大概率就是我想要找到的参数解析器。
这个argumentResolvers是个HandlerMethodArgumentResolverComposite类的实例,进入这个类中,就有一个HandlerMethodArgumentResolver数组,里面就是所有的参数解析器了。
继续在这个类中往下看看,会发现这么两个方法:

        @Override
	@Nullable
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
		if (resolver == null) {
			throw new IllegalArgumentException("Unknown parameter type [" + parameter.getParameterType().getName() + "]");
		}
		return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
	}

	/**
	 * Find a registered {@link HandlerMethodArgumentResolver} that supports the given method parameter.
	 */
	@Nullable
	private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
		HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
		if (result == null) {
			for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
				if (logger.isTraceEnabled()) {
					logger.trace("Testing if argument resolver [" + methodArgumentResolver + "] supports [" +
							parameter.getGenericParameterType() + "]");
				}
				if (methodArgumentResolver.supportsParameter(parameter)) {
					result = methodArgumentResolver;
					this.argumentResolverCache.put(parameter, result);
					break;
				}
			}
		}
		return result;
	}

先不去追究细节,这两个方法所表达出来的意思很明了。先根据Controller中的Method中的参数,来获取到对应的解析器,也就是HandlerMethodArgumentResolver的一个实例。然后用这个解析器来解析参数。
其中,获取解析器的时候,会先从缓存中获取,如果缓存中没有,那么就遍历所有的解析器,找到一款能够解析这个参数的解析器,并存入缓存中。
对于@RequestBody的解析器,可以先看看HandlerMethodArgumentResolver有哪些实现类,发现有很多种解析器,包括我们常用的一些@RequestParam,@RequestHeader等等

其中想要去看到就是框红的那个。进入这个类中,找到resolveArgument方法:

      /**
	 * Throws MethodArgumentNotValidException if validation fails.
	 * @throws HttpMessageNotReadableException if {@link RequestBody#required()}
	 * is {@code true} and there is no body content or if there is no suitable
	 * converter to read the content with.
	 */
	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		parameter = parameter.nestedIfOptional();
		Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
		String name = Conventions.getVariableNameForParameter(parameter);

		if (binderFactory != null) {
			WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
			if (arg != null) {
				validateIfApplicable(binder, parameter);
				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
					throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
				}
			}
			if (mavContainer != null) {
				mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
			}
		}

		return adaptArgumentIfNecessary(arg, parameter);

这个方法也是有两步,先readWithMessageConverters解析参数,后面就是对这个参数进行校验,比如你使用了require=true,或者@Valid/@Validate。
接着readWithMessageConverters往里走,来到了AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters中,其中对于我想要了解的东西,最关键的一行代码就是

message = new EmptyBodyCheckingHttpInputMessage(inputMessage);

在这个构造函数里面可以看到

		public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
			this.headers = inputMessage.getHeaders();
			InputStream inputStream = inputMessage.getBody();
			if (inputStream.markSupported()) {
				inputStream.mark(1);
				this.body = (inputStream.read() != -1 ? inputStream : null);
				inputStream.reset();
			}
			else {
				PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
				int b = pushbackInputStream.read();
				if (b == -1) {
					this.body = null;
				}
				else {
					this.body = pushbackInputStream;
					pushbackInputStream.unread(b);
				}
			}
		}

这里面就对输入流进行了包装处理,流程图:

其中PushbackInputStream,就是增加了一个unread功能。read是往前读一个字节,而unread就是往后读一个自己。

三、结论

  1. request.getParameter()不会对@RequestBody的解析造成影响,因为这完全是两种获取参数的方式,两个赛道。对于POST请求而言,getParameter是解析application/x-www-form-urlencoded类型的参数,而@RequestBody是解析application/json类型的参数

  2. 一般情况下,假如你在过滤器或任何@RequestBody解析之前的地方,读完了请求流,那么@RequestBody是获取不到参数内容的。

  3. 因此对于需要可重复读的请求流,一般网上也给了方案,对Request进行一层包装,且要覆写其中的getInputStream方法,这样才能随便通过getInputStream来读请求流。

posted @ 2021-03-12 16:46  Bencakes  阅读(3278)  评论(0编辑  收藏  举报