对于工龄稍大一点的程序员同学,基本都用过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中加上这段配置:

  1. <servlet>
  2. <servlet-name>app</servlet-name>
  3. <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  4. <init-param>
  5. <param-name>contextConfigLocation</param-name>
  6. <param-value>/WEB-INF/spring-mvc.xml</param-value>
  7. </init-param>
  8. <load-on-startup>1</load-on-startup>
  9. </servlet>
  10. <servlet-mapping>
  11. <servlet-name>app</servlet-name>
  12. <url-pattern>/app/*</url-pattern>
  13. </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,而是用过一个类来进行代替,这个类有程序员自己定义,比如:

  1. public class ZhouyuWebApplicationInitializer implements WebApplicationInitializer {
  2. @Override
  3. public void onStartup(javax.servlet.ServletContext servletContext) throws ServletException {
  4. AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
  5. context.register(AppConfig.class);
  6. // Create and register the DispatcherServlet
  7. DispatcherServlet servlet = new DispatcherServlet(context);
  8. ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
  9. registration.setLoadOnStartup(1);
  10. registration.addMapping("/");
  11. }
  12. }

如果在你的项目中定义了这个类,那么你可以选择你想用的Spring容器,比如你可以依旧使用ClassPathXmlApplicationContext,也可以使用AnnotationConfigWebApplicationContext。如果你选择后置,那么spring.xml也不需要使用了,这就是大家所说的Spring MVC的零配置,其实就是去XML化。

这种方式利用就是Servlet规范中ServletContainerInitializer机制,注意并不是上面这个类所实现的接口WebApplicationInitializer,WebApplicationInitializer是Spring MVC所定义的接口。

  1. public interface ServletContainerInitializer {
  2. void onStartup(Set<Class<?>> var1, ServletContext var2) throws ServletException;
  3. }

ServletContainerInitializer是Servlet规范下的一个接口,表示Servlet容器初始化器。

对于使用Tomcat的第三方,如果想在Tomcat启动过程中,想继续向Tomcat中添加servlet,或其他事情,那么则可以利用这种机制,Spring MVC就是利用这种机制,在Spring MVC中提供了SpringServletContainerInitializer类实现了ServletContainerInitializer。

  1. @HandlesTypes(WebApplicationInitializer.class)
  2. public class SpringServletContainerInitializer implements ServletContainerInitializer {
  3. @Override
  4. public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
  5. throws ServletException {
  6. // 遍历调用webAppInitializerClasses中的onStartup()方法
  7. }
  8. }

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中存在这个类:

  1. @Configuration(proxyBeanMethods = false)
  2. class ServletWebServerFactoryConfiguration {
  3. @Configuration(proxyBeanMethods = false)
  4. @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
  5. @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
  6. public static class EmbeddedTomcat {
  7. @Bean
  8. public TomcatServletWebServerFactory tomcatServletWebServerFactory(
  9. ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
  10. ObjectProvider<TomcatContextCustomizer> contextCustomizers,
  11. ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
  12. TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
  13. factory.getTomcatConnectorCustomizers()
  14. .addAll(connectorCustomizers.orderedStream().collect(Collectors.toList()));
  15. factory.getTomcatContextCustomizers()
  16. .addAll(contextCustomizers.orderedStream().collect(Collectors.toList()));
  17. factory.getTomcatProtocolHandlerCustomizers()
  18. .addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
  19. return factory;
  20. }
  21. }
  22. /**
  23. * Nested configuration if Jetty is being used.
  24. */
  25. @Configuration(proxyBeanMethods = false)
  26. @ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class })
  27. @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
  28. public static class EmbeddedJetty {
  29. @Bean
  30. public JettyServletWebServerFactory JettyServletWebServerFactory(
  31. ObjectProvider<JettyServerCustomizer> serverCustomizers) {
  32. JettyServletWebServerFactory factory = new JettyServletWebServerFactory();
  33. factory.getServerCustomizers().addAll(serverCustomizers.orderedStream().collect(Collectors.toList()));
  34. return factory;
  35. }
  36. }
  37. /**
  38. * Nested configuration if Undertow is being used.
  39. */
  40. @Configuration(proxyBeanMethods = false)
  41. @ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
  42. @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
  43. public static class EmbeddedUndertow {
  44. @Bean
  45. public UndertowServletWebServerFactory undertowServletWebServerFactory(
  46. ObjectProvider<UndertowDeploymentInfoCustomizer> deploymentInfoCustomizers,
  47. ObjectProvider<UndertowBuilderCustomizer> builderCustomizers) {
  48. UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
  49. factory.getDeploymentInfoCustomizers()
  50. .addAll(deploymentInfoCustomizers.orderedStream().collect(Collectors.toList()));
  51. factory.getBuilderCustomizers().addAll(builderCustomizers.orderedStream().collect(Collectors.toList()));
  52. return factory;
  53. }
  54. }
  55. }

这个类存在三段类似的代码,分别对应Tomcat、Jetty、Undertow。

举个例子,当classpath中如果不存在Tomcat的jar,那么Spring Boot通过解析@ConditionalOnClass注解,会将@ConditionalOnClass中指定的类的类名拿到,然后用classLoader去加载类,如果加载不到会抛异常,Spring Boot会捕获这个异常,并且返回false,表示当前条件不匹配。

在Spring Boot程序员通过什么方式来定义DispatcherServlet? 并且通过什么方式将DispatcherServlet添加到Tomcat容器中?

既然使用Spring,那么就通过定义Bean的方式来自定义DispatcherServlet,比如:

  1. @Bean(name = "dispatcherServlet")
  2. public DispatcherServlet dispatcherServlet(HttpProperties httpProperties, WebMvcProperties webMvcProperties) {
  3. DispatcherServlet dispatcherServlet = new DispatcherServlet();
  4. return dispatcherServlet;
  5. }

但是光光定义一个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中。