說說SpringMVC從http流到Controller介面引數的轉換過程

泥粑發表於2021-03-12

一,前言

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

相關文章