主頁 > 後端開發 > 精盡Spring MVC原始碼分析 - 一個請求的旅行程序

精盡Spring MVC原始碼分析 - 一個請求的旅行程序

2020-12-15 07:11:27 後端開發

該系列檔案是本人在學習 Spring MVC 的原始碼程序中總結下來的,可能對讀者不太友好,請結合我的原始碼注釋 Spring MVC 原始碼分析 GitHub 地址 進行閱讀

Spring 版本:5.2.4.RELEASE

該系列其他檔案請查看:《精盡 Spring MVC 原始碼分析 - 文章導讀》

在上一篇《WebApplicationContext 容器的初始化》檔案中分析了 Spring MVC 是如何創建兩個容器的,其中創建Root WebApplicationContext 后,呼叫其refresh()方法會觸發重繪事件,完成 Spring IOC 初始化相關作業,會初始化各種 Spring Bean 到當前容器中,該系列檔案暫不分析

我們先來了解一個請求是如何被 Spring MVC 處理的,由于整個流程涉及到的代碼非常多,所以本文的重點在于決議整體的流程,主要講解 DispatcherServlet 這個核心類,弄懂了這個流程后,才能更好的理解具體的原始碼,回過頭再來看則會更加的豁然開朗

整體流程圖

SpingMVC-Process

Spring MVC 處理請求的流程大致如上圖所示

  1. 用戶的瀏覽器發送一個請求,這個請求經過互聯網到達了我們的服務器,Servlet 容器首先接待了這個請求,并將該請求委托給 DispatcherServlet 進行處理,
  2. DispatcherServlet 將該請求傳給了處理器映射組件 HandlerMapping,并獲取到適合該請求的 HandlerExecutionChain 攔截器和處理器物件,
  3. 在獲取到處理器后,DispatcherServlet 還不能直接呼叫處理器的邏輯,需要進行對處理器進行適配,處理器適配成功后,DispatcherServlet 通過處理器配接器 HandlerAdapter 呼叫處理器的邏輯,并獲取回傳值 ModelAndView 物件,
  4. 之后,DispatcherServlet 需要根據 ModelAndView 決議視圖,決議視圖的作業由 ViewResolver 完成,若能決議成功,ViewResolver 會回傳相應的 View 視圖物件,
  5. 在獲取到具體的 View 物件后,最后一步要做的事情就是由 View 渲染視圖,并將渲染結果回傳給用戶,

以上就是 Spring MVC 處理請求的全程序,上面的流程進行了一定的簡化,主要涉及到最核心的組件,還有許多其他組件沒有表現出來,不過這并不影響大家對主程序的理解,

組件預覽

在上一篇《WebApplicationContext 容器的初始化》檔案講述 FramworkServlet 的 onRefresh 方法時,該方法由 DispatcherServlet 去實作,會初始化九大組件,如何初始化的這里暫時不展開討論,默認會從 spring-webmvc 下面的 DispatcherServlet.properties 檔案中讀取組件的實作類,感興趣可以先閱讀一下原始碼??,后續會依次描述

那么接下來就簡單介紹一下 DispatcherServlet 和九大組件:

組件 說明
DispatcherServlet Spring MVC 的核心組件,是請求的入口,負責協調各個組件作業
MultipartResolver 內容型別( Content-Type )為 multipart/* 的請求的決議器,例如決議處理檔案上傳的請求,便于獲取引數資訊以及上傳的檔案
HandlerMapping 請求的處理器匹配器,負責為請求找到合適的 HandlerExecutionChain 處理器執行鏈,包含處理器(handler)和攔截器們(interceptors
HandlerAdapter 處理器的配接器,因為處理器 handler 的型別是 Object 型別,需要有一個呼叫者來實作 handler 是怎么被執行,Spring 中的處理器的實作多變,比如用戶處理器可以實作 Controller 介面、HttpRequestHandler 介面,也可以用 @RequestMapping 注解將方法作為一個處理器等,這就導致 Spring MVC 無法直接執行這個處理器,所以這里需要一個處理器配接器,由它去執行處理器
HandlerExceptionResolver 處理器例外決議器,將處理器( handler )執行時發生的例外,決議( 轉換 )成對應的 ModelAndView 結果
RequestToViewNameTranslator 視圖名稱轉換器,用于決議出請求的默認視圖名
LocaleResolver 本地化(國際化)決議器,提供國際化支持
ThemeResolver 主題決議器,提供可設定應用整體樣式風格的支持
ViewResolver 視圖決議器,根據視圖名和國際化,獲得最終的視圖 View 物件
FlashMapManager FlashMap 管理器,負責重定向時,保存引數至臨時存盤(默認 Session)

Spring MVC 對各個組件的職責劃分的比較清晰,DispatcherServlet 負責協調,其他組件則各自做分內之事,互不干擾,經過這樣的職責劃分,代碼會便于維護,同時對于原始碼閱讀者來說,也會很友好,可以降低理解原始碼的難度,使大家能夠快速理清主邏輯,這一點值得我們學習,

ThemeResolver 和 FlashMapManager 組件在該系列檔案中不會進行講解,因為幾乎接觸不到,感興趣的可以去 Google 一下,嘻嘻~?? 筆者沒接觸過

FrameworkServlet

雖然在上面的整體流程圖中,我們看到請求是直接被 DispatcherServlet 所處理,但是實際上,FrameworkServlet 才是真正的入口,再來回顧一個 DispatcherServlet 的類圖,如下:

DispatcherServlet

FrameworkServlet 覆寫了 HttpServlet 的以下方法:

  • doGet(HttpServletRequest request, HttpServletResponse response)
  • doPost(HttpServletRequest request, HttpServletResponse response)
  • doPut(HttpServletRequest request, HttpServletResponse response)
  • doDelete(HttpServletRequest request, HttpServletResponse response)
  • doOptions(HttpServletRequest request, HttpServletResponse response)
  • doTrace(HttpServletRequest request, HttpServletResponse response)
  • service(HttpServletRequest request, HttpServletResponse response)

這些方法分別處理不同 HTTP 請求型別的請求,最終都會呼叫另一個 processRequest(HttpServletRequest request, HttpServletResponse response) 方法

其中 doGetdoPostdoPutdoDelete 四個方法是直接呼叫 processRequest 方法的

service

service(HttpServletRequest request, HttpServletResponse response) 方法,用于處理請求,方法如下:

@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

    // <1> 獲得請求方法
    HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
    // <2> 處理 PATCH 請求
    if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
        processRequest(request, response);
    }
    // <3> 處理其他型別的請求
    else {
        super.service(request, response);
    }
}
  1. 獲得 HttpMethod 請求方法

  2. 若請求方法是 PATCH ,呼叫 processRequest(HttpServletRequest request, HttpServletResponse response) 方法,處理請求,

    因為 HttpServlet 默認沒提供處理 Patch 請求型別的請求 ,所以只能通過覆寫父類的 service 方法來實作

  3. 如果是其他型別的請求,則直接呼叫父類的 service 方法,該方法如下:

    // HttpServlet.java
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String method = req.getMethod();
        if (method.equals(METHOD_GET)) {
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                // servlet doesn't support if-modified-since, no reason
                // to go through further expensive logic
                doGet(req, resp);
            } else {
                long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                if (ifModifiedSince < lastModified) {
                    // If the servlet mod time is later, call doGet()
                    // Round down to the nearest second for a proper compare
                    // A ifModifiedSince of -1 will always be less
                    maybeSetLastModified(resp, lastModified);
                    doGet(req, resp);
                } else {
                    resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }
        } else if (method.equals(METHOD_HEAD)) {
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);
        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);
        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);
        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);
        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);
        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);
        } else {
            // Note that this means NO servlet supports whatever method was requested, anywhere on this server.
            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[1];
            errArgs[0] = method;
            errMsg = MessageFormat.format(errMsg, errArgs);
            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
        }
    }
    

    可能你會有疑惑,為什么不在 service(HttpServletRequest request, HttpServletResponse response) 方法,直接呼叫 processRequest(HttpServletRequest request, HttpServletResponse response) 方法就好了?因為針對不同的請求方法,處理略微有所不同,父類 HttpServlet 已經處理了,

doOptions

doOptions(HttpServletRequest request, HttpServletResponse response)方法,用于處理 OPTIONS 型別的請求,方法如下:

@Override
protected void doOptions(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

    // 如果 dispatchOptionsRequest 為 true ,則處理該請求,默認為 true
    if (this.dispatchOptionsRequest || CorsUtils.isPreFlightRequest(request)) {
        // 處理請求
        processRequest(request, response);
        // 如果回應 Header 包含 "Allow" ,則不需要交給父方法處理
        if (response.containsHeader("Allow")) {
            // Proper OPTIONS response coming from a handler - we're done.
            return;
        }
    }

    // Use response wrapper in order to always add PATCH to the allowed methods
    // 呼叫父方法,并在回應 Header 的 "Allow" 增加 PATCH 的值
    super.doOptions(request, new HttpServletResponseWrapper(response) {
        @Override
        public void setHeader(String name, String value) {
            if ("Allow".equals(name)) {
                value = https://www.cnblogs.com/lifullmoon/p/(StringUtils.hasLength(value) ? value +", " : "") + HttpMethod.PATCH.name();
            }
            super.setHeader(name, value);
        }
    });
}

使用場景:AJAX 進行跨域請求時的預檢,需要向另外一個域名的資源發送一個HTTP OPTIONS請求頭,用以判斷實際發送的請求是否安全

doTrace

doTrace(HttpServletRequest request, HttpServletResponse response)方法,用于處理 TRACE 型別的請求,方法如下:

protected void doTrace(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

    // 如果 dispatchTraceRequest 為 true ,則處理該請求,默認為 false
    if (this.dispatchTraceRequest) {
        // 處理請求
        processRequest(request, response);
        // 如果回應的內容型別為 "message/http" ,則不需要交給父方法處理
        if ("message/http".equals(response.getContentType())) {
            // Proper TRACE response coming from a handler - we're done.
            return;
        }
    }
    // 呼叫父方法
    super.doTrace(request, response);
}

回顯服務器收到的請求,主要用于測驗或診斷,筆者目前還沒接觸過??

processRequest

processRequest(HttpServletRequest request, HttpServletResponse response) 方法,用于處理請求,方法如下:

protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

    // <1> 記錄當前時間,用于計算處理請求花費的時間
    long startTime = System.currentTimeMillis();
    // <2> 記錄例外,用于保存處理請求程序中發送的例外
    Throwable failureCause = null;

    // <3> TODO
    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    LocaleContext localeContext = buildLocaleContext(request);

    // <4> TODO
    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

    // <5> TODO
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

    // <6> TODO
    initContextHolders(request, localeContext, requestAttributes);

    try {
        // <7> 執行真正的邏輯
        doService(request, response);
    }
    catch (ServletException | IOException ex) {
        failureCause = ex; // <8> 記錄拋出的例外
        throw ex;
    }
    catch (Throwable ex) {
        failureCause = ex; // <8> 記錄拋出的例外
        throw new NestedServletException("Request processing failed", ex);
    }

    finally {
        // <9> TODO
        resetContextHolders(request, previousLocaleContext, previousAttributes);
        // <10> TODO
        if (requestAttributes != null) {
            requestAttributes.requestCompleted();
        }
        // <11> 如果日志級別為 DEBUG,則列印請求日志
        logResult(request, response, failureCause, asyncManager);
        // <12> 發布 ServletRequestHandledEvent 請求處理完成事件
        publishRequestHandledEvent(request, response, startTime, failureCause);
    }
}

<1> 記錄當前時間,用于計算處理請求花費的時間

<2> 記錄例外,用于保存處理請求程序中發送的例外

<7> 【核心】呼叫 doService(HttpServletRequest request, HttpServletResponse response) 抽象方法,執行真正的邏輯,由 DispatcherServlet 實作,所以這就是 DispatcherServlet 處理請求的真正入口

<8> 記錄執行程序拋出的例外,最終在 finally 的代碼段中使用,

<11> 如果日志級別為 DEBUG,則列印請求日志

<12> 呼叫 publishRequestHandledEvent 方法,通過 WebApplicationContext 發布 ServletRequestHandledEvent 請求處理完成事件,目前好像 Spring MVC 沒有監聽這個事件,可以自己寫一個監聽器用于獲取請求資訊,示例如下:

@Component
@Log4j2
public class ServletRequestHandledEventListener implements ApplicationListener<ServletRequestHandledEvent>{
	@Override
	public void onApplicationEvent(ServletRequestHandledEvent event) {
		log.info("請求描述:{}", event.getDescription());
		log.info("請求路徑:{}", event.getRequestUrl());
        log.info("開始時間:{}", event.getTimestamp());
		log.info("請求耗時:{}", event.getProcessingTimeMillis());
		log.info("狀 態 碼:{}", event.getStatusCode());
        log.info("失敗原因:{}", event.getFailureCause());
	}
}

到這里,FrameworkServlet 算是講完了,接下來就要開始講 DispatcherServlet 這個核心類了

DispatcherServlet

org.springframework.web.servlet.DispatcherServlet核心類,作為 Spring MVC 的核心類,承擔調度器的角色,協調各個組件進行作業,處理請求,一起來揭開這神秘的面紗吧??

靜態代碼塊

private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";

private static final Properties defaultStrategies;

static {
    // Load default strategy implementations from properties file.
    // This is currently strictly internal and not meant to be customized by application developers.
    try {
        ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
        defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
    }
    catch (IOException ex) {
        throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
    }
}

會從 DispatcherServlet.properties 檔案中加載默認的組件實作類,將相關配置加載到 defaultStrategies 中,檔案如下:

### org.springframework.web.servlet.DispatcherServlet.properties

# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
	org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
	org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
	org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
	org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
	org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
	org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

可以看到各個組件的默認實作類

構造方法

/** MultipartResolver used by this servlet. multipart 資料(檔案)處理器 */
@Nullable
private MultipartResolver multipartResolver;

/** LocaleResolver used by this servlet. 語言處理器,提供國際化的支持 */
@Nullable
private LocaleResolver localeResolver;

/** ThemeResolver used by this servlet. 主題處理器,設定需要應用的整體樣式 */
@Nullable
private ThemeResolver themeResolver;

/** List of HandlerMappings used by this servlet. 處理器匹配器,回傳請求對應的處理器和攔截器們 */
@Nullable
private List<HandlerMapping> handlerMappings;

/** List of HandlerAdapters used by this servlet. 處理器配接器,用于執行處理器 */
@Nullable
private List<HandlerAdapter> handlerAdapters;

/** List of HandlerExceptionResolvers used by this servlet. 例外處理器,用于決議處理器發生的例外 */
@Nullable
private List<HandlerExceptionResolver> handlerExceptionResolvers;

/** RequestToViewNameTranslator used by this servlet. 視圖名稱轉換器 */
@Nullable
private RequestToViewNameTranslator viewNameTranslator;

/** FlashMapManager used by this servlet. FlashMap 管理器,負責重定向時保存引數到臨時存盤(默認 Session)中 */
@Nullable
private FlashMapManager flashMapManager;

/** List of ViewResolvers used by this servlet. 視圖決議器,根據視圖名稱和語言,獲取 View 視圖 */
@Nullable
private List<ViewResolver> viewResolvers;

public DispatcherServlet() {
    super();
    setDispatchOptionsRequest(true);
}

public DispatcherServlet(WebApplicationContext webApplicationContext) {
    super(webApplicationContext);
    setDispatchOptionsRequest(true);
}

定義了九個組件,在組件預覽中已經做過簡單介紹了

構造方法中都會設定 dispatchOptionsRequesttrue,在父類 FrameworkServlet 中可以看到,如果請求是 OPTIONS 則會處理請求

onRefresh

onRefresh(ApplicationContext context) 方法,初始化 Spring MVC 的各個組件,方法如下:

@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}

/**
 * Initialize the strategy objects that this servlet uses.
 * <p>May be overridden in subclasses in order to initialize further strategy objects.
 */
protected void initStrategies(ApplicationContext context) {
    // 初始化 MultipartResolver
    initMultipartResolver(context);
    // 初始化 LocaleResolver
    initLocaleResolver(context);
    // 初始化 ThemeResolver
    initThemeResolver(context);
    // 初始化 HandlerMapping
    initHandlerMappings(context);
    // 初始化 HandlerAdapter
    initHandlerAdapters(context);
    // 初始化 HandlerExceptionResolver
    initHandlerExceptionResolvers(context);
    // 初始化 RequestToViewNameTranslator
    initRequestToViewNameTranslator(context);
    // 初始化 ViewResolver
    initViewResolvers(context);
    // 初始化 FlashMapManager
    initFlashMapManager(context);
}

創建 Servlet WebApplicationContext 容器后會觸發該方法,在《WebApplicationContext 容器的初始化》FrameworkServlet小節的 onRefresh 方法中提到過

可以看到每個方法會初始化構造方法中的每個組件

1. doService

doService(HttpServletRequest request, HttpServletResponse response)方法,DispatcherServlet 的處理請求的入口方法,代碼如下:

@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // <1> 如果日志級別為 DEBUG,則列印請求日志
    logRequest(request);

    // Keep a snapshot of the request attributes in case of an include,
    // to be able to restore the original attributes after the include.
    // <2> 保存當前請求中相關屬性的一個快照
    Map<String, Object> attributesSnapshot = null;
    if (WebUtils.isIncludeRequest(request)) {
        attributesSnapshot = new HashMap<>();
        Enumeration<?> attrNames = request.getAttributeNames();
        while (attrNames.hasMoreElements()) {
            String attrName = (String) attrNames.nextElement();
            if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
                attributesSnapshot.put(attrName, request.getAttribute(attrName));
            }
        }
    }

    // Make framework objects available to handlers and view objects.
    // <3> 設定 Spring 框架中的常用物件到 request 屬性中
    request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
    request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
    request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
    request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

    // <4> FlashMap 的相關配置
    if (this.flashMapManager != null) {
        FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
        if (inputFlashMap != null) {
            request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
        }
        request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
        request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
    }

    try {
        // <5> 執行請求的分發
        doDispatch(request, response);
    }
    finally {
        // <6> 異步處理相關
        if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
            // Restore the original attribute snapshot, in case of an include.
            if (attributesSnapshot != null) {
                restoreAttributesAfterInclude(request, attributesSnapshot);
            }
        }
    }
}
  1. 呼叫logRequest(HttpServletRequest request) 方法,如果日志級別為 DEBUG,則列印請求日志
  2. 保存當前請求中相關屬性的一個快照,作為異步處理的屬性值,防止被修改,暫時忽略
  3. 設定 Spring 框架中的常用物件到 request 屬性中,例如 webApplicationContextlocaleResolverthemeResolver
  4. FlashMap 的相關配置,暫時忽略
  5. 【重點】呼叫 doDispatch(HttpServletRequest request, HttpServletResponse response) 方法,執行請求的分發
  6. 異步處理相關,暫時忽略

2. doDispatch【核心】

doDispatch(HttpServletRequest request, HttpServletResponse response) 方法,行請求的分發,在開始看具體的代碼實作之前,我們在來回味下這張圖片:

SpingMVC-Process

這張圖,更多的反應的是 DispatcherServlet 的 doDispatch(...) 方法的核心流程,方法如下:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;

    // <1> 獲取異步管理器
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            // <2> 檢測請求是否為上傳請求,如果是則通過 multipartResolver 將其封裝成 MultipartHttpServletRequest 物件
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // Determine handler for the current request.
            // <3> 獲得請求對應的 HandlerExecutionChain 物件(HandlerMethod 和 HandlerInterceptor 攔截器們)
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) { // <3.1> 如果獲取不到,則根據配置拋出例外或回傳 404 錯誤
                noHandlerFound(processedRequest, response);
                return;
            }

            // Determine handler adapter for the current request.
            // <4> 獲得當前 handler 對應的 HandlerAdapter 物件
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            // Process last-modified header, if supported by the handler.
            // <4.1> 處理有Last-Modified請求頭的場景
            String method = request.getMethod();
            boolean isGet = "GET".equals(method);
            if (isGet || "HEAD".equals(method)) { // 不清楚為什么要判斷方法型別為'HEAD'
                // 獲取請求中服務器端最后被修改時間
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return;
                }
            }

            // <5> 前置處理 攔截器
            // 注意:該方法如果有一個攔截器的前置處理回傳false,則開始倒序觸發所有的攔截器的 已完成處理
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            // Actually invoke the handler.
            // <6> 真正的呼叫 handler 方法,也就是執行對應的方法,并回傳視圖
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            // <7> 如果是異步
            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }

            // <8> 無視圖的情況下設定默認視圖名稱
            applyDefaultViewName(processedRequest, mv);
            // <9> 后置處理 攔截器
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        catch (Exception ex) {
            dispatchException = ex; // <10> 記錄例外
        }
        catch (Throwable err) {
            // As of 4.3, we're processing Errors thrown from handler methods as well,
            // making them available for @ExceptionHandler methods and other scenarios.
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
        // <11> 處理正常和例外的請求呼叫結果
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
        // <12> 已完成處理 攔截器
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
    catch (Throwable err) {
        // <12> 已完成處理 攔截器
        triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err));
    }
    finally {
        // <13.1> Asyn
        if (asyncManager.isConcurrentHandlingStarted()) {
            // Instead of postHandle and afterCompletion
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        }
        // <13.1> 如果是上傳請求則清理資源
        else {
            // Clean up any resources used by a multipart request.
            if (multipartRequestParsed) {
                cleanupMultipart(processedRequest);
            }
        }
    }
}
  1. 獲得 WebAsyncManager 異步處理器,暫時忽略

  2. 【檔案】呼叫 checkMultipart(HttpServletRequest request) 方法,檢測請求是否為上傳請求,如果是則通過 multipartResolver 組件將其封裝成 MultipartHttpServletRequest 物件,便于獲取引數資訊以及檔案

  3. 【處理器匹配器】呼叫 getHandler(HttpServletRequest request) 方法,通過 HandlerMapping 組件獲得請求對應的 HandlerExecutionChain 處理器執行鏈,包含 HandlerMethod 處理器和 HandlerInterceptor 攔截器們

    1. 如果獲取不到對應的執行鏈,則根據配置拋出例外或回傳 404 錯誤
  4. 【處理器配接器】呼叫 getHandlerAdapter(Object handler) 方法,獲得當前處理器對應的 HandlerAdapter 配接器物件

  5. 處理有 Last-Modified 請求頭的場景,暫時忽略

  6. 【攔截器】呼叫 HandlerExecutionChain 執行鏈的 applyPreHandle(HttpServletRequest request, HttpServletResponse response) 方法,攔截器的前置處理

    如果出現攔截器前置處理失敗,則會呼叫攔截器的已完成處理方法(倒序)

  7. 【重點】呼叫 HandlerAdapter 配接器的 handle(HttpServletRequest request, HttpServletResponse response, Object handler) 方法,真正的執行處理器,也就是執行對應的方法(例如我們定義的 Controller 中的方法),并回傳視圖

  8. 如果是異步,則直接 return注意,還是會執行 finally 中的代碼

  9. 呼叫 applyDefaultViewName(HttpServletRequest request, ModelAndView mv) 方法,ModelAndView 不為空,但是沒有視圖,則設定默認視圖名稱,使用到了 viewNameTranslator 視圖名稱轉換器組件

  10. 【攔截器】呼叫 HandlerExecutionChain 執行鏈的 applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) 方法,攔截器的后置處理倒序

  11. 記錄例外,注意,此處僅僅記錄,不會拋出例外,而是統一交給 <11> 處理

  12. 【處理執行結果】呼叫 processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) 方法,處理正常例外的請求呼叫結果,包含頁面渲染

  13. 【攔截器】如果上一步發生了例外,則呼叫 triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, Exception ex) 方法,即呼叫 HandlerInterceptor 執行鏈的 triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, Exception ex) 方法,攔截器已完成處理倒序

  14. finally 代碼塊,異步處理,暫時忽略,如果是涉及到檔案的請求,則清理相關資源

上面將 DispatcherServlet 處理請求的整個流程步驟都列出來了,涉及到的組件分別在后續的檔案中將分開進行分析

2.1 checkMultipart

checkMultipart(HttpServletRequest request) 方法,檢測請求是否為上傳請求,如果是則通過 multipartResolver 組件將其封裝成 MultipartHttpServletRequest 物件

protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
    // 如果該請求是一個涉及到 multipart (檔案)的請求
    if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
        if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
            if (request.getDispatcherType().equals(DispatcherType.REQUEST)) {
                logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
            }
        }
        else if (hasMultipartException(request)) {
            logger.debug("Multipart resolution previously failed for current request - " +
                    "skipping re-resolution for undisturbed error rendering");
        }
        else {
            try {
                // 將 HttpServletRequest 請求封裝成 MultipartHttpServletRequest 物件,決議請求里面的引數以及檔案
                return this.multipartResolver.resolveMultipart(request);
            }
            catch (MultipartException ex) {
                if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
                    logger.debug("Multipart resolution failed for error dispatch", ex);
                    // Keep processing error dispatch with regular request handle below
                }
                else {
                    throw ex;
                }
            }
        }
    }
    // If not returned before: return original request.
    return request;
}

2.2 getHandler

getHandler(HttpServletRequest request) 方法,通過 HandlerMapping 組件獲得請求對應的 HandlerExecutionChain 處理器執行鏈,包含 HandlerMethod 處理器和 HandlerInterceptor 攔截器們

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        for (HandlerMapping mapping : this.handlerMappings) {
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }
    return null;
}

2.3 getHandlerAdapter

getHandlerAdapter(Object handler) 方法,獲得當前處理器對應的 HandlerAdapter 配接器物件

protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
    if (this.handlerAdapters != null) {
        for (HandlerAdapter adapter : this.handlerAdapters) {
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
    }
    throw new ServletException("No adapter for handler [" + handler +
            "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}

2.4 applyDefaultViewName

applyDefaultViewName(HttpServletRequest request, ModelAndView mv) 方法,ModelAndView 不為空,但是沒有視圖,則設定默認視圖名稱

private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
    if (mv != null && !mv.hasView()) {
        String defaultViewName = getDefaultViewName(request);
        if (defaultViewName != null) {
            mv.setViewName(defaultViewName);
        }
    }
}
@Nullable
protected String getDefaultViewName(HttpServletRequest request) throws Exception {
    // 使用到了 `viewNameTranslator` 視圖名稱轉換器組件
    return (this.viewNameTranslator != null ? this.viewNameTranslator.getViewName(request) : null);
}

2.5 processDispatchResult

processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) 方法,處理正常例外的請求呼叫結果,方法如下:

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
        @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
        @Nullable Exception exception) throws Exception {

    // <1> 標記是否為處理生成例外的 ModelAndView 物件
    boolean errorView = false;

    // <2> 如果該請求出現例外
    if (exception != null) {
        // 情況一,從 ModelAndViewDefiningException 中獲得 ModelAndView 物件
        if (exception instanceof ModelAndViewDefiningException) {
            logger.debug("ModelAndViewDefiningException encountered", exception);
            mv = ((ModelAndViewDefiningException) exception).getModelAndView();
        }
        // 情況二,處理例外,生成 ModelAndView 物件
        else {
            Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
            mv = processHandlerException(request, response, handler, exception);
            // 標記 errorView
            errorView = (mv != null);
        }
    }

    // Did the handler return a view to render?
    // <3> 是否進行頁面渲染
    if (mv != null && !mv.wasCleared()) {
        // <3.1> 渲染頁面
        render(mv, request, response);
        // <3.2> 清理請求中的錯誤訊息屬性
        // 因為上述的情況二中 processHandlerException 會通過 WebUtils 設定錯誤訊息屬性,所以這里得清理一下
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    }
    else {
        if (logger.isTraceEnabled()) {
            logger.trace("No view rendering, null ModelAndView returned.");
        }
    }

    // <4> 如果是異步
    if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
        // Concurrent handling started during a forward
        return;
    }

    // <5> 已完成處理 攔截器
    if (mappedHandler != null) {
        mappedHandler.triggerAfterCompletion(request, response, null);
    }
}
  1. 標記是否為處理生成例外的 ModelAndView 物件

  2. 如果該請求出現例外

    1. 情況一,從 ModelAndViewDefiningException 中獲得 ModelAndView 物件
    2. 情況二,呼叫 processHandlerException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 方法,處理例外,生成 ModelAndView 物件
  3. 如果 ModelAndView 不為空且沒有被清理,例如你現在使用最多的 @ResponseBody 這里就為空,不需要渲染

    1. 呼叫 render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) 方法,渲染頁面
    2. 如果是上面第 2 步中情況二生成的 ModelAndView 物件,則需要清理請求中的錯誤訊息屬性,因為上述的情況二會通過 WebUtils 設定錯誤訊息屬性,所以這里得清理一下
  4. 如果是異步請求,則直接 return

  5. 正常情況下,呼叫 triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, Exception ex) 方法,即呼叫 HandlerInterceptor 執行鏈的 triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, Exception ex) 方法,攔截器已完成處理倒序

2.5.1 processHandlerException

processHandlerException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 方法,處理例外,生成 ModelAndView 物件

@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
        @Nullable Object handler, Exception ex) throws Exception {

    // Success and error responses may use different content types
    // 移除 PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE 屬性
    request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

    // Check registered HandlerExceptionResolvers...
    // <a> 遍歷 HandlerExceptionResolver 陣列,決議例外,生成 ModelAndView 物件
    ModelAndView exMv = null;
    if (this.handlerExceptionResolvers != null) {
        // 遍歷 HandlerExceptionResolver 陣列
        for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
            // 決議例外,生成 ModelAndView 物件
            exMv = resolver.resolveException(request, response, handler, ex);
            // 生成成功,結束回圈
            if (exMv != null) {
                break;
            }
        }
    }
    // <b> 情況一,生成了 ModelAndView 物件,進行回傳
    if (exMv != null) {
        // ModelAndView 物件為空,則回傳 null
        if (exMv.isEmpty()) {
            request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
            return null;
        }
        // We might still need view name translation for a plain error model...
        // 沒有視圖則設定默認視圖
        if (!exMv.hasView()) {
            String defaultViewName = getDefaultViewName(request);
            if (defaultViewName != null) {
                exMv.setViewName(defaultViewName);
            }
        }
        // 列印日志
        if (logger.isTraceEnabled()) {
            logger.trace("Using resolved error view: " + exMv, ex);
        }
        if (logger.isDebugEnabled()) {
            logger.debug("Using resolved error view: " + exMv);
        }
        // 設定請求中的錯誤訊息屬性
        WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
        return exMv;
    }

    // <c> 情況二,未生成 ModelAndView 物件,則拋出例外
    throw ex;
}

<a> 處,遍歷 HandlerExceptionResolver 陣列,呼叫 HandlerExceptionResolver#resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 方法,決議例外,生成 ModelAndView 物件

<b> 處,情況一,生成了 ModelAndView 物件,邏輯比較簡單

<c> 處,情況二,未生成 ModelAndView 物件,則拋出例外

2.5.2 render

render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) 方法,渲染 ModelAndView,方法如下:

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    // Determine locale for request and apply it to the response.
    // <1> 決議 request 中獲得 Locale 物件,并設定到 response 中
    Locale locale = (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
    response.setLocale(locale);

    // 獲得 View 物件
    View view;
    String viewName = mv.getViewName();
    // 情況一,使用 viewName 獲得 View 物件
    if (viewName != null) {
        // We need to resolve the view name.
        // <2.1> 使用 viewName 獲得 View 物件
        view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
        if (view == null) { // 獲取不到,拋出 ServletException 例外
            throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
                    "' in servlet with name '" + getServletName() + "'");
        }
    }
    // 情況二,直接使用 ModelAndView 物件的 View 物件
    else {
        // No need to lookup: the ModelAndView object contains the actual View object.
        // <2.2> 直接使用 ModelAndView 物件的 View 物件
        view = mv.getView();
        if (view == null) { // 獲取不到,拋出 ServletException 例外
            throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
                    "View object in servlet with name '" + getServletName() + "'");
        }
    }

    // Delegate to the View object for rendering.
    // 列印日志
    if (logger.isTraceEnabled()) {
        logger.trace("Rendering view [" + view + "] ");
    }
    try {
        // <3> 設定回應的狀態碼
        if (mv.getStatus() != null) {
            response.setStatus(mv.getStatus().value());
        }
        // <4> 渲染頁面
        view.render(mv.getModelInternal(), request, response);
    }
    catch (Exception ex) {
        if (logger.isDebugEnabled()) {
            logger.debug("Error rendering view [" + view + "]", ex);
        }
        throw ex;
    }
}
  1. 呼叫 LocaleResolverresolveLocale(HttpServletRequest request) 方法,從 request 中獲得 Locale 物件,并設定到 response

  2. 獲得 View 物件,有兩種情況

    1. 呼叫 resolveViewName 方法,使用 viewName 通過獲得 View 物件,方法如下:

      @Nullable
      protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
              Locale locale, HttpServletRequest request) throws Exception {
      
          if (this.viewResolvers != null) {
              // 遍歷 ViewResolver 陣列
              for (ViewResolver viewResolver : this.viewResolvers) {
                  // 根據 viewName + locale 引數,決議出 View 物件
                  View view = viewResolver.resolveViewName(viewName, locale);
                  // 決議成功,直接回傳 View 物件
                  if (view != null) {
                      return view;
                  }
              }
          }
          return null;
      }
      
    2. 直接使用 ModelAndView 物件的 View 物件

  3. 設定回應的狀態碼

  4. 呼叫 Viewrender(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) 方法,渲染視圖

總結

本文對 Spring MVC 處理請求的整個程序進行了分析,核心就是通過 DispatcherServlet 協調各個組件作業,處理請求,因為 DispatcherServlet 是一個 Servlet,在 Servlet 容器中,會將請求交由它來處理,

通過本文對 DispatcherServlet 是如何處理請求已經有了一個整體的認識,不過在整個處理程序中涉及到的各個 Spring MVC 組件還沒有進行分析,對于許多細節存在疑惑,不要慌,那么接下來會對每一個 Spring MVC 組件進行分析,這樣,便于我們對 Spring MVC 的理解,然后再回過頭來思考 DispatcherServlet 這個類,能夠更好的將這些組件串聯在一起,先整體,后區域,逐步逐步抽絲剝繭,看清理透,


流程示意圖,來自 SpringMVC - 運行流程圖及原理分析

代碼序列圖


流程示意圖,來自《看透 Spring MVC 源代碼分析與實踐》 書籍中的第 123 頁

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

參考文章:Spring 揭秘 -- 尋找遺失的 web.xml

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

標籤:Java

上一篇:redis服務又出現卡死,又是一次不當使用,這個鍋你背定了!

下一篇:Jrebel License Server 激活 IDEA-Jrebel-在線-離線-均適用

標籤雲
其他(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