一,前言
談起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實現會導致預設的轉換器丟失需要特別注意。