主頁 > 後端開發 > SpringMVC這篇文章吃透了,最少最少漲5000

SpringMVC這篇文章吃透了,最少最少漲5000

2021-09-26 13:05:22 後端開發

文末可以領取所有系列高清 pdf,

大家好,我是路人,這是 SpringMVC 系列第 17 篇,

本文將通過閱讀原始碼的方式帶大家了解 springmv 容器啟動的程序,SpringMVC 中的各種組件都是在容器啟動的程序中組裝的,所以如果熟悉整個程序后,你可以隨意對 SpringMVC 進行擴展,SpringMVC 會被你玩的出神入化,

目錄

  • 1、前言

  • 2、回顧全注解方式 2 個關鍵類

    • 2.1、MVC 初始化類

    • 2.2、MVC 配置類

  • 2、SpringMVC 容器的生命周期(9 個階段)

  • 3、階段 1:Servlet 容器初始化

    • 3.1、ServletContainerInitializer

    • 3.2、SpringServletContainerInitializer

    • 3.3、SpringServletContainerInitializer#onStartup 方法

    • 3.4、WebApplicationInitializer:web 應用初始化

    • 3.5、進入 AbstractDispatcherServletInitializer#onStartup 方法

  • 4、階段 2:創建父容器

    • 4.1、程序

    • 4.2、①:負責創建父容器

    • 4.2、②:創建 ContextLoaderListener 監聽器

  • 5、階段 3&4:創建 springmvc 容器&注冊 DispatcherServlet

  • 6、階段 5:啟動父容器:ContextLoaderListener

    • 6.1、程序

    • 6.2、代碼 ①:配置父容器以及啟動父容器

    • 6.3、ApplicationContextInitializer 介面:容器啟動前用來初始化容器

  • 7、階段 6:啟動 springmvc 容器:DispatcherServlet#init()

    • 7.1、DispatcherServlet 類圖

    • 7.2、HttpServletBean#init()

    • 7.3、FrameworkServlet#initServletBean

    • 7.4、FrameworkServlet#initWebApplicationContext

    • 7.5、FrameworkServlet#configureAndRefreshWebApplicationContext

  • 8、階段 7:springmvc 容器啟動程序中處理@WebMVC

    • 8.1、SpringMVC 配置類被處理

    • 8.2、@EnableWebMvc:配置 springmvc 所需組件

    • 8.3、進入 DelegatingWebMvcConfiguration 類

    • 8.4、WebMvcConfigurationSupport:配置 springmvc 所需所有組件

    • 8.5、WebMvcConfigurer 介面

  • 9、階段 8:組裝 DispatcherServlet 中各種 SpringMVC 需要的組件

    • 9.1、觸發 ContextRefreshListener 監聽器

    • 9.2、進入 FrameworkServlet.this.onApplicationEvent(event);

    • 9.3、進入 DispatcherServlet#onRefresh

    • 9.4、DispatcherServlet#initStrategies:初始化 DispatcherServlet 中的組件

    • 9.5、initHandlerMappings(context);

    • 9.6、DispatcherServlet#getDefaultStrategies:兜底的方案查找組件

  • 10、階段 9:銷毀容器

    • 10.1、銷毀 springmvc 容器:DispatcherServlet#destroy

    • 10.2、銷毀父容器:ContextLoaderListener#contextDestroyed

  • 11、帶大家 debug 代碼

    • 11.1、拉取原始碼

    • 11.2、將下面這個模塊發布到 tomcat

    • 11.2、按照下面配置設定斷點,啟動,除錯代碼

  • 12、SpringMVC 系列目錄

  • 13、更多好文章

  • 14、【路人甲 Java】所有系列高清 PDF

1、前言

SpringMVC,建議大家使用全注解的方式,全注解的方式及原理不了解的,先去看一下這篇文章:SpringMVC 全注解方式如何使用?

上面的文章看懂以后再來看本文,否則將出現消化不良的現象,

本文以全注解的方式為基礎,來解說SpringMVC容器啟動的整個流程,

2、回顧全注解方式 2 個關鍵類

全注解的方式重點就在于 2 個類:MVC 初始化類、MVC 配置類

2.1、MVC 初始化類

代碼如下,這個類需要繼承 AbstractAnnotationConfigDispatcherServletInitializer,會有 web 容器來呼叫,這個類中有 4 個方法需要實作,干了 4 件事情

  • getRootConfigClasses():獲取父容器的配置類

  • getServletConfigClasses():獲取 springmvc 容器的配置類,這個配置類相當于 springmvc xml 組態檔的功能

  • getServletMappings():獲取 DispatcherServlet 能夠處理的 url,相當于 web.xml 中為 servlet 指定的 url-pattern

  • getServletFilters():定義所有的 Filter

/**
 * ①:1、創建Mvc初始化類,需要繼承AbstractAnnotationConfigDispatcherServletInitializer類
 */
public class MvcInit extends AbstractAnnotationConfigDispatcherServletInitializer {
    /**
     * springmvc容器的父容器spring配置類
     * 實際作業中我們的專案比較復雜,可以將controller層放在springmvc容器中
     * 其他層,如service層、dao層放在父容器了,bean管理起來更清晰一些
     * 也可以沒有父容器,將所有bean都放在springmvc容器中
     *
     * @return
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[0];
    }

    /**
     * ②:2、設定springmvc容器的spring配置類
     *
     * @return
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{MvcConfig.class};
    }

    /**
     * ③:3、配置DispatcherServlet的url-pattern
     *
     * @return
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    /**
     * ④:4、注冊攔截器
     *
     * @return
     */
    @Override
    protected Filter[] getServletFilters() {
        //添加攔截器,解決亂碼問題
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceRequestEncoding(true);
        characterEncodingFilter.setForceResponseEncoding(true);
        return new Filter[]{characterEncodingFilter};
    }
}

2.2、MVC 配置類

代碼如下,這個配置類相當于 springmvc xml 組態檔的功能,可以在里面定義 springmvc 各種組件

/**
 * 1.開啟springmvc注解配置
 * 2、配置視圖決議器
 * 3、配置截器
 * 4、配置靜態資源訪問
 * 5、組態檔上傳決議器
 * 6、配置全域例外處理器
 */
@Configuration
@ComponentScan("com.javacode2018.springmvc.chat12")
@EnableWebMvc //1:使用EnableWebMvc開啟springmvc注解方式配置
public class MvcConfig implements WebMvcConfigurer {

    /**
     * 2、添加視圖決議器(可以添加多個)
     *
     * @param registry
     */
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/view/");
        resolver.setSuffix(".jsp");
        resolver.setOrder(Ordered.LOWEST_PRECEDENCE);
        registry.viewResolver(resolver);
    }

    @Autowired
    private MyInterceptor myInterceptor;

    /**
     * 3、添加攔截器(可以添加多個)
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(this.myInterceptor).addPathPatterns("/**");
    }


    /**
     * 4、配置靜態資源訪問處理器
     *
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("/static/");
    }

    /**
     * 5、組態檔上傳決議器
     *
     * @return
     */
    @Bean
    public CommonsMultipartResolver multipartResolver() {
        CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver();
        //maxUploadSizePerFile:單個檔案大小限制(byte)
        //maxUploadSize:整個請求大小限制(byte)
        commonsMultipartResolver.setMaxUploadSizePerFile(10 * 1024 * 1024);
        commonsMultipartResolver.setMaxUploadSize(100 * 1024 * 1024);
        return commonsMultipartResolver;
    }
}

2、SpringMVC 容器的生命周期(9 個階段)

  1. 階段 1:Servlet 容器初始化

  2. 階段 2:創建父容器

  3. 階段 3:創建 springmvc 容器

  4. 階段 4:Servlet 容器中注冊 DispatcherServlet

  5. 階段 5:啟動父容器:ContextLoaderListener

  6. 階段 6:啟動 springmvc 容器:DispatcherServlet#init()

  7. 階段 7:springmvc 容器啟動程序中處理@WebMVC

  8. 階段 8:組裝 DispatcherServlet 中各種 SpringMVC 需要的組件

  9. 階段 9:銷毀 2 個容器

3、階段 1:Servlet 容器初始化

3.1、ServletContainerInitializer

咱們知道 servlet3.0 中新增了一個介面:ServletContainerInitializer,這個介面功能特別的牛逼,有了它之后,web.xml 組態檔可要可不要了,

public interface ServletContainerInitializer {
    public void onStartup(Set<Class<?>> c, ServletContext ctx)
        throws ServletException;
}

這個介面的實作類,如果滿足下面 2 個條件,Servlet 容器啟動的程序中會自動實體化這些類,然后呼叫他們的 onStartUp 方法,然后我們就可以在這些類的 onStartUp 方法中干活了,在 web.xml 干的所有事情,都可以在這個方法中干,特別強大:

  1. 這個類必須實作 ServletContainerInitializer 介面,且非抽象類

  2. 這個類的全類名必須要放在META-INF/services/javax.servlet.ServletContainerInitializer這個檔案中

3.2、SpringServletContainerInitializer

下面重點來了,springmvc 提供了一個類SpringServletContainerInitializer,滿足了上面個條件,

b5e2cd12a8f5245e0fa85a716e6cde5c.png

spring-web-5.3.6.jar!\META-INF\services\javax.servlet.ServletContainerInitializer

cfe152a75ec058aea76cf740a1bc4cd6.png

所以 SpringServletContainerInitializer 的 onStart 方法會 servlet 容器自動被呼叫

3.3、SpringServletContainerInitializer#onStartup 方法

這個類的原始碼,大家先看一下,這個類干的事情:

  1. 類上有@HandlesTypes(WebApplicationInitializer.class) 這個注解,注解的值為WebApplicationInitializer.class,所以 onStartup 方法的第一個引數是WebApplicationInitializer型別的集合,這個集合由 web 容器自動掃描獲取,然后傳入進來

  2. 實體化 WebApplicationInitializer 集合

  3. 對 WebApplicationInitializer 集合進行排序

  4. 回圈呼叫 WebApplicationInitializer 的 onStartup 方法

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

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

  List<WebApplicationInitializer> initializers = Collections.emptyList();

  if (webAppInitializerClasses != null) {
   initializers = new ArrayList<>(webAppInitializerClasses.size());
   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);
  }
 }

}

下面重點要看WebApplicationInitializer介面了,

3.4、WebApplicationInitializer:web 應用初始化

介面比較簡單,就一個方法,引數是 servlet 背景關系物件,有了個物件,可以干 web.xml 中的一切事情了,比如注冊 servlet、filter、監聽器等等

public interface WebApplicationInitializer {

 void onStartup(ServletContext servletContext) throws ServletException;

}

如下圖,看一下類的繼承關系,咱們的 MvcInit 就實作了這個介面,所以 MvcInit 的 onStartup 方法會被調費用

988e006c67479cb1e95d14084db6ed48.png

關鍵代碼在這 3 個類中

148177da348ac47b14dd4920661da108.png

3.5、進入 AbstractDispatcherServletInitializer#onStartup 方法

public void onStartup(ServletContext servletContext) throws ServletException {
    super.onStartup(servletContext);
    registerDispatcherServlet(servletContext);
}

這里是重點:這個方法中干了 4 件事情

  1. 創建父容器,只是實體化了,并未啟動

  2. 創建了監聽器 ContextLoaderListener,這是一個 ServletContextListener 型別的監聽器,稍后會在這個監聽器中啟動父容器

  3. 創建 springmvc 容器,只是實體化了,并未啟動,啟動的事情會在 DispatcherServlet#init 中做,稍后會說

  4. Servlet 容器中注冊 DispatcherServlet

下面,咱們來詳細看這幾個步驟,把這幾個步驟作為階段來解讀,

4、階段 2:創建父容器

父容器可有可無,并不是必須的,為了更好的管理 bean,springmvc 建議我們用父子容器,controller 之外的 bean,比如 service,dao 等,建議放到父容器中,controller 層的和 springmvc 相關的一些 bean 放在 springmvc 容器中,咱們繼續,

4.1、程序

AbstractDispatcherServletInitializer#onStartup方法中會呼叫父類的onStartup,即AbstractContextLoaderInitializer#onStartup,我們進到這個方法中,代碼如下圖,干了 2 個事情

  1. 圖中編號 ①:創建父容器,只是實體化了,并未啟動

  2. 圖中編號 ②:創建了一個監聽器 ContextLoaderListener,這是一個 ServletContextListener 型別的監聽器,稍后會在這個監聽器中啟動父容器

6cd8e179cfead8df1cc9e86fb18933fb.png

下面來分別來細說下上面 2 段代碼干的活,

4.2、①:負責創建父容器

AbstractAnnotationConfigDispatcherServletInitializer#createRootApplicationContext,只是創建了一個AnnotationConfigWebApplicationContext物件,并將父容器配置類 rootConfigClass 注冊到容器中,并沒有啟動這個容器,若 rootConfigClass 為空,父容器不會被創建,所以父容器可有可無,

a4559bae2da16e6fd604f924643ff02d.png

4.2、②:創建 ContextLoaderListener 監聽器

代碼如下,創建的時候將父容器物件 rootAContext 傳進去了,

ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
//getRootApplicationContextInitializers()回傳置為ApplicationContextInitializer陣列,是個函式式介面,在父容器初始化的程序中,會作為一個擴展點預留給開發者用
listener.setContextInitializers(getRootApplicationContextInitializers());
servletContext.addListener(listener);

ContextLoaderListener,這是一個 ServletContextListener 型別的監聽器,所以在 web 容器啟動和銷毀的程序中會被呼叫,如下圖,這個監聽器干了 2 件事

  1. contextInitialized 方法:這個方法會在 web 容器啟動時被呼叫,內部負責啟動父容器

  2. 在 contextDestroyed 方法:這個方法會在 web 容器銷毀時被呼叫,內部負責關閉父容器

004085310864ba72bb537358a82a8654.png

5、階段 3&4:創建 springmvc 容器&注冊 DispatcherServlet

在回到AbstractDispatcherServletInitializer#onStartup,看這個方法的第二行,如下圖

b8b587130bc3c679757e68d88d73555f.png

registerDispatcherServlet原始碼如下

protected void registerDispatcherServlet(ServletContext servletContext) {
    //①:DispatcherServlet的servlet名稱,默認為:dispatcher
    String servletName = getServletName();

    //②:創建springmvc容器
    WebApplicationContext servletAppContext = createServletApplicationContext();

    //③:創建DispatcherServlet,注意這里將springmvc容器物件做為引數傳遞給DispatcherServlet了
    FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
    //設定ApplicationContextInitializer串列,可以對springmvc容器在啟動之前進行定制化
    dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());

    //④:將 dispatcherServlet 注冊到servlet背景關系中
    ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
    registration.setLoadOnStartup(1);
    registration.addMapping(getServletMappings());
    registration.setAsyncSupported(isAsyncSupported());

    //⑤:注冊Filter
    Filter[] filters = getServletFilters();
    if (!ObjectUtils.isEmpty(filters)) {
        for (Filter filter : filters) {
            registerServletFilter(servletContext, filter);
        }
    }
    //⑥:這個方法預留給咱們自己去實作,可以對dispatcherServlet做一些特殊的配置
    customizeRegistration(registration);
}

protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
    return new DispatcherServlet(servletAppContext);
}

6、階段 5:啟動父容器:ContextLoaderListener

6.1、程序

上面的onStartup方法執行完畢之后,會執行監聽器ContextLoaderListener的初始化,會進入到他的contextInitialized方法中

3edcdc262245d9f0b9ea91ab6c5e5905.png

initWebApplicationContext原始碼如下,截取了主要的幾行

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    //this.context就是父容器物件
    ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
    //①:配置及啟動父容器
    configureAndRefreshWebApplicationContext(cwac, servletContext);
    //將父容器丟到servletContext中進行共享,方便其他地方獲取
    servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
}

6.2、代碼 ①:配置父容器以及啟動父容器

//①:配置及啟動父容器
configureAndRefreshWebApplicationContext(cwac, servletContext);

configureAndRefreshWebApplicationContext方法關鍵代碼如下

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
    //①:定制上線文,這里主要是遍歷ApplicationContextInitializer串列,呼叫每個ApplicationContextInitializer#initialize方法來對容器進行定制,相當于一個擴展點,可以有程式員自己控制
    customizeContext(sc, wac);
    //②:重繪容器,就相當于啟動容器了,此時就會組裝里面的bean了
    wac.refresh();
}

customizeContext方法,我們進去看一下,這里涉及到了一個新的類,所以有必要去看一下,混個臉熟,原始碼如下,這是給開發者留的一個擴展點,通過ApplicationContextInitializer這個來做擴展,這是一個函式式介面,下面代碼會遍歷ApplicationContextInitializer串列,然后呼叫其initialize方法,我們可以在這個方法中對 spring 上線文進行定制

protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
  List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses =
    determineContextInitializerClasses(sc);

    for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
        Class<?> initializerContextClass =
            GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
        if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
            throw new ApplicationContextException(String.format(
                "Could not apply context initializer [%s] since its generic parameter [%s] " +
                "is not assignable from the type of application context used by this " +
                "context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),
                wac.getClass().getName()));
        }
        this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
    }

    AnnotationAwareOrderComparator.sort(this.contextInitializers);
    for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
        initializer.initialize(wac);
    }
}

6.3、ApplicationContextInitializer 介面:容器啟動前用來初始化容器

是個函式式介面,在容器啟動之前用來對容器進行定制,作為一個擴展點預留給開發者用,父容器和 springmvc 容器都用到了,

@FunctionalInterface
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {

 /**
  * 初始化給定的spring容器
  * @param applicationContext the application to configure
  */
 void initialize(C applicationContext);

}

7、階段 6:啟動 springmvc 容器:DispatcherServlet#init()

到目前為止父容器已經啟動完畢了,此時 DispatcherServlet 會被初始化,會進入到他的 init()方法中,

7.1、DispatcherServlet 類圖

991cdc1240f1b20ed3754b09ec5d4982.png

7.2、HttpServletBean#init()

這個方法會呼叫initServletBean()這個方法,其他的先不看

ae38e03b19ca2a1a1fbb2ebca5c2cc83.png

7.3、FrameworkServlet#initServletBean

提取了關鍵的代碼,就 2 行

@Override
protected final void initServletBean() throws ServletException {
    //初始化springmvc容器,就是啟動springmvc容器
    this.webApplicationContext = initWebApplicationContext();
    //這個方法內部是空的,預留給子類去實作的,目前沒啥用
    initFrameworkServlet();
}

下面咱們進到initWebApplicationContext方法中去,

7.4、FrameworkServlet#initWebApplicationContext

關鍵代碼如下,干了 3 件事情:

  1. 從 servlet 上線文物件中找到父容器

  2. 為 springmvc 容器指定父容器

  3. 呼叫 configureAndRefreshWebApplicationContext 方法配置 springmvc 容器以及啟動容器,這個是關鍵咯

protected WebApplicationContext initWebApplicationContext() {
    //①:從servlet上線文中獲取父容器
    WebApplicationContext rootContext =
        WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;
 //②:this.webApplicationContext就是springmvc容器,此時這個對物件不為null,所以滿足條件
    if (this.webApplicationContext != null) {
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            //springmvc容器未啟動
            if (!cwac.isActive()) {
                //springmvc容器未設定父容器,則給其設定父容器,此時rootContext可能為空,這個沒什么關系
                if (cwac.getParent() == null) {
                    cwac.setParent(rootContext);
                }
                //③:配置springmvc容器以及啟動springmvc容器
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    //這里省略了一部分代碼,如果springmvc采用組態檔的方式會走這部分代碼
    ......
 //回傳容器
    return wac;
}

7.5、FrameworkServlet#configureAndRefreshWebApplicationContext

為了讓大家看清楚,如下代碼,這里只提取了關鍵代碼,主要干了 3 件事情

  1. 代碼 ①:向 springmvc 容器中添加了一個 ContextRefreshListener 監聽器,這個監聽器非常非常重要,springmvc 容器啟動完畢之后會被呼叫,會出現在階段 8 中

  2. 代碼 ②:給開發者預留的一個擴展點,通過 ApplicationContextInitializer#initialize 方法對容器進行定制

  3. 代碼 ③:啟動容器

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
 //①:向springmvc容器中添加了一個監聽器物件,這個監聽器特別重要,稍后在
    wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));
 //②:擴展點:回圈遍歷ApplicationContextInitializer串列,呼叫其initialize方法對容器進行定制
    applyInitializers(wac);
    //③:重繪容器,相當于啟動容器
    wac.refresh();
}

8、階段 7:springmvc 容器啟動程序中處理@WebMVC

8.1、SpringMVC 配置類被處理

此時 springmvc 容器啟動了,此時注意下MvcConfig這個類,由于其上有@Conguration 注解,所以會被當做一個配置類被處理,這個類有 2 個非常重要的特征,

  1. 標注了@EnableWebMvc 注解

  2. 實作了 WebMvcConfigurer 介面

d9d007d7cb888ef1d4edc56b7a05c930.png

下面來說說這 2 個特征的作用,

8.2、@EnableWebMvc:配置 springmvc 所需組件

看一下這個注解的原始碼,如下,重點在于它上面的@Import(DelegatingWebMvcConfiguration.class)注解,這個注解的功能不知道的,可以回頭去看我的 spring 系列,從頭看一遍,

e6caffc1d224aef39a768f4624a88167.png

8.3、進入 DelegatingWebMvcConfiguration 類

代碼如下,先注意下面 3 個特征

  1. 代碼編號 ①:標注有@Configuration 注解,說明是一個配置類

  2. 代碼編號 ②:繼承了 WebMvcConfigurationSupport 類,這個類中有很多@Bean 標注的方法,用來定義了 springmvc 需要的所有組件

  3. 代碼編號 ③:注入了WebMvcConfigurer串列,注意下,我們的 WebConfig 類就實作了 WebMvcConfigurer 這個介面,內部提供了很多方法可以用來對 springmvc 的組件進行自定義配置

b703184841f78d5e3047edd783e155c5.png

先來看看 WebMvcConfigurationSupport 這個類,

8.4、WebMvcConfigurationSupport:配置 springmvc 所需所有組件

這個類中會定義 springmvc 需要的所有組件,比如:RequestMapping、HandlerAdapter、HandlerInterceptor、HttpMessageConverter、HandlerMethodArgumentResolver、HandlerMethodReturnValueHandler 等等,所以如果你感覺@WebMVC 注解滿足不了你的需求時,你可以直接繼承這個類進行擴展,

這個類的原始碼我就不貼了,截幾個圖給大家看看

d326312623c5736feea27dea3e7debfd.png

8.5、WebMvcConfigurer 介面

這個介面就是我們用來對 springmvc 容器中的組件進行定制的,WebMvcConfigurationSupport 中創建 springmvc 組件的時候,會自動呼叫 WebMvcConfigurer 中對應的一些方法,來對組件進行定制,比如可以在 WebMvcConfigurer 中添加攔截器、配置默認 servlet 處理器、靜態資源處理器等等,這個介面的原始碼如下

public interface WebMvcConfigurer {

 /**
  * 配置PathMatchConfigurer
  */
 default void configurePathMatch(PathMatchConfigurer configurer) {
 }

 /**
  * 配置ContentNegotiationConfigurer
  */
 default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
 }

 /**
  * 異步處理配置
  */
 default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
 }

 /**
  * 配置默認servlet處理器
  */
 default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
 }

 /**
  * 配置Formatter
  */
 default void addFormatters(FormatterRegistry registry) {
 }

 /**
  * 添加攔截器
  */
 default void addInterceptors(InterceptorRegistry registry) {
 }

 /**
  * 靜態資源配置
  */
 default void addResourceHandlers(ResourceHandlerRegistry registry) {
 }

 /**
  * 跨越的配置
  */
 default void addCorsMappings(CorsRegistry registry) {
 }

 /**
  * 配置ViewController
  */
 default void addViewControllers(ViewControllerRegistry registry) {
 }

 /**
  * 注冊視圖決議器(ViewResolverRegistry)
  */
 default void configureViewResolvers(ViewResolverRegistry registry) {
 }

 /**
  * 注冊處理器方法引數決議器(HandlerMethodArgumentResolver)
  */
 default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
 }

 /**
  * 注冊處理器方法回傳值處理器(HandlerMethodReturnValueHandler)
  */
 default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
 }

 /**
  * 注冊http報文轉換器(HttpMessageConverter)
  */
 default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
 }

 /**
  * 擴展報文轉換器
  */
 default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
 }

 /**
  * 配置例外決議器(HandlerExceptionResolver)
  */
 default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
 }

 /**
  * 擴展例外決議器(HandlerExceptionResolver)
  */
 default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
 }

 /**
  * 獲得驗證器
  */
 @Nullable
 default Validator getValidator() {
  return null;
 }

 /**
  * 獲得MessageCodesResolver
  */
 @Nullable
 default MessageCodesResolver getMessageCodesResolver() {
  return null;
 }

}

9、階段 8:組裝 DispatcherServlet 中各種 SpringMVC 需要的組件

9.1、觸發 ContextRefreshListener 監聽器

大家回頭看一下 8.5 中,有這樣一段代碼,注冊了一個監聽器ContextRefreshListener

5ffafa65fb90ecb4f594a2f55b799f6d.png

再來看看這個監聽器的原始碼,如下圖,包含 2 點資訊

  1. 會監聽 ContextRefreshedEvent 事件

  2. 監聽到事件之后將執行FrameworkServlet.this.onApplicationEvent(event);,稍后會具體說這個代碼

d79bc133b784abaec505faaf49f29b06.png

如下代碼,springmvc 容器啟動完畢之后,會發布一個ContextRefreshedEvent事件,會觸發上面這個監聽器的執行

f101e2c4ce4e891b8628037c66071ca1.png

9.2、進入 FrameworkServlet.this.onApplicationEvent(event);

public void onApplicationEvent(ContextRefreshedEvent event) {
    //標記已收到重繪事件
    this.refreshEventReceived = true;
    synchronized (this.onRefreshMonitor) {
        onRefresh(event.getApplicationContext());
    }
}

上面的onRefresh(event.getApplicationContext());會進到DispatcherServlet#onRefresh方法中,

9.3、進入 DispatcherServlet#onRefresh

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

里面會呼叫initStrategies方法,

9.4、DispatcherServlet#initStrategies:初始化 DispatcherServlet 中的組件

代碼如下,這里面會初始化 DispatcherServlet 中的各種組件,這里的所有方法初始化的程序基本上差不多,就是先從 springmvc 容器中找這個組件,如果找不到一般會有一個兜底的方案

protected void initStrategies(ApplicationContext context) {
    //初始化MultipartResolver
    initMultipartResolver(context);
    //初始化LocaleResolver
    initLocaleResolver(context);
    //初始化ThemeResolver
    initThemeResolver(context);
    //初始化HandlerMappings
    initHandlerMappings(context);
    //初始化HandlerAdapters
    initHandlerAdapters(context);
    //初始化HandlerExceptionResolvers
    initHandlerExceptionResolvers(context);
    //初始化RequestToViewNameTranslator
    initRequestToViewNameTranslator(context);
    //初始化視圖決議器ViewResolvers
    initViewResolvers(context);
    //初始化FlashMapManager
    initFlashMapManager(context);
}

下面我們以initHandlerMappings(context);為例來看一下是如何初始化這些組件的,

9.5、initHandlerMappings(context);

原始碼如下,就是先從容器中找,找不到走兜底的方案,

private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;
    //是否需要查找所有的HandlerMapping,默認為true
    if (this.detectAllHandlerMappings) {
        //從容器中查找所有的HandlerMapping
        Map<String, HandlerMapping> matchingBeans =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        //對HandlerMapping串列進行排序
        if (!matchingBeans.isEmpty()) {
            this.handlerMappings = new ArrayList<>(matchingBeans.values());
            AnnotationAwareOrderComparator.sort(this.handlerMappings);
        }
    }
    else {
        try {
            //查找名稱為handlerMapping的HandlerMapping
            HandlerMapping hm = context.getBean("handlerMapping", HandlerMapping.class);
            this.handlerMappings = Collections.singletonList(hm);
        }
        catch (NoSuchBeanDefinitionException ex) {
        }
    }

    // 如果沒有找到HandlerMapping,則走兜底的方案
    if (this.handlerMappings == null) {
        this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
    }
}

下面我們來看一下兜底的代碼如何走的,進入 getDefaultStrategies 方法

9.6、DispatcherServlet#getDefaultStrategies:兜底的方案查找組件

這個方法的原始碼就不貼出來了,這里只說一下兜底的處理程序,springmvc 有個組態檔:spring-webmvc-5.3.6.jar!\org\springframework\web\servlet\DispatcherServlet.properties,properties 格式的檔案,key 為組件的完整類名,value 為多個實作類的串列,在這個組態檔中指定了每種型別的組件兜底的情況下對應的實作類,比如沒有找到 RequestMapping 的情況下,如下圖紅框的部分,有 3 個兜底的實作類,然后 springmvc 會實體化這 3 個類作為 RequestMapping,

0724ebbfa0121342c7693db5b3bde02d.png

10、階段 9:銷毀容器

10.1、銷毀 springmvc 容器:DispatcherServlet#destroy

DispatcherServlet 銷毀的時候會關閉 springmvc 容器

public void destroy() {
    if (this.webApplicationContext instanceof ConfigurableApplicationContext && !this.webApplicationContextInjected) {
        ((ConfigurableApplicationContext) this.webApplicationContext).close();
    }
}

10.2、銷毀父容器:ContextLoaderListener#contextDestroyed

父容器是在監聽器中啟動的,所以銷毀的也是監聽器負責的

public void contextDestroyed(ServletContextEvent event) {
    closeWebApplicationContext(event.getServletContext());
    ContextCleanupListener.cleanupAttributes(event.getServletContext());
}

springmvc 容器的生命周期到此就結束了,想掌握好這個程序,建議大家 debug 走幾遍,就熟悉了,下面帶大家 debug 一下代碼,

11、帶大家 debug 代碼

11.1、拉取原始碼

https://gitee.com/javacode2018/springmvc-series

11.2、將下面這個模塊發布到 tomcat

cc77bb136722a4c1b1081fc2f074481d.png

11.2、按照下面配置設定斷點,啟動,除錯代碼

依次在下面這些方法中設定斷點,然后啟動 tomcat,一步步除錯,我相信你們肯定可以吃透的,

1、org.springframework.web.SpringServletContainerInitializer#onStartup:入口
2、org.springframework.web.servlet.support.AbstractDispatcherServletInitializer#onStartup
3、org.springframework.web.context.AbstractContextLoaderInitializer#onStartup
4、org.springframework.web.context.AbstractContextLoaderInitializer#registerContextLoaderListener:創建父容器
5、org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer#createRootApplicationContext
6、org.springframework.web.servlet.support.AbstractDispatcherServletInitializer#registerDispatcherServlet
7、org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer#createServletApplicationContext:創建springmvc容器 & 注冊DispatcherServlet
8、org.springframework.web.context.ContextLoaderListener#contextInitialized
9、org.springframework.web.context.ContextLoader#initWebApplicationContext
10、org.springframework.web.context.ContextLoader#configureAndRefreshWebApplicationContext:啟動父容器
11、org.springframework.web.servlet.HttpServletBean#init
12、org.springframework.web.servlet.FrameworkServlet#initServletBean
13、org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext:初始化springmvc容器&啟動springmvc容器
14、org.springframework.web.servlet.FrameworkServlet#configureAndRefreshWebApplicationContext:啟動springmvc容器
15、org.springframework.web.servlet.FrameworkServlet.ContextRefreshListener#onApplicationEvent
16、org.springframework.web.servlet.DispatcherServlet#onRefresh
17、org.springframework.web.servlet.DispatcherServlet#initStrategies:組裝Dispathcer中各種springmvc組件

有問題歡迎加我微信:itsoku,交流,

12、SpringMVC 系列目錄

  1. SpringMVC 系列第 1 篇:helloword

  2. SpringMVC 系列第 2 篇:@Controller、@RequestMapping

  3. SpringMVC 系列第 3 篇:例外高效的一款介面測驗利器

  4. SpringMVC 系列第 4 篇:controller 常見的接收引數的方式

  5. SpringMVC 系列第 5 篇:@RequestBody 大解密,說點你不知道的

  6. SpringMVC 系列第 6 篇:上傳檔案的 4 種方式,你都會么?

  7. SpringMVC 系列第 7 篇:SpringMVC 回傳視圖常見的 5 種方式,你會幾種?

  8. SpringMVC 系列第 8 篇:回傳 json & 通用回傳值設計

  9. SpringMVC 系列第 9 篇:SpringMVC 回傳 null 是什么意思?

  10. SpringMVC 系列第 10 篇:異步處理

  11. SpringMVC 系列第 11 篇:集成靜態資源

  12. SpringMVC 系列第 12 篇:攔截器

  13. SpringMVC 系列第 13 篇:統一例外處理

  14. SpringMVC 系列第 14 篇:實戰篇:通用回傳值 & 例外處理設計

  15. SpringMVC 系列第 15 篇:全注解的方式 & 原理決議

  16. SpringMVC 系列第 16 篇:萬字長文,深度決議 SpringMVC 原始碼,讓你醍醐灌頂

13、更多好文章

  1. Spring 高手系列(共 56 篇)

  2. Java 高并發系列(共 34 篇)

  3. MySql 高手系列(共 27 篇)

  4. Maven 高手系列(共 10 篇)

  5. Mybatis 系列(共 12 篇)

  6. 聊聊 db 和快取一致性常見的實作方式

  7. 介面冪等性這么重要,它是什么?怎么實作?

  8. 泛型,有點難度,會讓很多人懵逼,那是因為你沒有看這篇文章!

14、【路人甲 Java】所有系列高清 PDF

領取方式,掃碼發送:yyds

a3214733beb6798efde805ec2bab3987.png

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

標籤:java

上一篇:簡單圖書管理系統練習-Java+Mysql

下一篇:Java 經典小題篇

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