一、背景
研究對象是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注解的參數進行處理。
- 找到打印這行日志的類以及行數, RequestMappingHandlerMapping:547.
發現這個類中總共都沒有547行,那么就去它繼承的父類中去找,結果在AbstractHandlerMethodMapping這個類中找到了對應的行和方法。
public void register(T mapping, Object handler, Method method) {
// 省略
}
從方法名以及參數上來看,肯定是將Controller中每個RequestMapping對應的Method注冊起對應關系。但是入參到底是啥也不清楚,那么就看哪些地方調用了這個方法。
發現了如下的調用鏈路:
AbstractHandlerMethodMapping#initHandlerMethods
AbstractHandlerMethodMapping#detectHandlerMethods
AbstractHandlerMethodMapping#registerHandlerMethod
AbstractHandlerMethodMapping#register
- 那么從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就是往后讀一個自己。
三、結論
-
request.getParameter()不會對@RequestBody的解析造成影響,因為這完全是兩種獲取參數的方式,兩個賽道。對於POST請求而言,getParameter是解析application/x-www-form-urlencoded類型的參數,而@RequestBody是解析application/json類型的參數
-
一般情況下,假如你在過濾器或任何@RequestBody解析之前的地方,讀完了請求流,那么@RequestBody是獲取不到參數內容的。
-
因此對於需要可重復讀的請求流,一般網上也給了方案,對Request進行一層包裝,且要覆寫其中的getInputStream方法,這樣才能隨便通過getInputStream來讀請求流。