Spring MVC启动原理详解(上)

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

一、零配置Spring MVC实现

在之前,先简单介绍一下Spring MVC是如何整合Spring的,在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 ),
})
public class RootConfig {

}

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

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

@Configuration
@ComponentScan(basePackages = {"com.lizhi"},includeFilters = {
    @ComponentScan.Filter(type = FilterType.ANNOTATION,value = {RestController.class, Controller.class})
},useDefaultFilters =false)
@EnableWebMvc 
public class WebAppConfig implements WebMvcConfigurer{

    /**
    * 配置拦截器
    * @return
    */
    @Bean
    public LizhiInterceptor lizhiInterceptor() {
        return new LizhiInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(lizhiInterceptor()).addPathPatterns("/*");
    }
}

public class LizhiInterceptor implements HandlerInterceptor {

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {
        System.out.println("Interceptor....preHandle");
        return true;
    }
}

1.2 实现初始化器接口

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

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

public class LizhiStarterInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

   // IOC 父容器的启动类
   @Override
   protected Class<?>[] getRootConfigClasses() {
      return new Class[]{RootConfig.class};
   }

   // IOC子容器配置 web容器配置
   @Override
   protected Class<?>[] getServletConfigClasses() {
      return new Class[]{WebAppConfig.class};
   }

   // 我们前端控制器DispatcherServlet的拦截路径
   @Override
   protected String[] getServletMappings() {
      return new String[]{"/"};
   }
}

二、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的使用:

定义一个接口:

public interface Search {
    public List<String> searchDoc(String keyword);   
}

定义两个接口的实现类:

public class FileSearch implements Search{
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.println("文件搜索 "+keyword);
        return null;
    }
}
public class DatabaseSearch implements Search{
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.println("数据搜索 "+keyword);
        return null;
    }
}

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

文件内容如下:

com.lizhi.FileSearch
com.lizhi.DatabaseSearch

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

public class TestCase {
    public static void main(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()方法

public interface ServletContainerInitializer {

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

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

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

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

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

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

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

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

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

if (webAppInitializerClasses != null) {
    initializers = new ArrayList<>(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()方法

public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        //registerContextLoaderListener  ok
        super.onStartup(servletContext);
        // registerDispatcherServlet
        registerDispatcherServlet(servletContext);
    }
    ……
}

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

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

public abstract class AbstractContextLoaderInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        registerContextLoaderListener(servletContext);
    }

    protected void registerContextLoaderListener(ServletContext servletContext) {
        // 创建父容器 ,
        WebApplicationContext rootAppContext = createRootApplicationContext();
        if (rootAppContext != null) {
            ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
            // 设置初始化器
            listener.setContextInitializers(getRootApplicationContextInitializers());
            servletContext.addListener(listener);
        }
    }
    ……
}

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

protected WebApplicationContext createRootApplicationContext() {
    Class<?>[] configClasses = getRootConfigClasses();
    if (!ObjectUtils.isEmpty(configClasses)) {
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(configClasses);
        return context;
    }
    else {
        return null;
    }
}

3.2 创建子容器

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

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

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

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

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

protected void registerDispatcherServlet(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");
    // 创建DispatcherServlet
    FrameworkServlet 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);
        }
    }
}

你可能感兴趣的:(Web开发实践与源码分析,spring,mvc,java)