SpringBoot为什么没有web.xml

几乎所有人都是从 servlet,jsp,filter 开始编写自己的第一个 hello world 工程。

那时,还得在 web.xml 文件中编写繁琐的 servlet 和 filter 的配置。

随着 spring 的普及,配置逐渐演变成了两种方式— java configuration 和 xml 配置共存。

现如今,springboot 的普及,java configuration 成了主流,xml 配置似乎已经“灭绝”了。

那么web.xml 中的配置项又是被什么替代项取代了

servlet3.0之前

servlet3.0规范之前 我们是这样写 servlet 和 filter

public class HelloWorldServlet extends HttpServlet {
 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/plain");
        PrintWriter out = resp.getWriter();
        out.println("hello world");
    }
public class HelloWorldFilter implements Filter {
 
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
 
    }
 
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("触发 hello world 过滤器...");
        filterChain.doFilter(servletRequest,servletResponse);
    }
 
    @Override
    public void destroy() {
 
    }
}

同时还得在 web.xml 中配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
        http://java.sun.com/xml/ns/javaee/web-app_4_0.xsd"
           version="4.0">
 
    <servlet>
        <servlet-name>HelloWorldServlet</servlet-name>
        <servlet-class>moe.cnkirito.servlet.HelloWorldServlet</servlet-class>
    </servlet>
 
    <servlet-mapping>
        <servlet-name>HelloWorldServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
 
    <filter>
        <filter-name>HelloWorldFilter</filter-name>
        <filter-class>moe.cnkirito.filter.HelloWorldFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>HelloWorldFilter</filter-name>
        <url-pattern>/hello</url-pattern>
    </filter-mapping>
 
</web-app>

servlet3.0 新特性

Servlet 3.0 是 Java EE 6 规范体系中一员,版本在 Servlet 2.5的基础上提供了若干新特性用于简化 Web 应用的开发和部署。其中一项新特性便是提供了无 xml 配置的特性。

servlet3.0 首先提供了 @WebServlet@WebFilter 等注解,这样便有了抛弃 web.xml 的第一个途径,凭借注解声明 servlet 和 filter 来做到这一点。

除了这种方式,servlet3.0 规范还提供了更强大的功能,可以在运行时 动态注册 servletfilterlistener ServletContext 为动态配置 Servlet 增加了如下方法:

//动态注册为一个可以用于服务的 Servlet
* Servletegistration.Dynamic addServlet(String servletName,Class<? extends Servlet> servletClass)

* ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)

* ServletRegistration.Dynamic addServlet(String servletName, String className)

//创建的 Servlet,通常需要做一些自定义的配置,然后使用 addServlet() 方法来将其注册
* T createServlet(Class clazz)

//动态为 Servlet 增加映射信息,等价于 web.xml 中为存在的 Servlet 增加映射信息
* ServletRegistration getServletRegistration(String servletName)

* Map<String,? extends ServletRegistration> getServletRegistrations()

以上 ServletContext 新增的方法要么是在 ServletContextListenercontexInitialized 方法中调用,要么是在 ServletContainerInitializer 的 onStartup() 方法中调用

ServletContainerInitializer 也是 Servlet 3.0 新增的一个接口,容器在启动时使用 JAR 服务 API(JAR Service API) 来发现 ServletContainerInitializer 的实现类,并且容器将 WEB-INF/lib 目录下 JAR 包中的类都交给该类的 onStartup() 方法处理,我们通常需要在该实现类上使用@HandlesTypes 注解来将感兴趣的一些类注入到ServletContainerInitializerde的onStartup方法作为参数传入

一个典型的 servlet3.0+ 的 web 项目结构如下:

├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── moe
    │   │       └── cnkirito
    │   │           ├── CustomServletContainerInitializer.java
    │   │           ├── filter
    │   │           │   └── HelloWorldFilter.java
    │   │           └── servlet
    │   │               └── HelloWorldServlet.java
    │   └── resources
    │       └── META-INF
    │           └── services
    │               └── javax.servlet.ServletContainerInitializer
    └── test
        └── java

CustomServletContainerInitializer ,它实现了 javax.servlet.ServletContainerInitializer 接口,用来在 web 容器启动时加载指定的 servlet 和 filter


public class CustomServletContainerInitializer implements ServletContainerInitializer {
 
  private final static String JAR_HELLO_URL = "/hello";
 
  @Override
  public void onStartup(Set<Class<?>> c, ServletContext servletContext) {
 
    System.out.println("创建 helloWorldServlet...");
 
    ServletRegistration.Dynamic servlet = servletContext.addServlet(
            HelloWorldServlet.class.getSimpleName(),
            HelloWorldServlet.class);
    servlet.addMapping(JAR_HELLO_URL);
 
    System.out.println("创建 helloWorldFilter...");
 
    FilterRegistration.Dynamic filter = servletContext.addFilter(
            HelloWorldFilter.class.getSimpleName(), HelloWorldFilter.class);
 
    EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);
    dispatcherTypes.add(DispatcherType.REQUEST); 
    dispatcherTypes.add(DispatcherType.FORWARD); 
 
    filter.addMappingForUrlPatterns(dispatcherTypes, true, JAR_HELLO_URL);
 
  }
}

ServletContext 我们称之为 servlet 上下文,它维护了整个 web 容器中注册的 servlet,filter,listener

可以使用 servletContext.addServlet 等方法来添加 servlet 一般正常的流程是使用 @HandlesTypes 指定需要处理的 class,而后对 Set<Class<?>> 进行判断是否属于该 class,onStartup 会加载不需要被处理的一些 class

声明一个 ServletContainerInitializer 的实现类,web 容器并不会识别它,所以,需要借助 SPI 机制来指定该初始化类,这一步骤是通过在项目路径下创建 META-INF/services/javax.servlet.ServletContainerInitializer 来做到的,它只包含一行内容

moe.cnkirito.CustomServletContainerInitializer

使用 ServletContainerInitializer 和 SPI 机制,我们的 web 应用便可以彻底摆脱 web.xml 了

Spring 是如何支持 servlet3.0 的

Spring 同样借助 SPI 机制 在其jar中定义有 ServletContainerInitializer

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    @Override
	public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException {
 
		List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();
 
		if (webAppInitializerClasses != null) {
			for (Class<?> waiClass : webAppInitializerClasses) {
				// Be defensive: Some servlet containers provide us with invalid classes,
				// no matter what @HandlesTypes says...
                // 由于 servlet 厂商实现的差异,onStartup 方法会加载我们本不想处理的 class,所以进行了特判
				if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
						WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
					try {
						initializers.add((WebApplicationInitializer) waiClass.newInstance());
					}
					catch (Throwable ex) {
						throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
					}
				}
			}
		}
 
		if (initializers.isEmpty()) {
			servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
			return;
		}
 
		servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
		AnnotationAwareOrderComparator.sort(initializers);
        /* spring 并没有在 SpringServletContainerInitializer 中直接对 servlet 和 filter 进行注册,
        委托给了一个陌生的类 WebApplicationInitializer ,*/
		for (WebApplicationInitializer initializer : initializers) {
			initializer.onStartup(servletContext);
		}
	}
}

Servlet3.0{@link ServletContainerInitializer}旨在支持使用Spring的{@link WebApplicationInitializer}SPI对Servlet容器进行基于代码的配置,而不是(或可能与)传统的{@code web.xml}。

WebApplicationInitializer 类便是 spring 用来初始化 web 环境的委托者类,它通常有三个实现类: 0bfbffc332a04a39908c6226e633a03f-image.png

其中AbstractDispatcherServletInitializer#registerDispatcherServlet 便是无 web.xml 前提下创建 dispatcherServlet 的关键代码

SpringBoot 如何加载 Servlet

SpringBoot没有完全遵守 servlet3.0 的规范

注册方式一:servlet3.0注解+@ServletComponentScan

Springboot 依旧兼容 servlet3.0 一系列以 @Web* 开头的注解:@WebServlet,@WebFilter,@WebListener


@WebServlet("/hello")
public class HelloWorldServlet extends HttpServlet{}

@WebFilter("/hello/*")
public class HelloWorldFilter implements Filter {}

@SpringBootApplication
@ServletComponentScan  //扫描这些类
public class SpringBootServletApplication {
   public static void main(String[] args) {
      SpringApplication.run(SpringBootServletApplication.class, args);
   }
}

注册方式二:RegistrationBean

@Bean
public ServletRegistrationBean helloWorldServlet() {
    ServletRegistrationBean helloWorldServlet = new ServletRegistrationBean();
    myServlet.addUrlMappings("/hello");
    myServlet.setServlet(new HelloWorldServlet());
    return helloWorldServlet;
}

@Bean
public FilterRegistrationBean helloWorldFilter() {
    FilterRegistrationBean helloWorldFilter = new FilterRegistrationBean();
    myFilter.addUrlPatterns("/hello/*");
    myFilter.setFilter(new HelloWorldFilter());
    return helloWorldFilter;
}

ServletRegistrationBeanFilterRegistrationBean 都集成自 RegistrationBean ,RegistrationBean 是 springboot 中广泛应用的一个注册类,负责把 servlet,filter,listener 给容器化,使他们被 spring 托管,并且完成自身对 web 容器的注册 7de2a9788cc241179435729718813b3b-image.png

从图中可以看出 RegistrationBean 的地位,它的几个实现类作用分别是:帮助容器注册 filter,servlet,listener,最后的 DelegatingFilterProxyRegistrationBean 使用的不多,但熟悉 SpringSecurity 的朋友不会感到陌生,SpringSecurityFilterChain 就是通过这个代理类来调用的。另外 RegistrationBean 实现了 ServletContextInitializer 接口,这个接口将会是下面分析的核心接口

Initializer被替换为TomcatStarter

当使用内嵌的 tomcat 时, springboot 完全走了另一套初始化流程,完全没有使用前面提到的 SpringServletContainerInitializer,而是进入了 TomcatStarter 这个类中

88c3aa6d0a4d42fbab52bfaeb4b65cf0-image.png

springboot 考虑到了如下的问题,我们在使用 springboot 时,开发阶段一般都是使用内嵌 tomcat 容器,但部署时却存在两种选择:一种是打成 jar 包,使用 java -jar 的方式运行;另一种是打成 war 包,交给外置容器去运行。前者就会导致容器搜索算法出现问题,因为这是 jar 包的运行策略,不会按照 servlet3.0 的策略去加载 ServletContainerInitializer!最后作者还提供了一个替代选项:ServletContextInitializer,注意是 ServletContextInitializer!它和 ServletContainerInitializer 长得特别像,别搞混淆了,前者 ServletContextInitializerorg.springframework.boot.web.servlet.ServletContextInitializer,后者 ServletContainerInitializer 是 javax.servlet.ServletContainerInitializer,前文还提到 RegistrationBean 实现了 ServletContextInitializer 接口

TomcatStarter中的ServletContextInitializer是关键

TomcatStarter 中的 org.springframework.boot.context.embedded.ServletContextInitializer 是 springboot 初始化 servlet,filter,listener 的关键

class TomcatStarter implements ServletContainerInitializer {

   private final ServletContextInitializer[] initializers;

   TomcatStarter(ServletContextInitializer[] initializers) {
      this.initializers = initializers;
   }

   @Override
   public void onStartup(Set<Class<?>> classes, ServletContext servletContext)
         throws ServletException {
         for (ServletContextInitializer initializer : this.initializers) {
            initializer.onStartup(servletContext);
         }
   }
}

可以看出 TomcatStarter 的主要逻辑,它其实就是负责调用一系列 ServletContextInitializer 的 onStartup 方。 总结如下

EmbeddedWebApplicationContext中的6层迭代加载

ApplicationContext 大家应该是比较熟悉的,这是 spring 一个比较核心的类,一般我们可以从中获取到那些注册在容器中的托管 Bean, EmbeddedWebApplicationContext 是怎么获取到所有的 servlet filter listener 的!以下方法均出自于 EmbeddedWebApplicationContext。

//第一层:onRefresh 是 ApplicationContext 的生命周期方法,EmbeddedWebApplicationContext 的实现非常简单,只干了一件事  createEmbeddedServletContainer 连接到了第二层
@Override
protected void onRefresh() {
   super.onRefresh();
   try {
      createEmbeddedServletContainer();//第二层的入口
   }
   catch (Throwable ex) {
      throw new ApplicationContextException("Unable to start embedded container",
            ex);
   }
}


//第二层:createEmbeddedServletContainer()创建一个内嵌的 servlet 容器,ServletContainer 其实就是 servlet filter listener 的总称。 
private void createEmbeddedServletContainer() {
   EmbeddedServletContainer localContainer = this.embeddedServletContainer;
   ServletContext localServletContext = getServletContext();
   if (localContainer == null && localServletContext == null) {
      EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();
      this.embeddedServletContainer = containerFactory
            .getEmbeddedServletContainer(getSelfInitializer());//第三层的入口
   }
   else if (localServletContext != null) {
      try {
         getSelfInitializer().onStartup(localServletContext);
      }
      catch (ServletException ex) {
         throw new ApplicationContextException("Cannot initialize servlet context",
               ex);
      }
   }
   initPropertySources();
}


//第三层:getSelfInitializer() 便涉及到了我们最为关心的初始化流程

private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
   return new ServletContextInitializer() {
      @Override
      public void onStartup(ServletContext servletContext) throws ServletException {
         selfInitialize(servletContext);
      }
   };
}


private void selfInitialize(ServletContext servletContext) throws ServletException {
   prepareEmbeddedWebApplicationContext(servletContext);
   ConfigurableListableBeanFactory beanFactory = getBeanFactory();
   ExistingWebApplicationScopes existingScopes = new ExistingWebApplicationScopes(
         beanFactory);
   WebApplicationContextUtils.registerWebApplicationScopes(beanFactory,
         getServletContext());
   existingScopes.restore();
   WebApplicationContextUtils.registerEnvironmentBeans(beanFactory,
         getServletContext());
   //第四层的入口
   for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
      beans.onStartup(servletContext);
   }
}
/*解释下这里的 getSelfInitializer() 和 selfInitialize(ServletContext servletContext) 为什么要这么设计:这是典型的回调式方式,当匿名 ServletContextInitializer 类被 TomcatStarter 的 onStartup 方法调用,设计上是触发了 
selfInitialize(ServletContext servletContext) 的调用。所以这下就清晰了,为什么 TomcatStarter 中没有出现 RegisterBean ,其实是隐式触发了 EmbeddedWebApplicationContext 中的 selfInitialize 方法。selfInitialize 方法中的 getServletContextInitializerBeans() 成了关键。*/


//第四层:getServletContextInitializerBeans() ServletContextInitializerBeans 是用来加载 Servlet 和 Filter 的
/**
 * Returns {@link ServletContextInitializer}s that should be used with the embedded
 * Servlet context. By default this method will first attempt to find
 * {@link ServletContextInitializer}, {@link Servlet}, {@link Filter} and certain
 * {@link EventListener} beans.
 * @return the servlet initializer beans
 */
protected Collection<ServletContextInitializer> getServletContextInitializerBeans() {
   return new ServletContextInitializerBeans(getBeanFactory());//第五层的入口
}


//第五层:ServletContextInitializerBeans的构造方法
public ServletContextInitializerBeans(ListableBeanFactory beanFactory) {
   this.initializers = new LinkedMultiValueMap<Class<?>, ServletContextInitializer>();
   addServletContextInitializerBeans(beanFactory);// 第六层的入口
   addAdaptableBeans(beanFactory);
   List<ServletContextInitializer> sortedInitializers = new ArrayList<ServletContextInitializer>();
   for (Map.Entry<?, List<ServletContextInitializer>> entry : this.initializers
         .entrySet()) {
      AnnotationAwareOrderComparator.sort(entry.getValue());
      sortedInitializers.addAll(entry.getValue());
   }
   this.sortedList = Collections.unmodifiableList(sortedInitializers);
}


//第六层:addServletContextInitializerBeans(beanFactory)
private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) {
   for (Entry<String, ServletContextInitializer> initializerBean : getOrderedBeansOfType(
         beanFactory, ServletContextInitializer.class)) {
      addServletContextInitializerBean(initializerBean.getKey(),
            initializerBean.getValue(), beanFactory);
   }
}

EmbeddedWebApplicationContext加载流程总结

  • EmbeddedWebApplicationContext 的 onRefresh 方法触发配置了一个匿名的 ServletContextInitializer。
  • 这个匿名的 ServletContextInitializer 的 onStartup 方法会去容器中搜索到了所有的 RegisterBean 并按照顺序加载到 ServletContext 中。
  • 这个匿名的 ServletContextInitializer 最终传递给 TomcatStarter,由 TomcatStarter 的 onStartup 方法去触发 ServletContextInitializer 的 onStartup 方法,最终完成装配!

第三种注册 Servlet 的方式

ServletContextInitializer 其实是 spring 中 ServletContainerInitializer 的代理,虽然 springboot 中 Servlet3.0 不起作用了,但它的代理还是会被加载的

@Configuration
public class CustomServletContextInitializer implements ServletContextInitializer {

    private final static String JAR_HELLO_URL = "/hello";

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        System.out.println("创建 helloWorldServlet...");

        ServletRegistration.Dynamic servlet = servletContext.addServlet(
                HelloWorldServlet.class.getSimpleName(),
                HelloWorldServlet.class);
        servlet.addMapping(JAR_HELLO_URL);

        System.out.println("创建 helloWorldFilter...");

        FilterRegistration.Dynamic filter = servletContext.addFilter(
                HelloWorldFilter.class.getSimpleName(), HelloWorldFilter.class);

        EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);
        dispatcherTypes.add(DispatcherType.REQUEST);
        dispatcherTypes.add(DispatcherType.FORWARD);

        filter.addMappingForUrlPatterns(dispatcherTypes, true, JAR_HELLO_URL);
    }
}

虽然 ServletCantainerInitializer 不能被内嵌容器加载,ServletContextInitializer 却能被 springboot 的 EmbeddedWebApplicationContext 加载到,从而装配其中的 servlet 和 filter。实际开发中,还是以一,二两种方法来注册为主

扩展

  1. TomcatStarter 既然不是通过 SPI 机制装配的,那是怎么被 spring 使用的?

被 new 出来的,在 TomcatEmbeddedServletContainerFactory#configureContext 中可以看到,TomcatStarter 是被主动实例化出来的,并且还传入了 ServletContextInitializer 的数组,和上面分析的一样,一共有三个 ServletContextInitializer,包含了 EmbeddedWebApplicationContext 中的匿名实现

protected void configureContext(Context context,
      ServletContextInitializer[] initializers) {
   TomcatStarter starter = new TomcatStarter(initializers);
   if (context instanceof TomcatEmbeddedContext) {
      // Should be true
      ((TomcatEmbeddedContext) context).setStarter(starter);
   }
   context.addServletContainerInitializer(starter, NO_CLASSES);
   ...
   }
}
  1. TomcatEmbeddedServletContainerFactory 又是如何被声明的?

只要类路径下存在 Tomcat 类,以及在 web 环境下,就会触发 springboot 的自动配置

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication
@Import(BeanPostProcessorsRegistrar.class)
public class EmbeddedServletContainerAutoConfiguration {

   /**
    * Nested configuration if Tomcat is being used.
    */
   @Configuration
   @ConditionalOnClass({ Servlet.class, Tomcat.class })
   @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
   public static class EmbeddedTomcat {

      @Bean
      public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
         return new TomcatEmbeddedServletContainerFactory();
      }

   }
}

总结

Servlet3.0的规范中提供了一种javaConfig的配置来代替我们原本的web.xml,这个javaConfig是就是 ServletContainerInitializer接口

可以动态加载servlet和filter

spi,即service privider interface,是jdk为厂商和插件提供的一种解耦机制。

spi的具体规范为:当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并通过反射机制实例化,完成模块的注入。 基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。

jdk提供服务实现查找的一个工具类:java.util.ServiceLoader


已有 0 条评论

    我有话说: