前言

我们在开发 SpringMVC 项目时,都会在 web.xml 里配置 listener、servlet

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" metadata-complete="true" version="3.1">
  
  <!-- 因为配置了spring的ContextLoderListener监听器,所以需指定spring核心文件的位置 ,
 	       否则默认加载/WEB-INF/applicationContext.xml这个文件-->
  <context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:spring/spring-*.xml</param-value>
  </context-param>
  
   <!-- Spring监听器 -->
  <listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  
  <servlet>
    <servlet-name>springMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:spring/spring-*.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>springMVC</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
 
</web-app>

SpringBoot 的出现极大的简化了我们的开发流程,细心地同学可能会发现,SpringBoot 是没有 web.xml 的,那么 Servlet、 Filter 和 Listener 等繁琐的 web 的相关配置,SpringBoot 是用什么替代的呢?

这个问题不难回答,Servlet、Filter 和 Listener 并不会凭空的帮我们自动配好,SpringBoot 既然抛弃了 web.xml,那么它肯定是在代码里提供了配置功能的类。

其实开发一个 Java Web 项目,不用 web.xml 配置,并不是 SpringBoot 开创的新特性。早在 Serlvet3.0 之后,我们开发一个 Java Web 项目就可以不用 web.xml 配置了,而 SpringMVC 又是基于 Servlet 的,所以要弄清楚 SpringBoot 是用什么替代 web.xml 的,得先从 Servlet 说起。

Serlvet3.0 之前的时代

为了让同学们的印象更深刻,一步步的探索 web.xml 消失之谜,我们先回忆一下 N 年前(Serlvet3.0 之前)是怎么开发一个 Java Web 项目的。

新建一个动态的 Java Web 项目

image

可以看到 Eclipse 自动的帮我们创建了 web.xml 文件

image

创建 HelloServlet

package com.example.servlet;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class HelloServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	public HelloServlet() {

	}

	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		response.setContentType("text/plain");
		response.getWriter().println("hello world!");

	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doGet(request, response);
	}

}

创建 HelloFilter

package com.example.filter;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class HelloFilter implements Filter {

	public HelloFilter() {
		
	}

	public void destroy() {
		
	}

	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		System.out.println("触发 HelloFilter 过滤器");
		chain.doFilter(request, response);
	}

	public void init(FilterConfig fConfig) throws ServletException {

	}

}

创建 HelloListener

package com.example.listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class HelloListener implements ServletContextListener {

    public HelloListener() {
       
    }

	
    public void contextDestroyed(ServletContextEvent arg0)  { 
    	System.out.println("销毁 HelloListener 监听器");
    }


    public void contextInitialized(ServletContextEvent arg0)  { 
         System.out.println("触发 HelloListener 监听器");
    }
	
}

在 web.xml 里配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
  
  <listener>
    <listener-class>com.example.listener.HelloListener</listener-class>
  </listener>
  
  <servlet>
    <description></description>
    <display-name>HelloServlet</display-name>
    <servlet-name>HelloServlet</servlet-name>
    <servlet-class>com.example.servlet.HelloServlet</servlet-class>
  </servlet>
  
  <servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/hello</url-pattern>
  </servlet-mapping>
  
  <filter>
    <display-name>HelloFilter</display-name>
    <filter-name>HelloFilter</filter-name>
    <filter-class>com.example.filter.HelloFilter</filter-class>
  </filter>
  
  <filter-mapping>
    <filter-name>HelloFilter</filter-name>
    <url-pattern>/hello</url-pattern>
  </filter-mapping>
  
</web-app>

测试结果

运行 Tomcat,可以看到 HelloListener 输出了

image

访问 http://localhost:8080/hello

过滤器触发了

image

网页输出如下

image

至此一个 Java Web 的 hello world 就完成了

可以看到仅仅是一个小小的 hello world,都少不了各种配置,如果项目非常大,那可谓是 xml 地狱。

Servlet3.0 新特性

为了简化开发人员的配置,servlet3.0 提供了 @WebServlet,@WebFilter,@WebListener 等注解和 ServletContainerInitializer 接口,帮助开发人员彻底抛弃 web.xml。

为了便于理解,我们还是以代码来演示。

新建一个动态的 Java Web 项目,web 版本选择 3.1

image

建完后项目结构如下,可以看到,Eclipse 默认帮我们去掉了 web.xml

image

基于注解

基于注解开发比较简单,只需要在类上加上相应的注解即可

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    
}
@WebFilter("/hello")
public class HelloFilter implements Filter {
    
}
@WebListener
public class HelloListener implements ServletContextListener {
    
}

可以看到代码和上面的是一样的,只不过多了 @WebServlet、@WebFilter 和 @WebListener 注解

运行后访问的效果和上面的也是一样的

基于接口 ServletContainerInitializer

ServletContainerInitializer 是 Servlet 3.0 新增的一个接口,容器在启动时初始化 ServletContainerInitializer 的实现类,调用 onStartup 方法,以完成 Servlet、Filter 和 Listener 的注册。

创建 ServletContainerInitializer 的实现类 MyServletContainerInitializer

package com.example.web;

import java.util.EnumSet;
import java.util.Set;
import javax.servlet.DispatcherType;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import com.example.filter.HelloFilter;
import com.example.listener.HelloListener;
import com.example.servlet.HelloServlet;

public class MyServletContainerInitializer implements ServletContainerInitializer {

	private static final String HELLO_URL = "/hello";

	@Override
	public void onStartup(Set<Class<?>> arg, ServletContext servletContext) throws ServletException {

		// 注册 HelloServlet
		Dynamic servlet = servletContext.addServlet(HelloServlet.class.getSimpleName(), HelloServlet.class);

		// 添加 Servlet 映射
		servlet.addMapping(HELLO_URL);

		// 注册 HelloFilter
		javax.servlet.FilterRegistration.Dynamic filter = servletContext.addFilter(HelloFilter.class.getSimpleName(),
				HelloFilter.class);
		
		// 添加 Filter 映射
		EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);
		dispatcherTypes.add(DispatcherType.REQUEST);
		dispatcherTypes.add(DispatcherType.FORWARD);
		filter.addMappingForUrlPatterns(dispatcherTypes, true, HELLO_URL);

		// 注册 HelloListener
		servletContext.addListener(HelloListener.class);

	}

}

指定实现类

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

com.example.web.MyServletContainerInitializer

image

测试结果

运行 Tomcat,发现控制台并没有输出 HelloListener 初始化的信息,访问 /hello 也没有输出 “hello world!”。

说明 Servlet、Filter 和 Listener 并没有注册成功,进而说明容器并没有初始化 MyServletContainerInitializer 类,自然也不用调用 onStartup 方法。

测试结论

折腾了好久才知道,原来基于 ServletContainerInitializer 接口注册 Servlet、Filter 和 Listener 不能直接在当前工程下借助 SPI 机制来指定初始化类,而是必须将 ServletContainerInitializer 的实现类打成 jar 包,在 jar 包里的 META-INF 文件夹里指定初始化类才行!

每个框架要使用 ServletContainerInitializer 就必须在对应的 jar 包的 META-INF/services 目录创建一个名为 javax.servlet.ServletContainerInitializer 的文件,文件内容指定具体的 ServletContainerInitializer 实现类,当web容器启动时就会运行这个初始化器做一些组件内的初始化工作。

以 jar 包的形式加载

如果是以 jar 包的形式才能正确的加载并完成相应的注册功能,那说明我们得另外新建一个 Java Web 工程,在这个新的工程里指定 ServletContainerInitializer 的实现类的初始化路径,并开发一个接口供客户端做具体的注册功能,所以我们要改造 ServletContainerInitializer 的实现类。

这里涉及到一个注解:@HandlesTypes

@HandlesTypes 注解会自动扫描项目中所的指定 .class 的实现类,并将其全部注入 onStartup 方法的第一个参数。

由此我们可以开放出一个接口,这个接口只有一个方法,专门用来注册 Servlet、Filter、Listener。 然后通过 @HandlesTypes 注解加载这个接口的实现类,最后在 ServletContainerInitializer 的实现类的 onStartup 方法中通过反射创建出这个接口的实现类,然后调用注册的方法即可完成注册。

请看改造后的 MyServletContainerInitializer

package com.example.web;

import java.lang.reflect.Modifier;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.HandlesTypes;

/**
 * @HandlesTypes 注解会自动扫描项目中所有的 WebApplicationInitializer.class的实现类,并将其全部注入 onStartup 方法的第一个参数。
 * @author miansen.wang
 * @date 2019-06-12
 */
@HandlesTypes({ WebApplicationInitializer.class })
public class MyServletContainerInitializer implements ServletContainerInitializer {

	/**
	 * 这个方法有两个参数:
	 * (1)webAppInitializerClasses:被 @HandlesTypes 指定的类及其子类
	 * (2)servletContext:servlet 上下文,它维护了整个 web 容器中注册的 servlet,filter,listener
	 */
	@Override
	public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException {

		List<WebApplicationInitializer> initializers = new LinkedList<>();

		// 通过反射将 WebApplicationInitializer 的实现类添加进 initializers
		if (webAppInitializerClasses != null) {
			for (Class<?> waiClass : webAppInitializerClasses) {

				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;
		}

		// 遍历执行实现类的方法,具体的注册功能在实现类里实现
		for (WebApplicationInitializer initializer : initializers) {
			initializer.onStartup(servletContext);
		}

	}

}

开发出来的接口 WebApplicationInitializer

package com.example.web;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;

public interface WebApplicationInitializer {

	/**
	 * 注册 Servlet、Filter、Listener 的方法,由客户端实现
	 * @param paramServletContext
	 * @throws ServletException
	 */
	public abstract void onStartup(ServletContext paramServletContext)
		    throws ServletException;
}

将这个工程打成 jar 包即可,记得要在 MATE-INF 文件夹里加入 services/javax.servlet.ServletContainerInitializer

并在里面添加 com.example.web.MyServletContainerInitializer

引入 jar 包

在工程里引入我们打好的 jar 包

image

然后实现 WebApplicationInitializer 接口

package com.example.initializer;

import java.util.EnumSet;
import javax.servlet.DispatcherType;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import com.example.filter.HelloFilter;
import com.example.listener.HelloListener;
import com.example.servlet.HelloServlet;
import com.example.web.WebApplicationInitializer;

public class MyWebApplicationInitializer implements WebApplicationInitializer {

	private static final String HELLO_URL = "/hello";

	/**
	 * 这个方法会在容器启动时,在 ServletContainerInitializer 接口的实现类中调用
	 */
	@Override
	public void onStartup(ServletContext servletContext) throws ServletException {
		// 注册 HelloServlet
		Dynamic servlet = servletContext.addServlet(HelloServlet.class.getSimpleName(), HelloServlet.class);

		// 添加 Servlet 映射
		servlet.addMapping(HELLO_URL);

		// 注册 HelloFilter
		javax.servlet.FilterRegistration.Dynamic filter = servletContext.addFilter(HelloFilter.class.getSimpleName(),
				HelloFilter.class);

		// 添加 Filter 映射
		EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);
		dispatcherTypes.add(DispatcherType.REQUEST);
		dispatcherTypes.add(DispatcherType.FORWARD);
		filter.addMappingForUrlPatterns(dispatcherTypes, true, HELLO_URL);

		// 注册 HelloListener
		servletContext.addListener(HelloListener.class);
	}

}

测试结果

运行 Tomcat 并访问 “/hello”

HelloListener

image

HelloFilter

image

HelloServlet

image

测试结论

可以看到成功注册了 HelloListener、HelloFilter 和 HelloServlet

看到这里的同学对于 “SpringBoot 项目为什么没有 web.xml” 这个问题想必也已经有答案了。

SpringBoot 正是基于 Servlet 3.0 提供的接口 ServletContainerInitializer 来摆脱 web.xml 的。

SpringMVC 的实现

SpringMVC 中 ServletContainerInitializer 的实现类:SpringServletContainerInitializer 的源码

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

    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
        List<WebApplicationInitializer> initializers = new LinkedList();
        Iterator var4;
        if (webAppInitializerClasses != null) {
            var4 = webAppInitializerClasses.iterator();

            while(var4.hasNext()) {
                Class<?> waiClass = (Class)var4.next();
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass, new Class[0]).newInstance());
                    } catch (Throwable var7) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
                    }
                }
            }
        }

        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
        } else {
            servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
            AnnotationAwareOrderComparator.sort(initializers);
            var4 = initializers.iterator();

            while(var4.hasNext()) {
                WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();
                initializer.onStartup(servletContext);
            }

        }
    }
}

可以看到注解 @HandlesTypes 指定加载的类是 WebApplicationInitializer

onStartup 方法里通过反射创建出 WebApplicationInitializer 的子类并调用

initializer.onStartup(servletContext);

WebApplicationInitializer 接口的源码

public interface WebApplicationInitializer {
    void onStartup(ServletContext var1) throws ServletException;
}

AbstractDispatcherServletInitializer 部分源码,我们所熟知的 DispatcherServlet 就是在此初始化的

protected void registerDispatcherServlet(ServletContext servletContext) {
        String servletName = this.getServletName();
        Assert.hasLength(servletName, "getServletName() must not return null or empty");
        WebApplicationContext servletAppContext = this.createServletApplicationContext();
        Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");
        FrameworkServlet dispatcherServlet = this.createDispatcherServlet(servletAppContext);
        Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
        dispatcherServlet.setContextInitializers(this.getServletApplicationContextInitializers());
        Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
        if (registration == null) {
            throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. Check if there is another servlet registered under the same name.");
        } else {
            registration.setLoadOnStartup(1);
            registration.addMapping(this.getServletMappings());
            registration.setAsyncSupported(this.isAsyncSupported());
            Filter[] filters = this.getServletFilters();
            if (!ObjectUtils.isEmpty(filters)) {
                Filter[] var7 = filters;
                int var8 = filters.length;

                for(int var9 = 0; var9 < var8; ++var9) {
                    Filter filter = var7[var9];
                    this.registerServletFilter(servletContext, filter);
                }
            }

            this.customizeRegistration(registration);
        }
    }

关于 SpringMVC 的源码这里就不做过多的解读了,感兴趣的同学可以自己去折腾。

最后我们找到 spring-web 的 jar 包,可以看到 MATE-INF 文件夹下指定了 SpringServletContainerInitializer 的全路径

image

image

至此,SpringBoot 就可以完全抛弃 web.xml

原文链接:https://miansen.wang/2019/06/11/1/