SpringMVC源码分析八、零XML配置SpringMVC环境原理

735 阅读8分钟

在SpringBoot的项目中, 我们可以看到, web.xml、springmvc.xml、applicationContext.xml这样的配置文件已 经不见了, 取而代之的是各种的注解, 注解开发给我们带来了很多的便利, 利用JavaConfig完成项目的配置会显得更 加的"高端", 本篇文章主要分析的是无XML实现SpringMVC环境搭建的原理, 如果理解了这些原理, 那么SpringBoot的 也就不会太神秘了

Servlet3.0规范的引入

众所周知, Tomcat等web容器是用来放置Servlet的, Tomcat完成对底层网络的处理, 最后才将请求打到Servlet中,
在Servlet3.0规范出来后, 引入了许多的新特性, 其中一个特性是这样的, 我们可以在/META-INF/services文件夹
下创建一个名为javax.servlet.ServletContainerInitializer的文件, 这个文件中可以放置任何的
ServletContainerInitializer接口的实现类, ServletContainerInitializer接口是一个函数式接口, 有一个
onStartup方法, Servlet3.0规范中指定, 在web容器中启动的过程中, 会扫描整个项目中/META-INF/services文件
夹下名为javax.servlet.ServletContainerInitializer的文件, 调用这个文件中所有
ServletContainerInitializer实现类的onStartup方法, 下面我们来演示一下:

目录结构:
    src
        main
            java
            resources
                META-INF
                    services
                        javax.servlet.ServletContainerInitializer

文件javax.servlet.ServletContainerInitializer内容:
    com.test.MyWebInit

MyWebInit类代码:
    public class MyWebInit implements ServletContainerInitializer {
        @Override
        public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
            System.out.println( "startUp......" );
        }
    }

分析:
    启动tomcat, 会发现控制台输出了: startUp......

我们再来看看这个onStartup方法, 有一个Set<Class<?>>的变量, 以及一个ServletContext, 后者大家应该很熟悉
了, Servlet上下文, 我们来介绍一下前者, 有这么一个注解@HandlesTypes, Servlet3.0规范中指定, 当该注解标
注在ServletContainerInitializer上的时候, web容器启动的过程中就会将整个项目中该注解中指定的接口的所有
实现类的Class对象放入一个set中并传入onStrartup方法中, 下面我们来修改下上述的例子:

package com.test.startup;
public interface StartUpInterface {}
public class StartUpInterfaceImpl implements StartUpInterface {}

@HandlesTypes( StartUpInterface.class )
public class MyWebInit implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
        System.out.println( c );
    }
}

分析:
    启动tomcat, 发现控制台输出了: [class com.test.startup.StartUpInterfaceImpl]

由上面的特性, 我们就可以了解到, web容器提供了扩展点, 在容器启动的过程中, 允许我们做一些事情, 利用这个扩
展点, 我们就能实现无xml对SpringMVC的环境进行配置了

实现无xml完成SpringMVC环境搭建

结合之前的DispatcherServlet初始化流程源码分析, 我们需要知道一个点, 之前我们配置SpringMVC的环境的时候,
是在web.xml中配置了DispatcherServlet的映射关系, 并且利用监听器注册了一个Spring容器, 在xml中配置
DispatcherServlet的映射关系, 其实就是将DispatcherServlet这个Servlet加入到了Servlet上下文中而已, 于是
我们联想上面的例子, 貌似我们可以手动的创建DispatcherServlet对象, 然后加到ServletContext中???

其次, 没有了springmvc.xml文件, 那么xml文件中配置的annotation-driven以及component-scan这些功能就没法
启用, 没法开启注解配置以及配置扫描的包了

再一次回顾前面的DispatcherServlet初始化流程源码分析的内容, DispatcherServlet继承于FrameworkServlet,
FrameworkServlet重写了GenericServlet的空参init方法, 在这个重写的方法中完成了容器的初始化, 于是乎, 我
们再来回顾一下, 容器的获取方式, 通常情况下, 是在这个初始化的过程中会自动创建web容器, 但是这个创建的web
容器xmlApplicationContext, 并且不方便我们去控制其创建的过程, 但是在原来的源码分析中, 笔者也提到了, 我
们可以手动的往DispatcherServlet中set一个容器, 那就好办了, 我们往里面set一个注解配置的容器, 而不是xml
配置的容器, 于是, 无xml配置的原理就出来了, 我们先看一下代码:
public class MyWebInit implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> c, ServletContext ctx) {
        AnnotationConfigWebApplicationContext context 
                                                = new AnnotationConfigWebApplicationContext();
        context.register( SpringConfig.class );

        DispatcherServlet dispatcherServlet = new DispatcherServlet();
        dispatcherServlet.setApplicationContext( context );
        ServletRegistration.Dynamic registration 
                                = ctx.addServlet( "dispatcherServlet", dispatcherServlet );
        registration.addMapping( "/" );
        registration.setLoadOnStartup( 1 );
        // registration.setInitParameter();
    }
}

分析:
    <1> 手动创建一个注解配置的Web容器, 如果对Spring源码有所了解, 那么register方法应该不会陌生, 就是
        注册一个配置类, 这个配置类的内容很简单:
            @Configuration
            @ComponentScan( "com.test" )
            @EnableWebMvc
            public class SpringConfig {}

    <2> 我们在独立的Spring环境下, 创建Spring环境是这样的:
            AnnotationConfigApplicationContext context 
                                = new AnnotationConfigApplicationContext(SpringConfig.class);
        可以在构造器中放入一个配置类, 构造器被执行的时候, 会调用register方法将SpringConfig配置类注册
        到容器中, 同时调用refresh方法开始完成容器的初始化, 在web环境下的容器, 没有提供一个构造器可以
        传递配置类的, 于是我们才手动调用register方法来注册一个配置类, 注意了, 我们没有调用refresh方法
        之后我们再来分析下原因
    <3> 手动创建一个DispatcherServlet, 同时将创建好的web容器设置到DispatcherServlet中, 之后
        DispatcherServlet在初始化的时候就不会自己创建了, 而是采用我们传入的这个
    <4> 将DispatcherServlet添加到Servlet上下文中, 就等价于在web.xml中配置了一个DispatcherServlet,
        然后利用返回值registration(ServletRegistration.Dynamic类型的)添加这个Servlet需要拦截的请求,
        最后两行就等价于在web.xml中的配置:
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>classpath:spring-mvc.xml</param-value>
            </init-param>
            <load-on-startup>1</load-on-startup>

到此为止, 我们利用Java代码完成了在web.xml中配置的DispatcherServlet的功能, 与此同时, 整合了注解配置的
web容器与DispatcherServlet, 再来说下为什么我们不主动调用refresh方法刷新容器, 回顾一下原来的
DispatcherServlet初始化流程源码分析, 原来的web容器是在FrameworkServlet的空参init方法被触发创建的, 由
于我们手动放入了web容器, 所以就不会手动创建, 大家可以回顾下之前的文章内容, 笔者着重的讲解了在容器创建后
或者从DisatcherServlet中取到了设置进去的容器后, 会调用一个configureAndRefreshWebApplicationContext
方法, 在这个方法中的最后有这么一行代码:
    wac.refresh();

于是乎, 我们知道了, 在FrameworkServlet中就会帮我们主动调用refresh方法, 如果在此时我们先调用的话, 那么
可能会有些问题, 因为在configureAndRefreshWebApplicationContext方法中, 调用refresh方法之前, 会对容器
进行一定的设置, 比如说环境、监听器(用于触发SpringMVC的九大策略的初始化)等, 所以我们不能在这个时候去调用

小小的总结

实现无xml完成的SpringMVC环境搭建的原理其实很简单, 就是利用了Servlet3.0规范提供的
ServletContainerInitializer接口来完成的, 因为这个接口会传入一个ServletContext, 所以我们可以直接对
ServletContext进行操作, 放入DispatcherServlet, 同时利用注解配置的上下文完成Spring环境的引入, 由此我
们已经可以直接将各种的xml配置文件删除了, 如果是Spring或者SpringMVC的配置, 可以利用上面的SpringConfig
这个类来完成, 而如果是web.xml的配置, 可以利用MyWebInit类中onStartUp方法来完成

内嵌tomcat容器

在我们之前的分析中, 已经完成了无xml配置SpringMVC的环境了, 这里做一个扩展, 我们知道SpringBoot是内嵌
tomcat的容器的, 我们也可以完成这样的壮举!!

引入tomcat的jar包:
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-core</artifactId>
      <version>8.5.31</version>
    </dependency>

    <!-- 引入JSP支持 -->
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-jasper</artifactId>
      <version>8.5.31</version>
    </dependency>

创建启动类:
    public class MyApplicationStarter {
        public static void main(String[] args) throws Exception{
            Tomcat tomcat = new Tomcat();
            tomcat.setPort(80);
            tomcat.addWebapp("/","D:\Users\Desktop\testProject\springmvc\src\main\webapp");
            context.addLifecycleListener(
                (LifecycleListener) Class.forName(tomcat.getHost().getConfigClass())
                .newInstance());

            tomcat.start();
            tomcat.getServer().await();
        }
    }

再结合之前的代码, 我们就实现了内嵌的tomcat应用, 从而实现了类似于SpringBoot的效果!!!

扩展: WebApplicationInitializer

最开始分析的Servlet3.0规范的时候, 我们发现利用3.0规范, 我们能够在tomcat容器启动的过程中进行插手, 只需
要在resources目录下建立/META-INF/services/javax.servlet.ServletContainerInitializer的文件就好了,
tomcat容器启动的时候会反射加载这个文件中的所有类, 并引入了@HandlesTypes注解, 在该注解标注的接口, 其所
有的子类会被传入onStartup方法中, 然而, 上面是我们自己实现的, 其实在SpringMVC中早就已经有了实现

SpringServletContainerInitializer是Spring对Servlet3.0规范的实现, 同时在spring-web这个包中, 就有一个
/META-INF/services/javax.servlet.ServletContainerInitializer文件, 在这个文件中就定义了这个
SpringServletContainerInitializer类, 这个类被@HandlesTypes注解标注了, 标注的接口是
WebApplicationInitializer接口, SpringServletContainerInitializer类的onStartUp方法完成的功能就是
从set中取出一个个WebApplicationInitializer接口的实现类, 创建对象, 并调用这些这些实现类的方法, 所以我
们只需要在程序中创建一个WebApplicationInitializer实现类就可以完成之前我们的所有功能了....