學習@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來讀請求流。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM