主頁 > 後端開發 > 精盡Spring MVC原始碼分析 - HandlerAdapter 組件(四)之 HandlerMethodReturnValueHandler

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

2020-12-22 07:59:24 後端開發

該系列檔案是本人在學習 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 = https://www.cnblogs.com/lifullmoon/p/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.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 = https://www.cnblogs.com/lifullmoon/p/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》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 原始碼分析》

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/238312.html

標籤:Java

上一篇:檔案上傳下載小工具

下一篇:Spring Boot 2.4版本前后的分組配置變化及對多環境配置結構的影響

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more