說說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實現會導致默認的轉換器丟失需要特別注意。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章