精盡Spring MVC原始碼分析 - HandlerAdapter 元件(四)之 HandlerMethodReturnValueHandler

月圓吖發表於2020-12-21

該系列文件是本人在學習 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 元件(四)之 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;
}

類圖

精盡Spring MVC原始碼分析 - HandlerAdapter 元件(四)之 HandlerMethodReturnValueHandler

因為返回結果型別是多變的,所以會有許多的 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.ignoreDefaultModelOnRedirectredirectModel 重定向 Model 為,並且 ignoreDefaultModelOnRedirecttrue ,即忽略 defaultModel
  • 那麼,問題就來了,redirectModelScenario 和 ignoreDefaultModelOnRedirect 什麼時候被改變?

    • redirectModelScenario 屬性,在下文的 ViewNameMethodReturnValueHandlerhandleReturnValue方法中會設定為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 結果處理器去處理

  • 在後文的 RequestResponseBodyMethodProcessorhandleReturnValue 會設定為 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 實現類。

精盡Spring MVC原始碼分析 - HandlerAdapter 元件(四)之 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》RequestMappingHandlerAdapter1.afterPropertiesSet 初始化方法中,第一步就會初始化所有 ControllerAdvice 相關的類

    然後在1.4 getDefaultReturnValueHandlers方法中,建立 RequestResponseBodyMethodProcessor 處理器時,會傳入 requestResponseBodyAdvice 引數

    使用示例可以參考 SpringMVC 中 @ControllerAdvice 註解的三種使用場景

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)這個核心方法,大致邏輯如下:

  1. 獲取使用的 MediaType 物件 contentType

    1. 從請求頭中獲取 Content-Type
    2. 請求頭中沒有則設定為預設的型別 application/octet-stream
  2. 獲取方法引數的 containing classtargetClass 目標型別,用於 HttpMessageConverter 解析

  3. 獲取 HTTP 方法

  4. 開始從請求中解析方法入參Object body

    1. 將請求訊息物件封裝成 EmptyBodyCheckingHttpInputMessage,用於校驗是否有請求體,沒有的話設定為 null

    2. 遍歷所有的 HttpMessageConverter 實現類,呼叫其 canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType)方法,判斷當前 HttpMessageConverter 實現類是否支援解析該方法入參,如果返回 true,則使用該 HttpMessageConverter 實現類進行解析

      1. 如果請求體不為空,則通過該 HttpMessageConverter 從請求體中解析出方法入參物件
      2. 如果請求體為空,則無需解析請求體

      注意:上面不管請求體是否為空,都會呼叫 RequestResponseBodyAdviceafterBodyRead 方法,存在 RequestBodyAdvice 則對方法入參進行修改

  5. 校驗解析出來的方法入參物件是否為空,丟擲異常或者返回null

  6. 返回方法入參物件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);
}
  1. 設定 mavContainer 已處理,也就是修改它的 requestHandled 屬性為 true,表示請求已處理,後續獲取到的 ModelAndView 物件就為 null

  2. 建立請求和響應,這裡是獲取到 javax.servlet.http.HttpServletRequestjavax.servlet.http.HttpServletResponse,然後分別封裝成 org.springframework.http.server.ServletServerHttpRequestorg.springframework.http.server.ServletServerHttpResponse 物件,便於從請求中獲取資料,往響應中設定資料

  3. 呼叫父類 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> 處,獲得 bodyvalueTypetargetType 三個屬性,例如上面提供的示例,三個值分對應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(斷點續傳,客戶端已下載一部分資料),這裡不做過多的講述

========== 第一步 ==========

  1. 選擇使用的 MediaType 物件 selectedMediaType
    1. 獲得響應中的 ContentType 的值,如果存在 ContentType 的值,並且不包含萬用字元,則使用它作為 selectedMediaType
    2. 否則,從請求中找到合適的 MediaType 物件
      1. 從請求中,獲得可接受的 MediaType 陣列 acceptableTypes。預設實現是,從請求頭 ACCEPT 中獲取
      2. 獲得可產生的 MediaType 陣列 producibleTypes
      3. 如果 body 非空,並且無可產生的 MediaType 陣列 producibleTypes,則丟擲 HttpMediaTypeNotAcceptableException 異常
      4. 通過 acceptableTypes 來比對,將符合的 producibleType 新增到 mediaTypesToUse 結果陣列中
      5. 如果沒有符合的,並且 body 非空,則丟擲 HttpMediaTypeNotAcceptableException 異常
      6. 按照 MediaType 的 specificity 和 quality 排序(權重),對mediaTypesToUse 進行排序
      7. 選擇其中一個最匹配的,主要考慮不包含萬用字元的,例如 application/json;q=0.8

========== 第二步 ==========

  1. 如果匹配到 MediaType 物件 selectedMediaType 不為空,則進行寫入邏輯
    1. 移除 quality 。例如,application/json;q=0.8 移除後為 application/json
    2. 遍歷 messageConverters 陣列,也就是所有的 HttpMessageConverter 實現類
    3. 判斷當前 HttpMessageConverter 是否支援轉換目標型別,呼叫其 canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType) 方法進行判斷

========== 第三步:寫入響應體==========

  1. 如果 4.3 的結果為 true,表示當前 HttpMessageConverter 實現類可以處理該返回型別

    1. 呼叫 RequestResponseBodyAdvicebeforeBodyWrite 方法,存在 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 實現類在這裡會被呼叫

    2. body 非空,則進行寫入,如果沒有 Content-Disposition 請求頭,則嘗試新增一個,關於檔案相關的內容

    3. 呼叫當前 HttpMessageConverter 實現類的 write(T t, @Nullable Type type, @Nullable MediaType contentType, HttpOutputMessage outputMessage) 方法,將 body 寫入響應中

    4. return 返回,結束整個邏輯

  2. 到達了此處,說明第 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》RequestMappingHandlerAdapter1.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

注意,此時 mavContainerrequestHandled 屬性,並未並未像 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 實現類
    1. 核心邏輯不復雜,主要是通過 HttpMessageConverter 實現類從請求體中獲取方法入參或者將返回結果設定到響應體中,關於 HttpMessageConverter 相關內容在下一篇文件《HandlerAdapter 元件(五)之 HttpMessageConverter》中分析
    2. 在處理返回結果時,會將 ModelAndViewContainerrequestHandled 屬性設定為 true,表示請求已經處理完成了,後續獲取 ModelAndView 物件時直接返回 null,不會進行檢視渲染,也就和前端分離了~
  • ViewNameMethodReturnValueHandler:處理返回結果是檢視名HandlerMethodReturnValueHandler實現類
    1. 如果你的方法返回值時void或者字串,該類都可以處理,將你的返回結果直接設定為檢視名
    2. 這裡不會將ModelAndViewContainerrequestHandled 屬性設定為 true,因為後續需要獲取 ModelAndView 物件進行檢視渲染

你是否會疑惑?如果我返回的是字串不是檢視名,被ViewNameMethodReturnValueHandler處理了怎麼辦?

放心,在 HandlerMethodReturnValueHandlerComposite 中判斷是否支援處理該返回結果中,會遍歷所有的 HandlerMethodReturnValueHandler 實現類,而 RequestResponseBodyMethodProcessor 排在 ViewNameMethodReturnValueHandler 前面,所以優先交給前者處理。

至於為什麼 RequestResponseBodyMethodProcessor 排在前面在本文中已經講過了,因為所有的 HandlerMethodReturnValueHandler 實現類用 ArrayList 集合儲存,RequestResponseBodyMethodProcessor 預設先新增進去?

參考文章:芋道原始碼《精盡 Spring MVC 原始碼分析》

相關文章