对于工龄稍大一点的程序员同学,基本都用过Spring MVC+Tomcat这一套,但是目前主流的是Spring Boot+Tomcat这一套了,这两套机制的底层原理是有相似地方的,所以本文会对这两套机制统一进行分析。
启动顺序
Spring MVC和Spring Boot底层都是一个Spring容器,也就是一个ApplicationContext对象,所以Spring MVC和Spring Boot的启动说简单点,就是去创建一个Spring容器。
对于Spring MVC+Tomcat而言,是先启动Tomcat,再创建一个Spring容器。
对于Spring Boot+Tomcat而言,是先创建Spring容器,再启动Tomcat。
这是这两套机制最大的差异点,也是导致它们的底层原理有所区别的地方。
Spring MVC+Tomcat
对于一个由Spring MVC和Tomcat开发出来的Web应用,是需要接收网络请求的,在Java层面,接收网络请求的方式有很多种,但用得最多的就是Servlet容器,在众多Servlet容器中,老大哥就是Tomcat。
当我们用Spring MVC开发一个Web应用时,我们打成war包,部署到Tomcat中,启动Tomcat,用户即可访问Web应用。在这个过程中,Spring MVC框架所提供的DispatcherServlet起到了承上启下的作用,正是有了这个Servlet,我们用Spring MVC所开发的项目部署到Tomcat后才能够正常的被请求。
所以我们需要在web.xml中加上这段配置:
<servlet><servlet-name>app</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><param-value>/WEB-INF/spring-mvc.xml</param-value></init-param><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>app</servlet-name><url-pattern>/app/*</url-pattern></servlet-mapping>
注意web.xml并不是Spring MVC所定义的概念,是Servlet规范中所定义的概念,是Tomcat所需要的。
在N年前,我们是通过这种方式把DispatcherServlet添加到Tomcat中去的,这样,我们的Web项目就能接收请求了,接收到请求后,就需要从Spring容器中找到和请求路径匹配的Controller的,本文不会详细讲匹配的逻辑,这里的重点是,我们所定义的Controller是如何添加到Spring容器中去的?
答案很简单,肯定是在创建Spring容器过程中进行扫描,扫描我们所指定的配置,比如上面所配置的:<param-value>/WEB-INF/spring-mvc.xml</param-value>
那么,在我们启动Tomcat时,是通过什么机制触发了Spring容器的创建呢?
答案有两种:
1、利用ServletContainerInitializer机制。
2、利用Servlet中的init机制。
Spring MVC中用的是第二种。**
DispatcherServlet的父类是FrameworkServlet,FrameworkServlet的父类是HttpServletBean,在Tomcat启动时,会调用到HttpServletBean中的init()方法,从而调用到FrameworkServlet的initServletBean(),而在这个方法中就会去创建Spring容器,并完成扫描。
这样,当DispatcherServlet接收到某个请求后,就可以从它自己所创建的Spring容器中匹配对应的Controller了,从而完成请求的执行。
新版Spring MVC+Tomcat
随着Spring MVC的发展,不建议大家使用web.xml了。在新版Spring MVC中,程序员可以不写web.xml,而是用过一个类来进行代替,这个类有程序员自己定义,比如:
public class ZhouyuWebApplicationInitializer implements WebApplicationInitializer {@Overridepublic void onStartup(javax.servlet.ServletContext servletContext) throws ServletException {AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();context.register(AppConfig.class);// Create and register the DispatcherServletDispatcherServlet servlet = new DispatcherServlet(context);ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);registration.setLoadOnStartup(1);registration.addMapping("/");}}
如果在你的项目中定义了这个类,那么你可以选择你想用的Spring容器,比如你可以依旧使用ClassPathXmlApplicationContext,也可以使用AnnotationConfigWebApplicationContext。如果你选择后置,那么spring.xml也不需要使用了,这就是大家所说的Spring MVC的零配置,其实就是去XML化。
这种方式利用就是Servlet规范中ServletContainerInitializer机制,注意并不是上面这个类所实现的接口WebApplicationInitializer,WebApplicationInitializer是Spring MVC所定义的接口。
public interface ServletContainerInitializer {void onStartup(Set<Class<?>> var1, ServletContext var2) throws ServletException;}
ServletContainerInitializer是Servlet规范下的一个接口,表示Servlet容器初始化器。
对于使用Tomcat的第三方,如果想在Tomcat启动过程中,想继续向Tomcat中添加servlet,或其他事情,那么则可以利用这种机制,Spring MVC就是利用这种机制,在Spring MVC中提供了SpringServletContainerInitializer类实现了ServletContainerInitializer。
@HandlesTypes(WebApplicationInitializer.class)public class SpringServletContainerInitializer implements ServletContainerInitializer {@Overridepublic void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)throws ServletException {// 遍历调用webAppInitializerClasses中的onStartup()方法}}
Tomcat在启动时,会调用SpringServletContainerInitializer中的onStartup方法,并将所有WebApplicationInitializer接口的实现类传给onStartup方法,在onStartup方法中会遍历调用所有WebApplicationInitializer的onStartup方法,并将servletContext作为入参,这时就会调用到ZhouyuWebApplicationInitializer的onStartup方法,从而完成Spring容器的创建、已经将DispatcherServlet定义出来并添加到Tomcat中去。
以上,就是Spring MVC和Tomcat两者之间的运行原理。
Spring Boot+Tomcat
上面分析了这么久的Spring MVC+Tomcat这一套,有同学可能发现Spring MVC中也提到了零配置,去XML化,是不是Spring Boot中不需要写XML的原因也是那样,答案是:完全没有关系,Spring Boot+Tomcat这一套的工作原理是完全不一样的。
根本原因在于,Spring Boot+Tomcat这一套,是先创建的Spring容器,然后再启动的Tomcat,所以这一套要解决的问题是:如何启动Tomcat,如何将DispatcherServlet注册到Tomcat中?
对于这个问题其实比较简单,但在这个问题基础上,还有一些扩展性的问题,比如作为Spring Boot的用户,如果不想用Tomcat,想用Jetty,Spring Boot如何实现这个功能?程序员通过什么方式来定义DispatcherServlet。
Spring Boot如何启动Tomcat?
使用内嵌Tomcat即可,Tomcat是用java写的,所以在Spring Boot完全可以构造一个Tomcat对象,然后调用它的start方法完成Tomcat的启动。
Spring Boot如何判断到底是用Tomcat还是Jetty?
答案是classpath如果存在Tomcat.class就用Tomcat,如果存在Jetty.class就用Jetty。而这个功能是通过@ConditionalOnClass来实现的。比如在Spring Boot中存在这个类:
@Configuration(proxyBeanMethods = false)class ServletWebServerFactoryConfiguration {@Configuration(proxyBeanMethods = false)@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)public static class EmbeddedTomcat {@Beanpublic TomcatServletWebServerFactory tomcatServletWebServerFactory(ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,ObjectProvider<TomcatContextCustomizer> contextCustomizers,ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();factory.getTomcatConnectorCustomizers().addAll(connectorCustomizers.orderedStream().collect(Collectors.toList()));factory.getTomcatContextCustomizers().addAll(contextCustomizers.orderedStream().collect(Collectors.toList()));factory.getTomcatProtocolHandlerCustomizers().addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));return factory;}}/*** Nested configuration if Jetty is being used.*/@Configuration(proxyBeanMethods = false)@ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class })@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)public static class EmbeddedJetty {@Beanpublic JettyServletWebServerFactory JettyServletWebServerFactory(ObjectProvider<JettyServerCustomizer> serverCustomizers) {JettyServletWebServerFactory factory = new JettyServletWebServerFactory();factory.getServerCustomizers().addAll(serverCustomizers.orderedStream().collect(Collectors.toList()));return factory;}}/*** Nested configuration if Undertow is being used.*/@Configuration(proxyBeanMethods = false)@ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)public static class EmbeddedUndertow {@Beanpublic UndertowServletWebServerFactory undertowServletWebServerFactory(ObjectProvider<UndertowDeploymentInfoCustomizer> deploymentInfoCustomizers,ObjectProvider<UndertowBuilderCustomizer> builderCustomizers) {UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();factory.getDeploymentInfoCustomizers().addAll(deploymentInfoCustomizers.orderedStream().collect(Collectors.toList()));factory.getBuilderCustomizers().addAll(builderCustomizers.orderedStream().collect(Collectors.toList()));return factory;}}}
这个类存在三段类似的代码,分别对应Tomcat、Jetty、Undertow。
举个例子,当classpath中如果不存在Tomcat的jar,那么Spring Boot通过解析@ConditionalOnClass注解,会将@ConditionalOnClass中指定的类的类名拿到,然后用classLoader去加载类,如果加载不到会抛异常,Spring Boot会捕获这个异常,并且返回false,表示当前条件不匹配。
在Spring Boot程序员通过什么方式来定义DispatcherServlet? 并且通过什么方式将DispatcherServlet添加到Tomcat容器中?
既然使用Spring,那么就通过定义Bean的方式来自定义DispatcherServlet,比如:
@Bean(name = "dispatcherServlet")public DispatcherServlet dispatcherServlet(HttpProperties httpProperties, WebMvcProperties webMvcProperties) {DispatcherServlet dispatcherServlet = new DispatcherServlet();return dispatcherServlet;}
但是光光定义一个DispatcherServlet是不够的,后续将这个DispatcherServlet添加到Tomcat中去时,还需给servlet取一个name,还需要设置mapping关系。所以在定义DispatcherServlet时还需要定义其他信息,这时Spring Boot提供了另外一个类DispatcherServletRegistrationBean,通过这个类可以定义跟Servlet相关的其他配置信息。
所以Spring Boot在创建了一个Tomcat对象之后,就可以从Spring容器中获取到DispatcherServletRegistrationBean,就相当于获取到了所定义的Servlet及其相关信息。
那么Spring Boot中是通过什么机制向Tomcat中添加DispatcherServlet的呢?
在Spring Boot中设计一套ServletContextInitializer机制,也就是ServletContext初始化器,通过该机制,可以很灵活的对ServletContext进行操作,包括向它添加Servlet。上文的DispatcherServletRegistrationBean就实现了ServletContextInitializer接口,到时Spring Boot会写将所有ServletContextInitializer获取出来,然后调用其onStartup()方法。从而调用DispatcherServletRegistrationBean中的逻辑,完成DispatcherServlet的注册。
Tomcat对象创建了之后,会组装initializers,构造一个TomcatStarter,并添加到为ServletContainerInitializer到Tomcat中,后续Tomcat开始启动,启动过程中就会调用TomcatStarter的onStartup的方法,从而调用initializers的onStartup,从而将DispatcherServlet添加到Tomcat中。
