該系列文件是本人在學習 Spring MVC 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋 Spring MVC 原始碼分析 GitHub 地址 進行閱讀
Spring 版本:5.2.4.RELEASE
該系列其他文件請檢視:《精盡 Spring MVC 原始碼分析 - 文章導讀》
HandlerAdapter 元件
HandlerAdapter 元件,處理器的介面卡。因為處理器 handler
的型別是 Object 型別,需要有一個呼叫者來實現 handler
是怎麼被執行。Spring 中的處理器的實現多變,比如使用者的處理器可以實現 Controller 介面或者 HttpRequestHandler 介面,也可以用 @RequestMapping
註解將方法作為一個處理器等,這就導致 Spring MVC 無法直接執行這個處理器。所以這裡需要一個處理器介面卡,由它去執行處理器
由於 HandlerMapping 元件涉及到的內容較多,考慮到內容的排版,所以將這部分內容拆分成了五個模組,依次進行分析:
- 《HandlerAdapter 元件(一)之 HandlerAdapter》
- 《HandlerAdapter 元件(二)之 ServletInvocableHandlerMethod》
- 《HandlerAdapter 元件(三)之 HandlerMethodArgumentResolver》
- 《HandlerAdapter 元件(四)之 HandlerMethodReturnValueHandler》
- 《HandlerAdapter 元件(五)之 HttpMessageConverter》
HandlerAdapter 元件(四)之 HandlerMethodReturnValueHandler
本文是接著《HandlerAdapter 元件(三)之 HandlerMethodArgumentResolver》一文來分享 HandlerMethodReturnValueHandler 元件。在 HandlerAdapter
執行處理器的過程中,具體的執行過程交由 ServletInvocableHandlerMethod
物件來完成,其中需要先通過 HandlerMethodArgumentResolver
引數解析器從請求中解析出方法的入參,然後再通過反射機制呼叫對應的方法,獲取到執行結果後需要通過 HandlerMethodReturnValueHandler 結果處理器來進行處理。
回顧
先來回顧一下 ServletInvocableHandlerMethod
在哪裡呼叫返回值處理器的,可以回到 《HandlerAdapter 元件(二)之 ServletInvocableHandlerMethod》 中 ServletInvocableHandlerMethod 小節下面的 invokeAndHandle
方法,如下:
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// <1> 執行呼叫
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// <2> 設定響應狀態碼
setResponseStatus(webRequest);
// <3> 設定 ModelAndViewContainer 為請求已處理,返回,和 @ResponseStatus 註解相關
if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
} else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}
// <4> 設定 ModelAndViewContainer 為請求未處理
mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
// <5> 處理返回值
this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
} catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}
<5>
處呼叫returnValueHandlers
對返回結果進行處理returnValueHandlers
為 HandlerMethodReturnValueHandlerComposite 組合物件,包含了許多的結果處理器
HandlerMethodReturnValueHandler 介面
org.springframework.web.method.support.HandlerMethodReturnValueHandler
,返回結果處理器
public interface HandlerMethodReturnValueHandler {
/**
* 是否支援該型別
*/
boolean supportsReturnType(MethodParameter returnType);
/**
* 處理返回值
*/
void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}
類圖
因為返回結果型別是多變的,所以會有許多的 HandlerMethodReturnValueHandler 的實現類,上圖僅列出了本文會分析的兩個實現類
ModelAndViewContainer
org.springframework.web.method.support.ModelAndViewContainer
,主要是作為 Model 和 View 的容器
構造方法
public class ModelAndViewContainer {
/**
* 是否在 redirect 重定向時,忽略 {@link #redirectModel}
*/
private boolean ignoreDefaultModelOnRedirect = false;
/**
* 檢視,Object 型別。
*
* 實際情況下,也可以是 String 型別的邏輯檢視
*/
@Nullable
private Object view;
/**
* 預設使用的 Model 。實際上是個 Map
*/
private final ModelMap defaultModel = new BindingAwareModelMap();
/**
* redirect 重定向的 Model ,在重定向時使用。
*/
@Nullable
private ModelMap redirectModel;
/**
* 處理器返回 redirect 檢視的標識
*/
private boolean redirectModelScenario = false;
/**
* Http 響應狀態
*/
@Nullable
private HttpStatus status;
private final Set<String> noBinding = new HashSet<>(4);
private final Set<String> bindingDisabled = new HashSet<>(4);
/**
* 用於設定 SessionAttribute 的標識
*/
private final SessionStatus sessionStatus = new SimpleSessionStatus();
/**
* 請求是否處理完的標識
*/
private boolean requestHandled = false;
}
getModel
getModel()
方法,獲得 Model 物件。程式碼如下:
public ModelMap getModel() {
// 是否使用預設 Model
if (useDefaultModel()) {
return this.defaultModel;
}
else {
if (this.redirectModel == null) {
this.redirectModel = new ModelMap();
}
return this.redirectModel;
}
}
/**
* Whether to use the default model or the redirect model.
*/
private boolean useDefaultModel() {
return (!this.redirectModelScenario || (this.redirectModel == null && !this.ignoreDefaultModelOnRedirect));
}
-
從程式碼中,可以看出,有兩種情況下,使用
defaultModel
預設 Model :- 情況一
!this.redirectModelScenario
,處理器返回 redirect 檢視的標識為false
的時候,即不重定向 - 情況二
this.redirectModel == null && !this.ignoreDefaultModelOnRedirect
,redirectModel
重定向 Model 為空,並且ignoreDefaultModelOnRedirect
為true
,即忽略defaultModel
- 情況一
-
那麼,問題就來了,redirectModelScenario 和 ignoreDefaultModelOnRedirect 什麼時候被改變?
-
redirectModelScenario
屬性,在下文的 ViewNameMethodReturnValueHandler的handleReturnValue方法中會設定為true
,詳情見下文 -
ignoreDefaultModelOnRedirect
屬性,和 RequestMappingHandlerAdapter 的ignoreDefaultModelOnRedirect
的屬性是一致的,預設為false
在 RequestMappingHandlerAdapter 的
invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod)
方法中,進行設定
-
-
另外,
org.springframework.ui.ModelMap
是繼承 LinkedHashMap 類,並增加了部分常用方法,比較簡單
View 相關的方法
public void setViewName(@Nullable String viewName) {
this.view = viewName;
}
@Nullable
public String getViewName() {
return (this.view instanceof String ? (String) this.view : null);
}
public void setView(@Nullable Object view) {
this.view = view;
}
@Nullable
public Object getView() {
return this.view;
}
public boolean isViewReference() {
return (this.view instanceof String);
}
requestHandled 屬性
請求是否處理完的標識
關於 requestHandled
的修改地方,實際在 Spring MVC 地方蠻多處都可以進行修改,例如:
-
在本文的開始處,
ServletInvocableHandlerMethod
物件的invokeAndHandle
方法中,會先設定為false
,表示請求還未處理,再交由 HandlerMethodReturnValueHandler 結果處理器去處理 -
在後文的
RequestResponseBodyMethodProcessor
的handleReturnValue
會設定為true
處理完結果後,接下來 RequestMappingHandlerAdapter
需要通過 ModelAndViewContainer
獲取 ModelAndView
物件,會用到 requestHandled
這個屬性
// RequestMappingHandlerAdapter.java
@Nullable
private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
modelFactory.updateModel(webRequest, mavContainer);
// 情況一,如果 mavContainer 已處理,則返回“空”的 ModelAndView 物件。
if (mavContainer.isRequestHandled()) {
return null;
}
// 情況二,如果 mavContainer 未處理,則基於 `mavContainer` 生成 ModelAndView 物件
ModelMap model = mavContainer.getModel();
// 建立 ModelAndView 物件,並設定相關屬性
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
if (model instanceof RedirectAttributes) {
Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request != null) {
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
}
return mav;
}
看到沒,如果已處理,則返回的 ModelAndView
物件為 null
HandlerMethodReturnValueHandlerComposite
org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite
,實現 HandlerMethodReturnValueHandler 介面,複合的 HandlerMethodReturnValueHandler 實現類
構造方法
public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler {
/** HandlerMethodReturnValueHandler 陣列 */
private final List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<>();
}
在《HandlerAdapter 元件(一)之 HandlerAdapter》的RequestMappingHandlerAdapter小節的 getDefaultReturnValueHandlers
方法中可以看到,預設的 returnValueHandlers
有哪些 HandlerMethodReturnValueHandler 實現類,注意這裡是有順序的新增哦
getReturnValueHandler
getReturnValueHandler(MethodParameter returnType)
方法,獲得方法返回值對應的 HandlerMethodReturnValueHandler 物件,方法如下:
@Nullable
private HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) {
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
很簡單,遍歷所有的 HandlerMethodReturnValueHandler 實現類,如果支援這個返回結果,則直接返回
這裡為什麼不加快取呢?
supportsReturnType
supportsReturnType(MethodParameter returnType)
方法,判斷是否支援該返回型別,方法如下:
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return getReturnValueHandler(returnType) != null;
}
實際上就是呼叫 getReturnValueHandler(MethodParameter returnType)
方法,存在對應的 HandlerMethodReturnValueHandler 實現類表示支援
handleReturnValue
handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
方法,處理返回值,方法如下:
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
// <x> 獲得 HandlerMethodReturnValueHandler 物件
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
這裡好奇的是沒有呼叫 getReturnValueHandler(MethodParameter returnType)
方法獲取對應的 HandlerMethodReturnValueHandler 物件,而是呼叫 selectHandler(Object value, MethodParameter returnType)
方法,方法如下:
@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
// 判斷是否為非同步返回值
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
// 遍歷 HandlerMethodReturnValueHandler 陣列,逐個判斷是否支援
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
// 如果支援,則返回
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
private boolean isAsyncReturnValue(@Nullable Object value, MethodParameter returnType) {
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (handler instanceof AsyncHandlerMethodReturnValueHandler &&
((AsyncHandlerMethodReturnValueHandler) handler).isAsyncReturnValue(value, returnType)) {
return true;
}
}
return false;
}
在 getReturnValueHandler(MethodParameter returnType)
的基礎上,增加了非同步處理器 AsyncHandlerMethodReturnValueHandler 的判斷
【重點】RequestResponseBodyMethodProcessor
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor
,繼承 AbstractMessageConverterMethodProcessor 抽象類,處理方法引數新增了 @RequestBody
註解方法入參,或者處理方法新增了 @ResponseBody
註解的返回值。
因為前後端分離之後,後端基本是提供 Restful API ,所以 RequestResponseBodyMethodProcessor 成為了目前最常用的 HandlerMethodReturnValueHandler 實現類。
從圖中,我們也會發現,RequestResponseBodyMethodProcessor 也是 HandlerMethodArgumentResolver 的實現類。示例程式碼:
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/walks")
public List<User> walk(@RequestBody User user) {
List<User> users = new ArrayList();
users.add(new User().setUsername("nihao"));
users.add(new User().setUsername("zaijian"));
return users;
}
}
雖然,walks()
方法的返回值沒新增 @ResponseBody
註解,但是 @RestController
註解,預設有 @ResponseBody
註解
構造方法
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters) {
super(converters);
}
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
@Nullable ContentNegotiationManager manager) {
super(converters, manager);
}
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
@Nullable List<Object> requestResponseBodyAdvice) {
super(converters, null, requestResponseBodyAdvice);
}
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
@Nullable ContentNegotiationManager manager, @Nullable List<Object> requestResponseBodyAdvice) {
super(converters, manager, requestResponseBodyAdvice);
}
}
-
converters
引數,HttpMessageConverter 陣列。關於 HttpMessageConverter,就是將返回結果設定到響應中,供客戶端獲取。例如,我們想要將 POJO 物件,返回成 JSON 資料給前端,就會使用到 MappingJackson2HttpMessageConverter 類。 -
requestResponseBodyAdvice
引數,在父類 AbstractMessageConverterMethodArgumentResolver 中會將其轉換成 RequestResponseBodyAdviceChain 物件advice
,不知你是否還記得這個引數,來回顧一下:在《HandlerAdapter 元件(一)之 HandlerAdapter》的RequestMappingHandlerAdapter的1.afterPropertiesSet 初始化方法中,第一步就會初始化所有 ControllerAdvice 相關的類
然後在1.4 getDefaultReturnValueHandlers方法中,建立 RequestResponseBodyMethodProcessor 處理器時,會傳入
requestResponseBodyAdvice
引數
supportsParameter
實現 supportsParameter(MethodParameter returnType)
方法,判斷是否支援處理該方法引數,方法如下:
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 該引數是否有 @RequestBody 註解
return parameter.hasParameterAnnotation(RequestBody.class);
}
該方法引數是否有 @RequestBody
註解
resolveArgument
實現 resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory)
方法,從請求中解析出帶有 @RequestBody
註解的引數,方法如下:
@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());
}
}
// 返回方法入參物件,如果有必要,則通過 Optional 獲取對應的方法入參
return adaptArgumentIfNecessary(arg, parameter);
}
呼叫readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter, Type paramType)
方法,從請求體中解析出方法入參物件
【核心】readWithMessageConverters
從請求體中解析出方法入參,方法如下:
@Override
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
// <1> 建立 ServletServerHttpRequest 請求物件
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
// <2> 讀取請求體中的訊息並轉換成入參物件
Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
// <3> 校驗方法入參物件
if (arg == null && checkRequired(parameter)) {
throw new HttpMessageNotReadableException("Required request body is missing: " +
parameter.getExecutable().toGenericString(), inputMessage);
}
return arg;
}
// AbstractMessageConverterMethodArgumentResolver.java
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
// <1> 獲取使用的 MediaType 物件
MediaType contentType;
boolean noContentType = false;
try {
// <1.1> 從請求頭中獲取 "Content-Type"
contentType = inputMessage.getHeaders().getContentType();
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
if (contentType == null) {
noContentType = true;
// <1.2> 為空則預設為 application/octet-stream
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
// <2> 獲取方法引數的 containing class 和 目標型別,用於 HttpMessageConverter 解析
Class<?> contextClass = parameter.getContainingClass();
Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
if (targetClass == null) {
// 如果為空,則從方法引數中解析出來
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
targetClass = (Class<T>) resolvableType.resolve();
}
// <3> 獲取 HTTP 方法
HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
Object body = NO_VALUE;
// <4> 開始從請求中解析方法入參
EmptyBodyCheckingHttpInputMessage message;
try {
// <4.1> 將請求訊息物件封裝成 EmptyBodyCheckingHttpInputMessage,用於校驗是否有請求體,沒有的話設定為 `null`
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
// <4.2> 遍歷 HttpMessageConverter
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter = (converter instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
// 如果該 HttpMessageConverter 能夠讀取當前請求體解析出方法入參
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
// <4.2.1> 如果請求體不為空
if (message.hasBody()) {
HttpInputMessage msgToUse = getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
// 通過該 HttpMessageConverter 從請求體中解析出方法入參物件
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
// 呼叫 RequestResponseBodyAdvice 的 afterBodyRead 方法,存在 RequestBodyAdvice 則對方法入參進行修改
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
// <4.2.2> 如果請求體為空,則無需解析請求體
else {
// 呼叫 RequestResponseBodyAdvice 的 afterBodyRead 方法,存在 RequestBodyAdvice 則對方法入參進行修改
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
}
// <5> 校驗解析出來的方法入參物件是否為空
if (body == NO_VALUE) {
if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
(noContentType && !message.hasBody())) {
return null;
}
throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
}
// 列印日誌
MediaType selectedContentType = contentType;
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
return "Read \"" + selectedContentType + "\" to [" + formatted + "]";
});
// <6> 返回方法入參物件
return body;
}
我們直接看到父類 AbstractMessageConverterMethodArgumentResolver 的 readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,Type targetType)
這個核心方法,大致邏輯如下:
-
獲取使用的 MediaType 物件
contentType
- 從請求頭中獲取
Content-Type
- 請求頭中沒有則設定為預設的型別
application/octet-stream
- 從請求頭中獲取
-
獲取方法引數的
containing class
和targetClass 目標型別
,用於 HttpMessageConverter 解析 -
獲取 HTTP 方法
-
開始從請求中解析方法入參
Object body
-
將請求訊息物件封裝成 EmptyBodyCheckingHttpInputMessage,用於校驗是否有請求體,沒有的話設定為
null
-
遍歷所有的 HttpMessageConverter 實現類,呼叫其
canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType)
方法,判斷當前 HttpMessageConverter 實現類是否支援解析該方法入參,如果返回true
,則使用該 HttpMessageConverter 實現類進行解析- 如果請求體不為空,則通過該 HttpMessageConverter 從請求體中解析出方法入參物件
- 如果請求體為空,則無需解析請求體
注意:上面不管請求體是否為空,都會呼叫
RequestResponseBodyAdvice
的afterBodyRead
方法,存在 RequestBodyAdvice 則對方法入參進行修改
-
-
校驗解析出來的方法入參物件是否為空,丟擲異常或者返回
null
-
返回方法入參物件
body
方法雖然很長,但是不難理解,大致邏輯就是找到合適的 HttpMessageConverter 實現類從請求體中獲取到方法入參物件
邏輯和下面的 writeWithMessageConverters
差不多,我們重點來看到下面這個方法?
supportsReturnType
實現 supportsReturnType(MethodParameter returnType)
方法,判斷是否支援處理該返回型別,方法如下:
@Override
public boolean supportsReturnType(MethodParameter returnType) {
// 該方法或者所在類是否有 @ResponseBody 註解
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
returnType.hasMethodAnnotation(ResponseBody.class));
}
該方法或者所在類是否有 @ResponseBody
註解
handleReturnValue
實現 handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
方法,方法如下:
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
// <1> 設定已處理
mavContainer.setRequestHandled(true);
// <2> 建立請求和響應
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
// <3> 使用 HttpMessageConverter 對物件進行轉換,並寫入到響應
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
-
設定
mavContainer
已處理,也就是修改它的requestHandled
屬性為true
,表示請求已處理,後續獲取到的ModelAndView
物件就為null
-
建立請求和響應,這裡是獲取到
javax.servlet.http.HttpServletRequest
和javax.servlet.http.HttpServletResponse
,然後分別封裝成org.springframework.http.server.ServletServerHttpRequest
和org.springframework.http.server.ServletServerHttpResponse
物件,便於從請求中獲取資料,往響應中設定資料 -
呼叫父類 AbstractMessageConverterMethodProcessor 的
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage)
方法,使用 HttpMessageConverter 對物件進行轉換,並寫入到響應
不知你是否還記得 HttpMessageConverter 是在哪兒會初始化呢?我們來回顧一下
回到《HandlerAdapter 元件(一)之 HandlerAdapter》的RequestMappingHandlerAdapter的構造方法中,預設會新增了四個 HttpMessageConverter 物件。當然,預設還會新增其他的,例如 MappingJackson2HttpMessageConverter 為 JSON 訊息格式的轉換器,至於其他 HttpMessageConverter 實現類如何新增的,本文就不分析了,你知道就行?
然後在 1.4 getDefaultReturnValueHandlers 方法中,建立 RequestResponseBodyMethodProcessor 處理器時,會傳入
getMessageConverters()
引數,也就是獲取所有的 HttpMessageConverter 實現類,所以在下面這個方法就需要用到
【核心】writeWithMessageConverters
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage)
方法,使用 HttpMessageConverter 物件進行轉換,並寫入到響應,方法如下:
// AbstractMessageConverterMethodProcessor.java
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
// <1> 獲得 body、valueType、targetType
Object body;
Class<?> valueType;
Type targetType;
if (value instanceof CharSequence) { // 如果是字串則直接賦值
body = value.toString();
valueType = String.class;
targetType = String.class;
}
else {
body = value;
// 獲取返回結果的型別(返回值 body 不為空則直接獲取其型別,否則從返回結果型別 returnType 獲取其返回值型別)
valueType = getReturnValueType(body, returnType);
// 獲取泛型
targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
}
// <2> 是否為 Resource 型別
if (isResourceType(value, returnType)) {
// 設定響應頭 Accept-Ranges
outputMessage.getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes");
// 資料不為空、請求頭中的 Range 不為空、響應碼為 200
if (value != null && inputMessage.getHeaders().getFirst(HttpHeaders.RANGE) != null
&& outputMessage.getServletResponse().getStatus() == 200) {
Resource resource = (Resource) value;
try {
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
// 斷點續傳,客戶端已下載一部分資料,此時需要設定響應碼為 206
outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());
// 獲取哪一段資料需返回
body = HttpRange.toResourceRegions(httpRanges, resource);
valueType = body.getClass();
targetType = RESOURCE_REGION_LIST_TYPE;
}
catch (IllegalArgumentException ex) {
outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength());
outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
}
}
}
// <3> 選擇使用的 MediaType
MediaType selectedMediaType = null;
// <3.1> 獲得響應中的 ContentType 的值
MediaType contentType = outputMessage.getHeaders().getContentType();
// <3.1.1> 如果存在 ContentType 的值,並且不包含萬用字元,則使用它作為 selectedMediaType
if (contentType != null && contentType.isConcrete()) {
if (logger.isDebugEnabled()) {
logger.debug("Found 'Content-Type:" + contentType + "' in response");
}
selectedMediaType = contentType;
}
else {
HttpServletRequest request = inputMessage.getServletRequest();
// <3.2.1> 從請求中,獲得可接受的 MediaType 陣列。預設實現是,從請求頭 ACCEPT 中獲取
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
// <3.2.2> 獲得可產生的 MediaType 陣列
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
// <3.2.3> 如果 body 非空,並且無可產生的 MediaType 陣列,則丟擲 HttpMediaTypeNotAcceptableException 異常
if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
}
// <3.2.4> 通過 acceptableTypes 來比對,將符合的 producibleType 新增到 mediaTypesToUse 結果陣列中
List<MediaType> mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
// <3.2.5> 如果沒有符合的,並且 body 非空,則丟擲 HttpMediaTypeNotAcceptableException 異常
if (mediaTypesToUse.isEmpty()) {
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(producibleTypes);
}
if (logger.isDebugEnabled()) {
logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
}
return;
}
// <3.2.6> 按照 MediaType 的 specificity 和 quality 排序
MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
// <3.2.7> 選擇其中一個最匹配的,主要考慮不包含萬用字元的,例如 application/json;q=0.8
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
if (logger.isDebugEnabled()) {
logger.debug("Using '" + selectedMediaType + "', given " +
acceptableTypes + " and supported " + producibleTypes);
}
}
// <4> 如果匹配到,則進行寫入邏輯
if (selectedMediaType != null) {
// <4.1> 移除 quality 。例如,application/json;q=0.8 移除後為 application/json
selectedMediaType = selectedMediaType.removeQualityValue();
// <4.2> 遍歷 messageConverters 陣列
for (HttpMessageConverter<?> converter : this.messageConverters) {
// <4.3> 判斷 HttpMessageConverter 是否支援轉換目標型別
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter
? (GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType)
: converter.canWrite(valueType, selectedMediaType)) {
// <5.1> 如果有 RequestResponseBodyAdvice,則可能需要對返回的結果做修改
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
// <5.2> body 非空,則進行寫入
if (body != null) {
// 列印日誌
Object theBody = body; // 這個變數的用途是,列印是匿名類,需要有 final
LogFormatUtils.traceDebug(logger, traceOn -> "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
// 新增 CONTENT_DISPOSITION 頭,一般情況下用不到
addContentDispositionHeader(inputMessage, outputMessage);
// <5.3> 寫入內容
if (genericConverter != null) {
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}
else {
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
// <5.4> return 返回,結束整個邏輯
return;
}
}
}
// <6> 如果到達此處,並且 body 非空,說明沒有匹配的 HttpMessageConverter 轉換器,則丟擲 HttpMediaTypeNotAcceptableException 異常
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
}
}
方法有點長,慢慢來看,核心邏輯簡單?
<1>
處,獲得 body
、valueType
、targetType
三個屬性,例如上面提供的示例,三個值分對應users返回結果
、ArrayList 型別
、User 型別
<2>
處,呼叫 isResourceType(Object value, MethodParameter returnType)
方法,判斷是否為 Resource 型別,方法如下:
// AbstractMessageConverterMethodProcessor.java
protected boolean isResourceType(@Nullable Object value, MethodParameter returnType) {
Class<?> clazz = getReturnValueType(value, returnType);
return clazz != InputStreamResource.class && Resource.class.isAssignableFrom(clazz);
}
設定響應頭 Accept-Ranges 為 "bytes",如果資料不為空,且請求頭中的 Range 不為空,且響應碼為 200,則設定狀態碼為 206(斷點續傳,客戶端已下載一部分資料),這裡不做過多的講述
========== 第一步 ==========
- 選擇使用的 MediaType 物件
selectedMediaType
- 獲得響應中的 ContentType 的值,如果存在 ContentType 的值,並且不包含萬用字元,則使用它作為
selectedMediaType
- 否則,從請求中找到合適的 MediaType 物件
- 從請求中,獲得可接受的 MediaType 陣列
acceptableTypes
。預設實現是,從請求頭 ACCEPT 中獲取 - 獲得可產生的 MediaType 陣列
producibleTypes
- 如果
body
非空,並且無可產生的 MediaType 陣列producibleTypes
,則丟擲 HttpMediaTypeNotAcceptableException 異常 - 通過
acceptableTypes
來比對,將符合的producibleType
新增到mediaTypesToUse
結果陣列中 - 如果沒有符合的,並且
body
非空,則丟擲 HttpMediaTypeNotAcceptableException 異常 - 按照 MediaType 的 specificity 和 quality 排序(權重),對
mediaTypesToUse
進行排序 - 選擇其中一個最匹配的,主要考慮不包含萬用字元的,例如
application/json;q=0.8
- 從請求中,獲得可接受的 MediaType 陣列
- 獲得響應中的 ContentType 的值,如果存在 ContentType 的值,並且不包含萬用字元,則使用它作為
========== 第二步 ==========
- 如果匹配到 MediaType 物件
selectedMediaType
不為空,則進行寫入邏輯- 移除 quality 。例如,
application/json;q=0.8
移除後為application/json
- 遍歷
messageConverters
陣列,也就是所有的 HttpMessageConverter 實現類 - 判斷當前 HttpMessageConverter 是否支援轉換目標型別,呼叫其
canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType)
方法進行判斷
- 移除 quality 。例如,
========== 第三步:寫入響應體==========
-
如果
4.3
的結果為true
,表示當前 HttpMessageConverter 實現類可以處理該返回型別-
呼叫
RequestResponseBodyAdvice
的beforeBodyWrite
方法,存在 ResponseBodyAdvice 則對返回的結果進行修改// RequestResponseBodyAdviceChain.java @Override @Nullable public Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType contentType, Class<? extends HttpMessageConverter<?>> converterType, ServerHttpRequest request, ServerHttpResponse response) { return processBody(body, returnType, contentType, converterType, request, response); } @Nullable private <T> Object processBody(@Nullable Object body, MethodParameter returnType, MediaType contentType, Class<? extends HttpMessageConverter<?>> converterType, ServerHttpRequest request, ServerHttpResponse response) { for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) { if (advice.supports(returnType, converterType)) { body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType, contentType, converterType, request, response); } } return body; }
就是你新增了@ControllerAdvice註解的 ResponseBodyAdvice 實現類在這裡會被呼叫
-
body
非空,則進行寫入,如果沒有Content-Disposition
請求頭,則嘗試新增一個,關於檔案相關的內容 -
呼叫當前 HttpMessageConverter 實現類的
write(T t, @Nullable Type type, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
方法,將body
寫入響應中 -
return
返回,結束整個邏輯
-
-
到達了此處,說明第
4
步 沒有找到對應的 MediaType 物件,或者第5
步沒有一個 HttpMessageConverter 實現類支援處理該返回結果如果
body
不為空,也就是說有返回值但是沒有處理,則丟擲 HttpMediaTypeNotAcceptableException 異常
雖然上面的方法很長,但是不難理解,大致邏輯就是找到合適的 HttpMessageConverter 實現類去將返回結果寫入到響應體中?
但是 HttpMessageConverter 怎麼才合適,怎麼寫入到響應體中,沒有展開討論,涉及到的內容不少,就在下一篇文件《HandlerAdapter 元件(五)之 HttpMessageConverter》中分析吧
ViewNameMethodReturnValueHandler
org.springframework.web.servlet.mvc.method.annotation.ViewNameMethodReturnValueHandler
,實現 HandlerMethodReturnValueHandler 介面,處理返回結果是檢視名的 ReturnValueHandler 實現類。
ViewNameMethodReturnValueHandler 適用於前後端未分離,Controller 返回檢視名的場景,例如 JSP、Freemarker 等等。
構造方法
public class ViewNameMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
/**
* 重定向的表示式的陣列
*/
@Nullable
private String[] redirectPatterns;
protected boolean isRedirectViewName(String viewName) {
// 符合 redirectPatterns 表示式,或者以 redirect: 開頭
return (PatternMatchUtils.simpleMatch(this.redirectPatterns, viewName) || viewName.startsWith("redirect:"));
}
}
redirectPatterns
:重定向的表示式的陣列,用於判斷某個檢視是否為重定向的檢視,一般情況下,不進行設定。
可以看到isRedirectViewName(String viewName)
方法,判斷某個檢視是否為重定向的檢視,如果檢視名以 redirect:
開頭,也是重定向的檢視
supportsReturnType
實現 supportsReturnType(MethodParameter returnType)
方法,判斷是否支援處理該返回型別,方法如下:
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> paramType = returnType.getParameterType();
return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));
}
該方法的返回型別是否為void
或者字串
你是否會疑惑?如果我返回的是字串,想要使用 RequestResponseBodyMethodProcessor 怎麼辦,不會有問題嗎?
回到《HandlerAdapter 元件(一)之 HandlerAdapter》的RequestMappingHandlerAdapter的1.4 getDefaultReturnValueHandlers方法中,如下:
private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() { List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(); // ... 省略其他 HandlerMethodReturnValueHandler 實現類的新增 handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice)); // Multi-purpose return value types handlers.add(new ViewNameMethodReturnValueHandler()); // ... 省略其他 HandlerMethodReturnValueHandler 實現類的新增 return handlers; }
RequestResponseBodyMethodProcessor 是在 ViewNameMethodReturnValueHandler 之前新增的,所以不會出現上述問題
handleReturnValue
實現 handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
方法,程式碼如下:
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
// 如果是 String 型別
if (returnValue instanceof CharSequence) {
// 設定檢視名到 mavContainer 中
String viewName = returnValue.toString();
mavContainer.setViewName(viewName);
// 如果是重定向,則標記到 mavContainer 中
if (isRedirectViewName(viewName)) {
mavContainer.setRedirectModelScenario(true);
}
}
// 如果是非 String 型別,而且非 void ,則丟擲 UnsupportedOperationException 異常
else if (returnValue != null) {
// should not happen
throw new UnsupportedOperationException("Unexpected return type: " +
returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
}
}
- 如果返回結果是
String
型別,則作為檢視名設定到mavContainer
中 - 如果是重定向,則標記到
mavContainer
中的redirectModelScenario
屬性中為true
注意,此時 mavContainer
的 requestHandled
屬性,並未並未像 RequestResponseBodyMethodProcessor 一樣,設定為 true
表示請求已處理
這是為什麼呢?因為返回結果是檢視名的場景下,需要使用 ViewResolver 從 ModelAndView 物件中解析出其對應的檢視 View 物件,然後執行 View#render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
方法,進行渲染。如果你設定為 true
,在後續獲取到的 ModelAndView
物件就為null
了,無法渲染檢視
總結
在 HandlerAdapter
執行 HandlerMethod
處理器的過程中,會將該處理器封裝成 ServletInvocableHandlerMethod
物件,通過該物件來執行處理器。該物件通過反射機制呼叫對應的方法,在呼叫方法之前,藉助 HandlerMethodArgumentResolver
引數解析器從請求中獲取到對應的方法引數值,在呼叫方法之後,需要藉助於HandlerMethodReturnValueHandler 返回值處理器將返回結果設定到響應中,或者設定相應的 Model 和 View 用於後續的檢視渲染。
HandlerMethodReturnValueHandler 返回值處理器的實現類非常多,採用了組合模式來進行處理,如果有某一個返回值處理器支援處理該返回值型別,則使用它對返回結果進行處理,例如將返回結果寫入響應體中。注意,這裡有一定的先後順序,因為是通過 ArrayList 儲存所有的實現類,排在前面的實現類則優先處理。
本文分析了我們常用的 @ResponseBody
註解和前後端未分離時返回檢視名兩種處理方式,對應的 HandlerMethodReturnValueHandler 實現類,如下:
RequestResponseBodyMethodProcessor
:處理方法引數新增了@RequestBody
註解方法入參,或者處理方法新增了@ResponseBody
註解的返回值。在前後端分離之後,後端基本是提供 Restful API ,所以這種方式成為了目前最常用的 HandlerMethodReturnValueHandler 實現類- 核心邏輯不復雜,主要是通過
HttpMessageConverter
實現類從請求體中獲取方法入參或者將返回結果設定到響應體中,關於HttpMessageConverter
相關內容在下一篇文件《HandlerAdapter 元件(五)之 HttpMessageConverter》中分析 - 在處理返回結果時,會將
ModelAndViewContainer
的requestHandled
屬性設定為true
,表示請求已經處理完成了,後續獲取ModelAndView
物件時直接返回null
,不會進行檢視渲染,也就和前端分離了~
- 核心邏輯不復雜,主要是通過
ViewNameMethodReturnValueHandler
:處理返回結果是檢視名的 HandlerMethodReturnValueHandler實現類- 如果你的方法返回值時
void
或者字串
,該類都可以處理,將你的返回結果直接設定為檢視名 - 這裡不會將
ModelAndViewContainer
的requestHandled
屬性設定為true
,因為後續需要獲取ModelAndView
物件進行檢視渲染
- 如果你的方法返回值時
你是否會疑惑?如果我返回的是字串不是檢視名,被
ViewNameMethodReturnValueHandler
處理了怎麼辦?放心,在
HandlerMethodReturnValueHandlerComposite
中判斷是否支援處理該返回結果中,會遍歷所有的 HandlerMethodReturnValueHandler 實現類,而RequestResponseBodyMethodProcessor
排在ViewNameMethodReturnValueHandler
前面,所以優先交給前者處理。至於為什麼
RequestResponseBodyMethodProcessor
排在前面在本文中已經講過了,因為所有的 HandlerMethodReturnValueHandler 實現類用 ArrayList 集合儲存,RequestResponseBodyMethodProcessor
預設先新增進去?
參考文章:芋道原始碼《精盡 Spring MVC 原始碼分析》