SpringMVC 原理 - 設計原理、啟動過程、請求處理詳細解讀

来源:https://www.cnblogs.com/lbzhello/archive/2019/08/19/spring-mvc.html
-Advertisement-
Play Games

SpringMVC 原理 設計原理、啟動過程、請求處理詳細解讀 目錄 "一、 設計原理" "二、 啟動過程" "三、 請求處理" 一、 設計原理 Servlet 規範 SpringMVC 是基於 Servlet 的。 Servlet 是運行在 web 伺服器上的程式,它接收並響應來自 web 客戶端 ...


SpringMVC 原理 - 設計原理、啟動過程、請求處理詳細解讀

目錄

一、 設計原理
二、 啟動過程
三、 請求處理

一、 設計原理

Servlet 規範

SpringMVC 是基於 Servlet 的。

Servlet 是運行在 web 伺服器上的程式,它接收並響應來自 web 客戶端的請求(通常是 HTTP 請求)。

Servlet 規範有三個主要的技術點: Servlet, Filter, Listener

1. Servlet

Servlet 是實現 Servlet 介面的程式。對於 HTTP, 通常繼承 javax.servlet.http.HttpServlet, 可以為不同的 URL 配置不同的 Servlet。Servlet 是"單例"的,所有請求共用一個 Servlet, 因此對於共用變數(比如實例變數),需要自己保證其線程安全性。DispatcherServlet 便是一個 Servlet。

Servlet 生命周期

  1. Servlet 實例化後,Servlet 容器會調用 init 方法初始化。init 只會被調用一次,且必須成功執行才能提供服務。

  2. 客戶端每次發出請求,Servlet 容器調用 service 方法處理請求。

  3. Servlet 被銷毀前,Servlet 容器調用 destroy 方法。通常用來清理資源,比如記憶體,文件,線程等。

2. Filter

Filter 是過濾器,用於在客戶端的請求訪問後端資源之前,攔截這些請求;或者在伺服器的響應發送回客戶端之前,處理這些響應。只有通過 Filter 的請求才能被 Servlet 處理。

Filter 可以通過配置(xml 或 java-based)攔截特定的請求,在 Servlet 執行前後(由 chain.doFilter 劃分)處理特定的邏輯,如許可權過濾,字元編碼,日誌列印,Session 處理,圖片轉換等

Filter 生命周期

  1. Filter 實例化後,Servlet 容器會調用 init 方法初始化。init 方法只會被調用一次,且成功執行(不拋出錯誤且沒有超時)才能提供過濾功能

  2. 客戶端每次發出請求,Servlet 容器調用 doFilter 方法攔截請求。

    public void  doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws java.io.IOException, ServletException {
    
        // 客戶端的請求訪問後端資源之前的處理邏輯
        System.out.println("我在 Servlet 前執行");
    
        // 把請求傳回過濾鏈,即傳給下一個 Filter, 或者交給 Servlet 處理
        chain.doFilter(request,response);
    
        // 伺服器的響應發送回客戶端之前的處理邏輯
        System.out.println("我在 Servlet 後執行");
    }
  3. Filter 生命周期結束時調用 destroy 方法,通常用來清理它所持有的資源,比如記憶體,文件,線程等。

3. Listener

Listener 是監聽某個對象的狀態變化的組件,是一種觀察者模式。

被監聽的對象可以是域對象 ServletContext, Session, Request

監聽的內容可以是域對象的創建與銷毀,域對象屬性的變化

ServletContext HttpSession ServletRequest
對象的創建與銷毀 ServletContextListener HttpSessionListener ServletRequestListener
對象的屬性的變化 ServletContextAttributeListener HttpSessionAttributeListener ServletRequestAttributeListener

ContextLoaderListener 是一個 ServletContextListener, 它會監聽 ServletContext 創建與銷毀事件

public interface ServletContextListener extends EventListener {

    // 通知 ServletContext 已經實例化完成了,這個方法會在所有的 servlet 和 filter 實例化之前調用
    public void contextInitialized(ServletContextEvent sce);

    // 通知 ServletContext 將要被銷毀了,這個方法會在所有的 servlet 和 filter 調用 destroy 之後調用
    public void contextDestroyed(ServletContextEvent sce);

}

Servlet 的配置與 SpringMVC 的實現

通過 web.xml

這個是以前常用的配置方式。Servlet 容器會在啟動時載入根路徑下 /WEB-INF/web.xml 配置文件。根據其中的配置載入 Servlet, Listener, Filter 等,然後根據 Servlet 規範調用相應的方法。下麵是 SpringMVC 的常見配置:

    <listener>
        <!--監聽器,用來管理 root WebApplicationContext 的生命周期:載入、初始化、銷毀-->
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <!--root web application context, 通過 ContextLoaderListener 載入-->
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>

    <context-param>
        <!--可以不配置,預設為 XmlWebApplicationContext-->
        <param-name>contextClass</param-name>
        <!--WebApplicationContext 實現類-->
        <param-value>org.springframework.web.context.support.XmlWebApplicationContext</param-value>
    <context-param>

    <servlet>
        <servlet-name>dispatcher</servlet>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <!--DispatchServlet 持有的 WebApplicationContext-->
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/applicationContext.xml</param-value>
            <load-on-startup>1</load-on-startup>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatch</servlet-name>
        <servlet-pattern>/*</servlet-pattern>
    </servlet-mapping>

通過 ServletContainerInitializer

這個是 Servlet 3.0 的規範,新的 code-based 的配置方式。簡單來說就是容器會去載入文件JAR包下 META-INF/services/javax.servlet.ServletContainerInitalizer 文件中聲明的 ServletContainerInitalizer(SCI) 實現類,並調用他的 onStartup 方法,可以通過 @HandlesTypes 註解將特定的 class 添加到 SCI。

在 spring-web 模塊下有個文件 META-INF/services/javax.servlet.ServletContainerInitializer, 其內容為

org.springframework.web.SpringServletContainerInitializer

SpringServletContainerInitializer

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

    @Override
    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {

        List<WebApplicationInitializer> initializers = new LinkedList<>();

        if (webAppInitializerClasses != null) {
            for (Class<?> waiClass : webAppInitializerClasses) {
                // Be defensive: Some servlet containers provide us with invalid classes,
                // no matter what @HandlesTypes says...
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                        WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer)
                                ReflectionUtils.accessibleConstructor(waiClass).newInstance());
                    }
                    catch (Throwable ex) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                    }
                }
            }
        }

        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
            return;
        }

        servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
        AnnotationAwareOrderComparator.sort(initializers);
        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);
        }
    }

}

它會探測並載入 ClassPath 下 WebApplicationContextInitializer 的實現類,調用它的 onStartUp 方法。

簡單來說,只要 ClassPath 下存在 WebApplicationContextInitializer 的實現類,SpringMVC 會自動發現它,並且調用他的 onStartUp 方法

下麵是一個 SpringMVC 的 java-based 配置方式:

public class MyWebAppInitializer implements WebApplicationInitializer {
 
    @Override
    public void onStartup(ServletContext container) {
        // 創建根容器
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register(AppConfig.class);

        // 創建 ContextLoaderListener
        // 用來管理 root WebApplicationContext 的生命周期:載入、初始化、銷毀
        container.addListener(new ContextLoaderListener(rootContext));

        // 創建 dispatcher 持有的上下文容器
        AnnotationConfigWebApplicationContext dispatcherContext = new AnnotationConfigWebApplicationContext();
        dispatcherContext.register(DispatcherConfig.class);

        // 註冊、配置 dispatcher servlet
        ServletRegistration.Dynamic dispatcher = container.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
    }
}

可見,它和上面 web.xml 配置方式基本一致,也配置了 ContextLoaderListener 和 DispatcherServlet 以及其持有的 application context,不過通過代碼實現,邏輯更加清晰。

如果每次都需要創建 ContextLoaderListener 和 DispatcherServlet,顯然很麻煩,不符合 KISS 原則(keep it simple and stupid)。

SpringMVC 為 WebApplicationInitializer 提供了基本的抽象實現類

WebApplicationInitializer

代碼實現這裡不再贅述,總之就是利用模版方法,調用鉤子方法。子類只需提供少量的配置即可完成整個邏輯的創建。

所以,更簡單的方法是繼承 AbstractAnnotationConfigDispatcherServletInitializer

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { MyWebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }
}

WebApplicationContext

SpringMVC 用 Spring 化的方式來管理 web 請求中的各種對象。

什麼是 Spring 化? IOC 和 AOP, 這不是本文的重點,具體自行查閱。

SpringMVC 會通過 WebApplicationContext 來管理伺服器請求中涉及到的各種對象和他們之間的依賴關係。我們不需要花費大量的精力去理清各種對象之間的複雜關係,而是以離散的形式專註於單獨的功能點。

WebApplicationContext 繼承自 ApplicationContext, 它定義了一些新的作用域,提供了 getServletContext 介面。

public interface WebApplicationContext extends ApplicationContext {

    // 根容器名,作為 key 存儲在 ServletContext 中; ServletContext 持有的 WebApplicationContext
    String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";

    /**
     * 這三個是 WebApplicationContext 特有的作用域
     * 通過 WebApplicationContextUtils.registerWebApplicationScopes 註冊相應的處理器
     */
    String SCOPE_REQUEST = "request";
    String SCOPE_SESSION = "session";
    String SCOPE_APPLICATION = "application";

    /**
     * ServletContext 在 WebApplicationContext 中的名字
     * 通過 WebApplicationContextUtils.registerEnvironmentBeans 註冊到 WebApplicationContext 中
     */
    String SERVLET_CONTEXT_BEAN_NAME = "servletContext";

    /**
      * ServletContext 和 ServletConfig 配置參數在 WebApplicationContext 中的名字
      * ServletConfig 的參數具有更高的優先順序,會覆蓋掉 ServletContext 中的參數
      * 值為 Map<String, String> 結構
      */
    String CONTEXT_PARAMETERS_BEAN_NAME = "contextParameters";

    /**
      * ServletContext 屬性信息在 WebApplicationContext 中的名字
      * 值為 Map<String, String> 結構
      * 屬性是用來描述 ServletContext 本身的一些信息的
      */
    String CONTEXT_ATTRIBUTES_BEAN_NAME = "contextAttributes";


    /**
     * 獲取 ServletContext 介面
     */
    @Nullable
    ServletContext getServletContext();

}

WebApplicationContext 類圖

ApplicationContext 有一個抽象實現類 AbstractApplicationContext, 模板方法的設計模式。它有一個 refresh 方法,它定義了載入或初始化 bean 配置的基本流程。後面的實現類提供了不同的讀取配置的方式,可以是 xml, annotation, web 等,並且可以通過模板方法定製自己的需求。

AbstractApplicationContext 有兩個實現體系, 他們的區別是每次 refresh 時是否會創建一個新的 DefaultListableBeanFactory。DefaultListableBeanFactory 是實際存放 bean 的容器, 提供 bean 註冊功能。

  1. AbstractRefreshableApplicationContext 這個 refreshable 並不是指 refresh 這個方法,而是指 refreshBeanFactory 這個方法。他會在每次 refresh 時創建一個新的 BeanFactory(DefaultListableBeanFactory)用於存放 bean,然後調用 loadBeanDefinitions 將 bean 載入到新創建的 BeanFactory。

  2. GenericApplicationContext 內部持有一個 DefaultListableBeanFactory, 所以可以提前將 Bean 載入到 DefaultListableBeanFactory, 它也有 refreshBeanFactory 方法,但是這個方法啥也不做。

根據讀取配置的方式,可以分成 3 類,基於 xml 的配置, 基於 annotation 的配置基於 java-based 的配置

  1. 基於 xml 的配置使用 xml 作為配置方式, 此類的名字都含有 Xml, 比如從文件系統路徑讀取配置的 FilePathXmlApplicationContext, 從 ClassPath 讀取配置的 ClassPathXmlApplicationContext, 基於 Web 的 XmlWebApplicationContext 等

  2. 基於註解的配置通過掃描指定包下麵具有某個註解的類,將其註冊到 bean 容器,相關註解有 @Component, @Service, @Controller, @Repository,@Named 等

  3. java-based 的配置方式目前是大勢所趨,結合註解的方式使用簡單方便易懂,主要是 @Configuration 和 @Bean

上面幾個類是基礎類,下麵是 SpringMVC 相關的 WebApplicationContext

XmlWebApplicationContext 和 AnnotationConfigWebApplicationContext 繼承自 AbstractRefreshableApplicationContext,表示它們會在 refresh 時新創建一個 DefaultListableBeanFactory, 然後 loadBeanDefinitions。 它們分別從 xml 和 註解類(通常是 @Configuration 註解的配置類)中讀取配置信息。

XmlEmbeddedWebApplicationContext 和 AnnotationConfigEmbeddedWebApplicationContext 繼承自 GenericApplicationContext,表示他們內部持有一個 DefaultListableBeanFactory, 從名字可以看出它們是用於 "Embedded" 方面的,即 SpringBoot 嵌入容器所使用的 WebApplicationContext

SpringMVC 應用中幾乎所有的類都交由 WebApplicationContext 管理,包括業務方面的 @Controller, @Service, @Repository 註解的類, ServletContext, 文件處理 multipartResolver, 視圖解析器 ViewResolver, 處理器映射器 HandleMapping 等。

refresh 流程

AbstractApplicationContext#refresh

@Override
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        prepareRefresh();

        // XmlWebApplicationContext 和 AnnotationConfigWebApplicationContext 會在這裡執行 refreshBeanFactory
        // 創建一個新的 DefaultListableBeanFactory 然後從 xml 或 java-config 配置中 loadBeanDefinitions
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

        prepareBeanFactory(beanFactory);

        try {
            // 在這裡會配置一些 web 相關的東西,註冊 web 相關的 scope
            postProcessBeanFactory(beanFactory);

            // 下麵步驟和初始化其他 ApplicationContext 基本一致,忽略
            invokeBeanFactoryPostProcessors(beanFactory);
            registerBeanPostProcessors(beanFactory);
            initMessageSource();
            initApplicationEventMulticaster();
            onRefresh();
            registerListeners();
            finishBeanFactoryInitialization(beanFactory);
            finishRefresh();
        }

        catch (BeansException ex) {
            if (logger.isWarnEnabled()) {
                logger.warn("Exception encountered during context initialization - " +
                        "cancelling refresh attempt: " + ex);
            }

            destroyBeans();
            cancelRefresh(ex);
            throw ex;
        }

        finally {
            resetCommonCaches();
        }
    }
}

AbstractRefreshableWebApplicationContext 重寫了 postProcessBeanFactory 方法

AbstractRefreshableWebApplicationContext#postProcessBeanFactory

@Override
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
    // servlet-context aware 後處理器
    beanFactory.addBeanPostProcessor(new ServletContextAwareProcessor(this.servletContext, this.servletConfig));
    beanFactory.ignoreDependencyInterface(ServletContextAware.class);
    beanFactory.ignoreDependencyInterface(ServletConfigAware.class);

    // 註冊 scope: request, session, application; 
    // bean 依賴: ServletRequest, ServletResponse, HttpSession, WebRequest, beanFactory
    WebApplicationContextUtils.registerWebApplicationScopes(beanFactory, this.servletContext);

    // 註冊 servletContext, servletConfig, contextParameters, contextAttributes
    WebApplicationContextUtils.registerEnvironmentBeans(beanFactory, this.servletContext, this.servletConfig);
}

這些方法都比較簡單,不再展開

SpringMVC 通過兩種方式創建 WebApplicationContext

一種是通過 ContextLoaderListener, 它創建的 WebApplicationContext 稱為 root application context,或者說根容器。一個 ServletContext 中只能有一個根容器,而一個 web application 中只能有一個 ServletContext,因此一個 web 應用程式中只能有一個根容器,根容器的作用和 ServletContext 類似,提供了一個全局的訪問點,可以用於註冊多個 servlet 共用的業務 bean。 根容器不是必要的

另一種是通過 DispatcherServlet, 它創建的 WebApplicationContext,稱為上下文容器,上下文容器只在 DispatcherServlet 範圍內有效。DispatcherServlet 本質上是一個 Servlet,因此可以有多個 DispatcherServlet,也就可以有多個上下文容器。

如果上下文容器的 parent 為 null, 並且當前 ServletContext 中存在根容器,則把根容器設為他的父容器。

二、 啟動過程

ContextLoaderListener

一般我們會配置(web.xml 或 java-based)一個 ContextLoaderListener, 它實現了 ServletContextListener 介面, 主要用來載入根容器。

根據 Servelet 規範,這個 Listener 會在 ServletContext 創建時執行 ServletContextListener#contextInitialized 方法。

相關代碼如下:

@Override
public void contextInitialized(ServletContextEvent event) {
    initWebApplicationContext(event.getServletContext());
}

ContextLoader#initWebApplicationContext

/**
 * Initialize Spring's web application context for the given servlet context,
 * using the application context provided at construction time, or creating a new one
 * according to the "{@link #CONTEXT_CLASS_PARAM contextClass}" and
 * "{@link #CONFIG_LOCATION_PARAM contextConfigLocation}" context-params.
 * @param servletContext current servlet context
 * @return the new WebApplicationContext
 * @see #ContextLoader(WebApplicationContext)
 * @see #CONTEXT_CLASS_PARAM
 * @see #CONFIG_LOCATION_PARAM
 */
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    // 當前 ServletContext 中是否已經存在 root web applicationContext
    // 一個 ServletContext 中只能有一個根容器
    if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
        throw new IllegalStateException(
                "Cannot initialize context because there is already a root application context present - " +
                "check whether you have multiple ContextLoader* definitions in your web.xml!");
    }

    servletContext.log("Initializing Spring root WebApplicationContext");
    Log logger = LogFactory.getLog(ContextLoader.class);
    if (logger.isInfoEnabled()) {
        logger.info("Root WebApplicationContext: initialization started");
    }
    long startTime = System.currentTimeMillis();

    try {
        // context 可以通過構造方法傳入(這個在 java config 方式會用到)
        if (this.context == null) {
            // 若 web application 為空,創建一個, 這個一般是 web.xml 方式配置的
            this.context = createWebApplicationContext(servletContext);
        }
        if (this.context instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
            if (!cwac.isActive()) {
                // The context has not yet been refreshed -> provide services such as
                // setting the parent context, setting the application context id, etc
                if (cwac.getParent() == null) {
                    // The context instance was injected without an explicit parent ->
                    // determine parent for root web application context, if any.
                    ApplicationContext parent = loadParentContext(servletContext);
                    cwac.setParent(parent);
                }
                // 設置 ID, ServletContext, contextConfigLocation
                // 執行 refresh 操作
                configureAndRefreshWebApplicationContext(cwac, servletContext);
            }
        }

        // 將 web application context 放進 servlet context 中
        // 因此可以調用 servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) 拿到這個 WebApplicationContext
        // 更簡單的方法是通過 SpringMVC 提供的工具類 WebApplicationContextUtils.getWebApplicationContext(servletContext)
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

        ClassLoader ccl = Thread.currentThread().getContextClassLoader();
        if (ccl == ContextLoader.class.getClassLoader()) {
            currentContext = this.context;
        }
        else if (ccl != null) {
            currentContextPerThread.put(ccl, this.context);
        }

        if (logger.isInfoEnabled()) {
            long elapsedTime = System.currentTimeMillis() - startTime;
            logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
        }

        return this.context;
    }
    catch (RuntimeException | Error ex) {
        logger.error("Context initialization failed", ex);
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
        throw ex;
    }
}

如果 context 為 null, 則創建一個

ContextLoader#createWebApplicationContext

// web.xml 配置方式需要調用此方法創建 WebApplicationContext
// java-config 一般通過構造方法傳入,不需要再創建,上面有示例
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
    // 決定使用哪個 WebApplicationContext 的實現類
    Class<?> contextClass = determineContextClass(sc);
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
                "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
    }
    // 調用工具類實例化一個 WebApplicationContext
    return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

ContextLoader#determineContextClass 根據 ContextLoader.CONTEXT_CLASS_PARAM 確定使用哪個 WebApplicationContext 的實現類。

protected Class<?> determineContextClass(ServletContext servletContext) {
    // CONTEXT_CLASS_PARAM = "contextClass", 即在 web.xml 中配置的初始化參數 contextClass
    String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
    if (contextClassName != null) {
        try {
            return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
        }
        catch (ClassNotFoundException ex) {
            throw new ApplicationContextException(
                    "Failed to load custom context class [" + contextClassName + "]", ex);
        }
    }
    else {
        // 如果未配置 contextClass, 從 defaultStrategies 屬性文件中獲取,下麵會說到
        contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
        try {
            return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
        }
        catch (ClassNotFoundException ex) {
            throw new ApplicationContextException(
                    "Failed to load default context class [" + contextClassName + "]", ex);
        }
    }
}

若 contextClass 未指定,則從 defaultStrategies 這個 Properties 中獲取,他預設載入 ClassPath 路徑下, ContextLoader.properties 文件中配置的類,預設為 XmlWebApplicationContext。

// 屬性文件中的類為 XmlWebApplicationContext。
private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";


private static final Properties defaultStrategies;

// 靜態載入 XmlWebApplicationContext 到 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, ContextLoader.class);
        defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
    }
    catch (IOException ex) {
        throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
    }
}

確定 class 後,反射實例化一個 WebApplicationContext 的實現類,一個"裸"的根容器創建出來了。

想一想,平時為啥創建 ApplicationContext?

作為 Bean 容器,當然是用來管理 Bean 了。

既然用來管理 Bean,是不是應該先把 Bean 放進去? 通過 xml? 註解? 或者乾脆直接調用 register 方法註冊? 然後是不是應該 refresh 一下?配置一些 post-processer,設置一些參數,提前創建 Singleton?

ContextLoader#configureAndRefreshWebApplicationContext

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        // The application context id is still set to its original default value
        // -> assign a more useful id based on available information
        String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
        if (idParam != null) {
            wac.setId(idParam);
        }
        else {
            // Generate default id...
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                    ObjectUtils.getDisplayString(sc.getContextPath()));
        }
    }

    // WebApplication 會持有當前 ServletContext
    wac.setServletContext(sc);
    // CONFIG_LOCATION_PARAM = "contextConfigLocation", web.xml 裡面配置參數 
    // root web application context 的 Bean 配置文件
    String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
    if (configLocationParam != null) {
        wac.setConfigLocation(configLocationParam);
    }

    // The wac environment's #initPropertySources will be called in any case when the context
    // is refreshed; do it eagerly here to ensure servlet property sources are in place for
    // use in any post-processing or initialization that occurs below prior to #refresh
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        // 初始化屬性資源,占位符等
        // 在這裡調用確保 servlet 屬性資源在 post-processing 和 initialization 階段是可用的
        ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
    }

    // ApplicationContextInitializer 回調介面,在 refresh 之前定製一些信息
    customizeContext(sc, wac);

    // 所有的 ApplicationContext 調用 refresh 之後才可用,此方法位於
    // AbstractApplication,它統一了 ApplicationContext 初始化的基本
    // 流程,子類(包括 WebApplicationContext 的實現類)通過鉤子方法
    //(模版方法)定製一些自己的需求
    // web refresh 流程上面以已經說過
    wac.refresh();
}

小結: ContextLoaderListener 的初始化流程可以用下麵的代碼表示

def initWebApplicationContext():
    if context == null:
        context = createWebApplicationContext()

    configureAndRefreshWebApplicationContext(context)

DispatcherServlet 初始化流程

SpringMVC 將前端的所有請求都交給 DispatcherServlet 處理,他本質上是一個 Servlet,可以通過 web.xml 或者 java config 方式配置。

DispatcherServlet 類圖

dispatcher-servlet

SpringMVC 將 DispatcherServlet 也當做一個 bean 來處理,所以對於一些 bean 的操作同樣可以作用於 DispatcherServlet, 比如相關 *Aware 介面。

Servlet 容器會在啟動時調用 init 方法。完成一些初始化操作,其調用流程如下:

HttpServletBean#init -> FrameworkServlet#initServletBean -> FrameworkServlet#initWebApplicationContext

前面兩個方法比較簡單,主要是 initWebApplicationContext

FrameworkServlet#initWebApplicationContext

/**
 * Initialize and publish the WebApplicationContext for this servlet.
 * <p>Delegates to {@link #createWebApplicationContext} for actual creation
 * of the context. Can be overridden in subclasses.
 * @return the WebApplicationContext instance
 * @see #FrameworkServlet(WebApplicationContext)
 * @see #setContextClass
 * @see #setContextConfigLocation
 */
protected WebApplicationContext initWebApplicationContext() {
    // 獲取根容器,ServletContext 持有的 WebApplicationContext
    WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    // 上下文容器,當前 DispatcherServlet 持有的 WebApplicationContext
    WebApplicationContext wac = null;

    // web application 可以通過構造方法傳入, java-config 方式會用到
    if (this.webApplicationContext != null) {
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            if (!cwac.isActive()) {
                // The context has not yet been refreshed -> provide services such as
                // setting the parent context, setting the application context id, etc
                if (cwac.getParent() == null) {
                    // The context instance was injected without an explicit parent -> set
                    // the root application context (if any; may be null) as the parent
                    cwac.setParent(rootContext);
                }

                // 配置刷新 web application context, 下麵會說到
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    if (wac == null) {
        // No context instance was injected at construction time -> see if one
        // has been registered in the servlet context. If one exists, it is assumed
        // that the parent context (if any) has already been set and that the
        // user has performed any initialization such as setting the context id
        wac = findWebApplicationContext();
    }
    if (wac == null) {
        // 如果通過 web.xml 方式配置,此時 wac 為空,創建一個,預設 XmlWebApplicationContext
        // 配置文件位置 contextConfigLocation 在這裡載入
        // 這個方法比較簡單,不再贅述
        wac = createWebApplicationContext(rootContext);
    }

    if (!this.refreshEventReceived) {
        // 初始化 SpringMVC 處理過程中面向不同功能的策略對象
        // 比如 MultipartResolver, HandlerMappings, ViewResolvers 等
        synchronized (this.onRefreshMonitor) {
            onRefresh(wac);
        }
    }

    if (this.publishContext) {
        // 將 DispatcherServlet 持有的 web application context 放進 ServletContext
        // 命名規則為 SERVLET_CONTEXT_PREFIX + dispatcherServlet 名字
        // SERVLET_CONTEXT_PREFIX = FrameWorkServlet.class.getName() + ".CONTEXT."
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
    }

    return wac;
}

DispatcherServlet 持有的 WebApplicationContext 可以通構造方法傳入,或者 createWebApplicationContext 方法創建

創建容器步驟和 ContextLoader#createWebApplicationContext 有所不同

FrameworkServlet#createWebApplicationContext

// web.xml 配置方式需要調用此方法創建 WebApplicationContext
// java-config 一般通過構造方法傳入,不需要再創建,上面有示例
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
    // 返回 WebApplicationContext 的實現類,預設為 XmlWebApplicationContext
    Class<?> contextClass = getContextClass();
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException(
                "Fatal initialization error in servlet with name '" + getServletName() +
                "': custom WebApplicationContext class [" + contextClass.getName() +
                "] is not of type ConfigurableWebApplicationContext");
    }
    ConfigurableWebApplicationContext wac =
            (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

    wac.setEnvironment(getEnvironment());
    // parent 為 rootContext
    wac.setParent(parent);

    // 獲 bean 取配置文件位置
    String configLocation = getContextConfigLocation();
    if (configLocation != null) {
        wac.setConfigLocation(configLocation);
    }

    // 配置,刷新容器,下麵會說到
    configureAndRefreshWebApplicationContext(wac);

    return wac;
}

FrameworkServlet#configureAndRefreshWebApplicationContext

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        // The application context id is still set to its original default value
        // -> assign a more useful id based on available information
        if (this.contextId != null) {
            wac.setId(this.contextId);
        }
        else {
            // Generate default id...
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                    ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
        }
    }

    // 配置 Servlet 相關信息
    wac.setServletContext(getServletContext());
    wac.setServletConfig(getServletConfig());
    wac.setNamespace(getNamespace());
    wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

    // The wac environment's #initPropertySources will be called in any case when the context
    // is refreshed; do it eagerly here to ensure servlet property sources are in place for
    // use in any post-processing or initialization that occurs below prior to #refresh
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        // 初始化屬性資源,占位符等
        // 在這裡調用確保 servlet 屬性資源在 post-processing 和 initialization 階段是可用的
        ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
    }

    // 空方法,可以在 refresh 之前配置一些信息
    postProcessWebApplicationContext(wac);
    // ApplicationContextInitializer 回調介面
    applyInitializers(wac);
    // 所有的 ApplicationContext 調用 refresh 之後才可用,此方法位於
    // AbstractApplication,它統一了 ApplicationContext 初始化的基本
    // 流程,子類(包括 WebApplicationContext 的實現類)通過鉤子方法
    //(模版方法)定製一些自己的需求
    // web refresh 流程上面以已經說過
    wac.refresh();
}

DispatcherServlet#onRefresh

/**
 * This implementation calls {@link #initStrategies}.
 */
@Override
protected void onRefresh(ApplicationContext context) {
    // 初始化面向不同功能的策略對象
    initStrategies(context);
}

DispatcherServlet#initStrategies

protected void initStrategies(ApplicationContext context) {
    initMultipartResolver(context);
    initLocaleResolver(context);
    initThemeResolver(context);
    initHandlerMappings(context);
    initHandlerAdapters(context);
    initHandlerExceptionResolvers(context);
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    initFlashMapManager(context);
}

這些策略方法的執行流程都是相似的,即從當前 context 中查找相應類型、相應名字的 bean,將他設為當前 DispatcherServlet 的成員變數。對於必須存在的 bean, 通過 DispatcherServlet.properties 文件提供。下麵以 initHandlerMappings 為例說明

DispatcherServlet#initHandlerMappings

private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;

    // 預設為 true, 表示查找所有的 HandlerMappings 實現類
    if (this.detectAllHandlerMappings) {
        // 從 ApplicationContext(包括父容器)中查找所有的 HandlerMappings
        Map<String, HandlerMapping> matchingBeans =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerMappings = new ArrayList<>(matchingBeans.values());
            // We keep HandlerMappings in sorted order.
            AnnotationAwareOrderComparator.sort(this.handlerMappings);
        }
    }
    else {
        try {
            // 只載入名字為 handlerMapping 的 HandlerMapping
            HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
            this.handlerMappings = Collections.singletonList(hm);
        }
        catch (NoSuchBeanDefinitionException ex) {
            // Ignore, we'll add a default HandlerMapping later.
        }
    }

    // 確保至少有一個 HandlerMapping
    if (this.handlerMappings == null) {
        // 載入預設的 HandlerMapping, 下麵會說到
        this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
        if (logger.isTraceEnabled()) {
            logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
                    "': using default strategies from DispatcherServlet.properties");
        }
    }
}

DispatcherServlet#getDefaultStrategies

protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
    String key = strategyInterface.getName();
    // 從 defaultStrategies 這個 Properties 中獲取
    String value = defaultStrategies.getProperty(key);
    
    // 後面反射創建 value 類,省略
    ...
}

下麵位於 DispatcherServlet

// 靜態載入 DispatcherServlet.properties 文件中的類到 defaultStrategies
static {
    
    try {
        // DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";
        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#onRefresh 之前將需要的策略類註冊進 context, 它們會在 onRefresh 之後生效。

小結: DispatcherServlet 的初始化流程可以表示為

def initWebApplicationContext():
    rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext())
  
    if context == null:
        context = createWebApplicationContext(rootContext)

    context.setParent(rootContext)
    configureAndRefreshWebApplicationContext(context)

    onRefresh(context)

三、 請求處理

DispatcherServlet 處理流程

DispatcherServlet 中主要組件的簡析

Handler

處理器。請求對應的處理方法,@Controller 註解的類的方法

HandlerInterceptor

攔截器。在 handler 執行前後及視圖渲染後執行攔截,可以註冊不同的 interceptor 定製工作流程


public interface HandlerInterceptor {

    // 在 handler 執行前攔截,返回 true 才能繼續調用下一個 interceptor 或者 handler
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }

    // 在 handler 執行後,視圖渲染前進行攔截處理
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
        @Nullable ModelAndView modelAndView) throws Exception {
    }

    // 視圖渲染後,請求完成後進行處理,可以用來清理資源
    // 除非 preHandle 放回 false,否則一定會執行,即使發生錯誤
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
        @Nullable Exception ex) throws Exception {
    }

}

HandlerExecutionChain

處理器執行鏈。裡面包含 handler 和 interceptors

HandlerMapping

處理器映射器。request -> handler 的映射。主要有 BeanNameUrlHandlerMapping 和 RequestMappingHandlerMapping 兩個實現類

BeanNameUrlHandlerMapping 將 bean 名字作為 url 映射到相應的 handler, 也就是說 bean 名字必須是這種形式的: "/foo", "/bar",這個應該是比較老的東西了

RequestMappingHandlerMapping 使用 @RequestMapping 註解將 url 和 handler 相關聯。

HandlerAdapter

處理器適配器。適配器模式,通過他來調用具體的 handler

ViewResolver

視圖解析器。其中的 resolveViewName 方法可以根據視圖名字,解析出對應的 View 對象。可以配置不同的 viewResolver 來解析不同的 view, 常見的如 Jsp, Xml, Freemarker, Velocity 等

View

視圖。不同的 viewResolver 對應不同 View 對象,調用 view.render 方法渲染視圖

SpringMVC 處理請求流程圖

mvc-process

  1. 客戶端發出請求,會先經過 filter 過濾,通過的請求才能到達 DispatcherServlet。
  2. DispatcherServlet 通過 handlerMapping 找到請求對應的 handler,返回一個 HandlerExecutionChain 裡面包含 interceptors 和 handler
  3. DispatcherServlet 通過 handlerAdapter 調用實際的 handler 處理業務邏輯, 返回 ModelAndView。裡面會包含邏輯視圖名和 model 數據。註意,在此之前和之後,會分別調用 interceptors 攔截處理
  4. 調用 viewResolver 將邏輯視圖名解析成 view 返回
  5. 調用 view.render 渲染視圖,寫進 response。然後 interceptors 和 filter 依次攔截處理,最後返回給客戶端

下麵結合源碼看一看

DispatcherServlet 請求處理源碼解析

DispatcherServlet 是一個 servlet,他的調用流程大致如下

HttpServlet#service -> FrameworkServlet#processRequest -> DispatcherServlet#doService -> DispatcherServlet#doDispatch

DispatcherServlet#doDispatch

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

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

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

        try {
            // 檢查是否為文件上傳請求
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // 通過 handlerMapping 查到請求對應的 handler
            // 返回 HandlerExecutionChain 裡面包含 handler 和 interceptors
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // 根據 handler 匹配對應的 handlerAdapter
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            // Process last-modified header, if supported by the handler.
            String method = request.getMethod();
            boolean isGet = "GET".equals(method);
            if (isGet || "HEAD".equals(method)) {
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return;
                }
            }

            // 攔截器前置處理,調用 HandlerInterceptor#preHandle
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            // 調用 handler, 就是 @Controller 註解的類對應的方法
            // 如果是一個 rest 請求,mv 為 null,後面不會再調用 render 方法
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }

            // 設置 viewName, 後面會根據 viewName 找到對應的 view
            applyDefaultViewName(processedRequest, mv);

            // 攔截器後置處理,調用 HandlerInterceptor#postHandle
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        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);
        }

        // 結果處理,錯誤,視圖等
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
        // 攔截器結束處理, 調用 HandlerInterceptor#afterCompletion
        // 即使發生錯誤也會執行
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
    catch (Throwable err) {
        triggerAfterCompletion(processedRequest, response, mappedHandler,
                new NestedServletException("Handler processing failed", err));
    }
    finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            // Instead of postHandle and afterCompletion
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        }
        else {
            // Clean up any resources used by a multipart request.
            if (multipartRequestParsed) {
                cleanupMultipart(processedRequest);
            }
        }
    }
}

processDispatchResult 會進行異常處理(如果存在的話),然後調用 render 方法渲染視圖

DispatcherServlet#render

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    // Determine locale for request and apply it to the response.
    Locale locale =
            (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
    response.setLocale(locale);

    View view;
    // 這個是 @Controller 方法返回的名字 
    String viewName = mv.getViewName();
    if (viewName != null) {
        // 調用 viewResolver 解析視圖,返回一個視圖對象
        // 會遍歷 viewResolvers 找到第一個匹配的處理, 返回 View 對象
        view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
        if (view == null) {
            throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
                    "' in servlet with name '" + getServletName() + "'");
        }
    }
    else {
        // 已經有視圖對象
        view = mv.getView();
        if (view == null) {
            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 {
        if (mv.getStatus() != null) {
            response.setStatus(mv.getStatus().value());
        }

        // 渲染,不同的 viewResolver 會有不同的邏輯實現
        view.render(mv.getModelInternal(), request, response);
    }
    catch (Exception ex) {
        if (logger.isDebugEnabled()) {
            logger.debug("Error rendering view [" + view + "]", ex);
        }
        throw ex;
    }
}

總結

  1. SpringMVC 是基於 Servlet 的, 因此 SpringMVC 的啟動流程基於 Servlet 的啟動流程

  2. ServletContext 持有的 WebApplicationContext 稱為根容器; 根容器在一個 web 應用中都可以訪問到,因此可以用於註冊共用的 bean;如果不需要可以不創建,根容器不是必要的

  3. 根容器是指在 ServletContext 中以 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 為 key 的 WebApplicationContext。根容器並不一定要由 ContextLoaderListener 創建。

  4. DispatcherServlet 持有的 WebApplicationContext 稱為它的上下文容器;每個 DispatcherServlet 都會持有一個上下文容器(自己創建或者構造傳入)。

  5. SpringMVC 的處理流程並不一定按上面的順序執行,比如如果是 json 請求,HandlerAdapter 調用 handler 處理後返回的 mv 可能是 null, 後面就不會進行視圖渲染

  6. 請求如果沒有到達 DispatcherServlet 可能是被過濾器過濾了(許可權?異常?);一定不是被攔截器攔截的,因為攔截器在 DispatcherServlet 內部執行。

  7. 除非請求被 interceptor#preHandle 攔截,否則 interceptor#afterCompletion 一定會執行,即使發生錯誤。

  8. 獲取 WebApplicationContext, 除了相關 Aware 介面,還可以通過 WebApplicationContextUtils.getWebApplicationContext 獲取根容器,相關原理在這裡, 或者通過 RequestContextUtils.findWebApplicationContext 獲取當前 DispatcherServlet 對應的上下文容器,相關代碼在 DispatcherServlet#doService

  9. SpringBoot 應用部署到外部容器的時候為啥要繼承 SpringBootServletInitializer, 因為它是一個 WebApplicationContextInitializer, 具體見這裡

備註

以上相關代碼基於 SpringBoot 2.1.6.RELEASE, SpringMVC 5.1.6.RELEASE, Servlet 3.0

結語

寫了不少,算是對 SpringMVC 的一次複習了。能力有限,如有不正確的地方,歡迎拍磚!

參考:

  1. servlet API
  2. Spring MVC

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 用for迴圈或者forEach遍曆數組的話,在刪除第一個元素的時候,會導致數組改變,從而導致splice中的i不是想要的i,解決方法是用filter或者逆向迴圈 1、使用filter 2、使用逆向迴圈 let arr = [2, 3, 5, 7] for (let i = arr.length - ...
  • 問題:由於業務需要,我們需要判斷圖片能否正常的載入,如果未正常載入的話,需要顯示一張預設圖片; 方案:1,由於後臺返回的是一個圖片id數組,例如 imgList=['343313131','21333413244','3312w232211'],圖片的完整路徑應為http://公司伺服器地址/xxx ...
  • Redux是一個可預測的狀態容器,提供可預測的狀態管理。 什麼是狀態?狀態其實也就是數據,管理狀態也就是對數據的管理。那麼什麼是可預測的狀態管理呢?能夠監聽數據的變化,獲取變化的來源,在發生變化時可以觸發某些動作,即為狀態管理(個人理解)。 如上所示,就是一個最簡單的狀態管理,實現了數據基本的管理, ...
  • 在標簽中經常會用到alt標簽,這對於seo的優化是有一定的幫助。 ...
  • 本篇筆記只敘述 var 與 let 的區別 var 是可以進行變數的提升的,好比先定義一個變數,不指定類型,後面再用 var 來聲明它,於是它從無類型變成了有類型,但是這樣是不好的 當你使用 var 時,可以根據需要多次聲明相同名稱的變數,但是 let 不能。 而對面 let 來說,它更像我們學的 ...
  • jQueryt靜態方法詳解 ==> each ==> map ==> trim ==> isArray ==> isFunction ==> isWindow ==> holdReady 一,each方法 註:為了更好的展示,首先創建一個數組和一個對象 (let 與 arr 區別詳解見 JavaSc ...
  • 分散式事務的挑戰 在多個服務、資料庫和消息代理之間維持數據的一致性的傳統方式是採用分散式事務。分散式的事實標註是XA、XA採用了兩階段提交老保證事務中的所有參與方同時完成提交,或者失敗時同時回滾。應用程式的整個技術棧需要滿足XA標準。 許多新技術,包括NoSQLshujk ,liru MongoDB ...
  • 背景 自己手上有一個項目服務用的是AWS EC2,最近從安全性和性能方面考慮,最近打算把騰訊雲的MySQL資料庫遷移到AWS RDS上,因為AWS的出口規則和安全組等問題,我需要修改預設的3306埠和Bind Address限制特定的IP訪問,我在Stackoverflow上查詢瞭如何修改,但是網 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...