Spring MVC启动原理详解

2022年10月21日10:16:32

《从Servlet到Spring MVC》中,介绍了基于xml配置使用的方式,但我们我现在用的更多的基于注解零配置的方式,尤其是在使用SpringBoot的时候,只需要引入web的start包即可,这边文章前面会简单介绍一下Spring MVC零配置的的使用,然后详细分析Spring MVC启动的原理,可以更加深入理解为什么只需要简单的配置,就可以提供强大的功能

一、零配置Spring MVC实现

在之前,先简单介绍一下Spring MVC是如何整合Spring的,在Spring MVC的官网,提供了一张父子容器的图:

Spring MVC启动原理详解

从上面这张图可以清晰的看到,在Spring MVC整合Spring中,其实是由两个容器组成的,其中下面的根容器就是Spring自身的容器,而上面的容器,是Spring MVC特有的容器,那为什么要这么设计呢?只是用一个容器不行吗

其实这种设计方法最大的考量兼容第三方MVC框架,比如以前常用的Struts框架,MVC容器用于存放Controller、视图解析器、处理器映射器这样的Bean,而提供服务的Bean由下层的Spring容器来管理,实现了解耦。但在SpringBoot中就不再使用父子容器,SpringBoot作为一个集成的解决方法,就是使用SpringMVC来作为Web框架,不需要再兼容其他第三方框架,那么直接使用是一个容器就可以了。

既然由两个容器,那么两个容器再启动时,就会用到不同的配置来加载Bean,所有使用零配置实现SpringMVC时首先就要定义两个配置文件

1.1 定义配置文件

定义根容器的配置类,不扫描@Controller注解的类和子容器的配置类

@Configuration@ComponentScan(basePackages="com.lizhi",excludeFilters={@ComponentScan.Filter(type=FilterType.ANNOTATION,value={Controller.class}),@ComponentScan.Filter(type= ASSIGNABLE_TYPE,value=WebAppConfig.class),})publicclassRootConfig{}

定义子容器的配置类,只扫描有@RestController和@Controller注解的类,同时需要添加@EnableWebMvc注解

子容器的配置类,可以实现WebMvcConfigurer接口,该接口中提供了添加拦截器、资源处理器、参数解析器等的扩展,下面我们只配置添加一个拦截器

@Configuration@ComponentScan(basePackages={"com.lizhi"},includeFilters={@ComponentScan.Filter(type=FilterType.ANNOTATION,value={RestController.class,Controller.class})},useDefaultFilters=false)@EnableWebMvcpublicclassWebAppConfigimplementsWebMvcConfigurer{/**
    * 配置拦截器
    * @return
    */@BeanpublicLizhiInterceptorlizhiInterceptor(){returnnewLizhiInterceptor();}@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(lizhiInterceptor()).addPathPatterns("/*");}}publicclassLizhiInterceptorimplementsHandlerInterceptor{publicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{System.out.println("Interceptor....preHandle");returntrue;}}

1.2 实现初始化器接口

上面定义完了配置类,但是并没有配置这两个配置类该给哪个容器用,所以,接下来就是要去实现AbstractAnnotationConfigDispatcherServletInitializer抽象类,定义每个容器启动时加载的类

在创建容器时,可以通过下面的方法,来获取配置类进行解析

publicclassLizhiStarterInitializerextendsAbstractAnnotationConfigDispatcherServletInitializer{// IOC 父容器的启动类@OverrideprotectedClass<?>[]getRootConfigClasses(){returnnewClass[]{RootConfig.class};}// IOC子容器配置 web容器配置@OverrideprotectedClass<?>[]getServletConfigClasses(){returnnewClass[]{WebAppConfig.class};}// 我们前端控制器DispatcherServlet的拦截路径@OverrideprotectedString[]getServletMappings(){returnnewString[]{"/"};}}

二、Spring MVC容器启动灵魂 —SPI

在使用Spring MVC整合Spring的时候,我们没有像执行main()方法一样去手动创建Spring的容器,那么Spring的容器又是在什么时候创建的,这就要说到Java强大的扩展机制 ——SPI(Service Provider Interface)),翻译过来就是服务提供商接口,那SPI是如何用的呢?

2.1 Java扩展机制SPI

按照Java的SPI规范,我们只要在META-INF/services目录创建一个文件,文件名就是服务提供商提供的接口名,而文件内容就是实现这个接口的类的全限定名,然后Java在运行时候,可以通过ServiceLoader来加载这些类,这种方法提供了良好的扩展,下面代码示例SPI的使用:

定义一个接口:

publicinterfaceSearch{publicList<String>searchDoc(String keyword);}

定义两个接口的实现类:

publicclassFileSearchimplementsSearch{@OverridepublicList<String>searchDoc(String keyword){System.out.println("文件搜索 "+keyword);returnnull;}}
publicclassDatabaseSearchimplementsSearch{@OverridepublicList<String>searchDoc(String keyword){System.out.println("数据搜索 "+keyword);returnnull;}}

然后在META-INF/services目录下创建一个接口权限名的文件:

Spring MVC启动原理详解

文件内容如下:

com.lizhi.FileSearch
com.lizhi.DatabaseSearch

测试方法:ServiceLoader的load()方法会拿到接口对应的文件里面的实现类,然后在iterator的next()方法中,会去实例化这些实现类,这就是Java SPI的使用

publicclassTestCase{publicstaticvoidmain(String[] args){ServiceLoader<Search> s=ServiceLoader.load(Search.class);Iterator<Search> iterator= s.iterator();while(iterator.hasNext()){Search search=  iterator.next();
            search.searchDoc("hello world");}}}

注:数据库连接的依赖包中,比如mysql-connector-java-5.1.44.jar,也有利用这种SPI机制来加载驱动

2.2 Servlet规范中的SPI

在Servlet3.1的规范中,明确指出Web容器需要支持对javax.servlet.ServletContainerInitailizer接口的扩展,Web容器(Tomcat)在启动的的时候,会根据META-INF/services目录中的文件内容,去加载所有ServletContainerInitailizer的实现类,然后调用它们的onStartup()方法

publicinterfaceServletContainerInitializer{publicvoidonStartup(Set<Class<?>> c,ServletContext ctx)throwsServletException;}

而Spring MVC中就定义了一个实现该接口的类SpringServletContainerInitializer

该类上面的注解@HandlesTypes指定了,在调用onStartup()方法时,第一个参数需要传入什么类型的实现类

SpringMVC指定了需要传入WebApplicationInitializer的实现类或接口

@HandlesTypes(WebApplicationInitializer.class)publicclassSpringServletContainerInitializerimplementsServletContainerInitializer{publicvoidonStartup(@NullableSet<Class<?>> webAppInitializerClasses,ServletContext servletContext)throwsServletException{
		……}}

SpringMVC在onStartup()方法的实现中,然后拿到了所有实现了WebApplicationInitializer接口的类和接口,但是,会把接口和抽象类过滤掉,SpringMVC自身提供了三个实现类,分别是:AbstractContextLoaderInitializer、AbstractDispatcherServletInitializer和AbstractAnnotationConfigDispatcherServletInitializer,不过这三个类都是抽象类,在启动的时候是没法使用的,这就是为什么我们在零配置使用SpringMVC的时候需要,需要添加一个类,来继承AbstractAnnotationConfigDispatcherServletInitializer抽象类

过滤完之后,就会去遍历所有过滤得到的WebApplicationInitializer类的是实现类,然后调用它们的onStartup()方法

List<WebApplicationInitializer> initializers=Collections.emptyList();if(webAppInitializerClasses!=null){
    initializers=newArrayList<>(webAppInitializerClasses.size());for(Class<?> waiClass: webAppInitializerClasses){// 接口和抽象类servlet容器也会给我们,但是我们不要// 排除接口和容器if(!waiClass.isInterface()&&!Modifier.isAbstract(waiClass.getModifiers())&&WebApplicationInitializer.class.isAssignableFrom(waiClass)){// 实例化,然后添加到集合中
            initializers.add((WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass).newInstance());}}}AnnotationAwareOrderComparator.sort(initializers);// 调用initializer.onStartup  进行扩展for(WebApplicationInitializer initializer: initializers){
    initializer.onStartup(servletContext);}

三、创建父子容器

3.1 创建父容器

在前面我们定义的AbstractAnnotationConfigDispatcherServletInitializer的实现类LizhiStarterInitializer中,并没有实现onStartup()方法,所以会去调用父类的onStartup()方法

AbstractAnnotationConfigDispatcherServletInitializer中onStartup()方法的定义如下:

因为其继承自AbstractContextLoaderInitializer抽象类,所以又会去调用父类的onStartup()方法

publicabstractclassAbstractDispatcherServletInitializerextendsAbstractContextLoaderInitializer{@OverridepublicvoidonStartup(ServletContext servletContext)throwsServletException{//registerContextLoaderListener  oksuper.onStartup(servletContext);// registerDispatcherServletregisterDispatcherServlet(servletContext);}
    ……}

在AbstractContextLoaderInitializer类的onStartup()方法中,就会去创建Spring的父容器,然后再创建一个ContextLoaderListener类型的监听器,这个监听器实现了ServletContextListener接口,可以监听Servlet上下文信息,这个我们在使用xml开发时,是要固定配置的,后面会详细讲到这个监听器的用处

最后把这个监听器添加到Servlet容器中

publicabstractclassAbstractContextLoaderInitializerimplementsWebApplicationInitializer{@OverridepublicvoidonStartup(ServletContext servletContext)throwsServletException{registerContextLoaderListener(servletContext);}protectedvoidregisterContextLoaderListener(ServletContext servletContext){// 创建父容器 ,WebApplicationContext rootAppContext=createRootApplicationContext();if(rootAppContext!=null){ContextLoaderListener listener=newContextLoaderListener(rootAppContext);// 设置初始化器
            listener.setContextInitializers(getRootApplicationContextInitializers());
            servletContext.addListener(listener);}}
    ……}

创建父容器的方法实现在AbstractAnnotationConfigDispatcherServletInitializer类中,看到这里就很熟悉了,根据getRootConfigClasses()方法来获取父容器的配置类,然后注册该配置类,到这里,Spring容器还并没有启动,只是创建完成了而已

protectedWebApplicationContextcreateRootApplicationContext(){Class<?>[] configClasses=getRootConfigClasses();if(!ObjectUtils.isEmpty(configClasses)){AnnotationConfigWebApplicationContext context=newAnnotationConfigWebApplicationContext();
        context.register(configClasses);return context;}else{returnnull;}}

3.2 创建子容器

在AbstractDispatcherServletInitializer中,调用父类的onStartup()创建完父容器之后,接着就会去调用registerDispatcherServlet()方法来创建子容器,以及创建DispatcherServlet实例

publicvoidonStartup(ServletContext servletContext)throwsServletException{//registerContextLoaderListener  oksuper.onStartup(servletContext);// registerDispatcherServletregisterDispatcherServlet(servletContext);}

registerDispatcherServlet()方法中,会调用createServletApplicationContext()方法创建子容器,逻辑与创建父容器一样

然后调用createDispatcherServlet()方法来创建DispatcherServlet,会把子容器设置到DispatcherServlet实例中,这个需要注意,在DispatcherServlet初始化的时候,会使用到子容器的

然后就是把DispatcherServlet添加到Servlet上下文中,返回一个ServletRegistration.Dynamic的对象,然后设置一些DispatcherServlet的基础信息,这些信息都是在使用xml时需要手动配置的

protectedvoidregisterDispatcherServlet(ServletContext servletContext){String servletName=getServletName();Assert.hasLength(servletName,"getServletName() must not return null or empty");// 创建子容器WebApplicationContext servletAppContext=createServletApplicationContext();Assert.notNull(servletAppContext,"createServletApplicationContext() must not return null");// 创建DispatcherServletFrameworkServlet dispatcherServlet=createDispatcherServlet(servletAppContext);ServletRegistration.Dynamic registration= servletContext.addServlet(servletName, dispatcherServlet);// 启动时加载
    registration.setLoadOnStartup(1);// 映射
    registration.addMapping(getServletMappings());// 是否异步支持
    registration.setAsyncSupported(isAsyncSupported());// 设置DispatcherServlet的过滤器Filter[] filters=getServletFilters();if(!ObjectUtils.isEmpty(filters)){for(Filter filter: filters){registerServletFilter(servletContext, filter);}}}
  • 作者:sermonlizhi
  • 原文链接:https://blog.csdn.net/sermonlizhi/article/details/122003306
    更新时间:2022年10月21日10:16:32 ,共 8709 字。