說說SpringMVC從http流到Controller介面參數的轉換過程
一,前言
談起springMVC框架介面請求過程大部分人可能會這樣回答:負責將請求分發給對應的handler,然後handler會去調用實際的介面。核心功能是這樣的,但是這樣的回答未免有些草率。面試過很多人,大家彷佛約定好了的一般,給的都是這樣”泛泛”的標準答案。最近開發遇到了這樣的兩個場景:
- 1>,上游的回調介面要求接受類型為application/x-www-form-urlencode,請求方式post,接受消息為xml文本。
- 2>,對接系統動態生成文件(文件實時變更,採用chunk編碼),導致業務系統無法預覽文件(瀏覽器會直接下載),採用中轉介面對文件流進行轉發。
針對上述需求,如何開發rest風格的介面解決呢?
二、request的生命周期
我們知道,當一個請求到達後端web應用(mvc架構的應用)監聽的埠, 率先被攔截器攔截到,然後轉交到對應的介面。我們知道底層的數據必定是數據流形式的,那麼他是怎麼把流轉成介面需要的參數,從而發起調用的呢?此時我們便需要去研究DispathServlet的處理邏輯了。
2.1 DispatchServlet具備的職能
- handler 容器
- handler 前、後置處理器
- 請求轉發(交由HandlerApdater.handler()執行)
- 響應結果轉發
具體入口程式碼如下(DipatchServlet.doDispatch):
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 找到與請求匹配的handler
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 找到與請求匹配的HandlerAdpater
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// ... 省略部分程式碼
// handler 前置處理器
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// handler 調用: 會實際調用到我們的controller介面
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// handler 後置處理
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 返回結果分發
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
// 省略部分程式碼
}
}
這個介面就是我們尋常所說的handler的轉發邏輯。但是我們也知道了實際上去調用我們controller介面的是HandlerAdapter
2.2 HandlerAdapter具備的職能
從上述我們知道了請求的轉發過程,現在我們要弄清楚handler怎麼調用到我們的controller介面的(以RequestMappingHandlerAdapter為例)。
- argumentResolvers 參數解析器,提供了supportsParameter()、resolveArgument()兩個方法來告訴容器是否能解析該參數以及怎麼解析
- returnValueHandlers 返回值解析器,
- modelAndViewResolvers 模型視圖解析器
- messageConverters 消息轉換器,
跟蹤源碼發現(RequestMappingHandlerAdapter.invokeHandlerMethod()),他調用Controller介面發生再ServletInvocableHandlerMethod.invokeAndHandle()方法。看一下主體邏輯:
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 調用controller介面
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// ... 省略部分程式碼
try {
// 處理返回結果
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}
調用controller介面的方法跟蹤源碼會發現,主要是通過request尋找到正確的參數解析器,然後去解析參數,這裡我們以@RequestBody標註的參數為例,看其是如何解析的:
(RequestResponseBodyMethodProcessor.readWithMessageConverters())
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
MediaType contentType;
boolean noContentType = false;
//... 省略部分程式碼
EmptyBodyCheckingHttpInputMessage message;
try {
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
if (message.hasBody()) {
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
}
// ... 省略部分程式碼
return body;
}
可以看到其實就是簡單的找到適配的MessageConvert,調用其read方法即可。把參數解析出來之後,發起對controller介面調用。至此從發起請求到落到controller介面的過程就是這樣子的。
2.3 總結從容器接受到請求到交付到controller介面的過程。
上圖較為完整的描述了從http報文位元組流到controller介面java對象的過程,返回的處理是類型的流程不在贅述。
三、總結
有章節二知道了生命周期,我們知道嚴格意義上,對於問題一,我們只需要定義一個HandlerMethodArgumentResolver去專門解析類似參數(實際上我們用@RequestBody修飾的參數,那麼只需要定義一個MessageConvert即可),然後注入到容器即可。針對問題二,其實只要不要覆蓋原生的MessageConverts對於文件流的輸出本身SpringMVC就是支援的,但是因為我們通常注入MessageConvert是通過WebMvcConfigurerAdapter實現會導致默認的轉換器丟失需要特別注意。