在最新的SpringMVC项目中,一个web项目中无需配置传统的web.xml配置文件就能正常启动运行,这是怎么实现的呢?其实这并不是SpringMVC的功劳,而是servlet3规范支持无需web.xml,以及web容器对这个规范的实现。

简单使用

配置

引入依赖:

... ....
        <!-- 指定servlet版本为3.0 -->
        <dependency>
			<groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <!-- 指定作用域为provided,避免与tomcat冲突 -->
            <scope>provided</scope>
        </dependency>

... ...
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <!-- 忽略无xml的错误 -->
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>

自定义Servlet

package com.morris.servlet;

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

public class HelloServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("hello");
        resp.getWriter().write("hello servlet 3.0");
    }
}

自定义ServletContainerInitializer

ServletContainerInitializer有一个onStartup方法,这个方法会在web容器启动时被调用,可以使用这个方法向容器中注入Servlet、Listener、Filter。

package com.morris.servlet;

import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import java.util.Set;

public class MyServletContainerInitializer implements ServletContainerInitializer {
    public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
        System.out.println("MyServletContainerInitializer");
        ServletRegistration.Dynamic servlet = servletContext.addServlet("helloServlet", HelloServlet.class);
        servlet.addMapping("/hello");

    }
}

配置ServletContainerInitializer,在resources目录下新建META-INF/services/javax.servlet.ServletContainerInitializer,文件内容为自定义的ServletContainerInitializer的全限定名:

com.morris.servlet.MyServletContainerInitializer

Tomcat容器在启动时会扫描所有jar中的META-INF/services/javax.servlet.ServletContainerInitializer文件,然后加载文件中指定的类,并调用其onStartup()方法。

发布至tomcat

servlet3.0需要tomcat7及其以上版本,这里使用内置的Tomcat。

引入Tomcat的依赖:

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-core</artifactId>
    <version>8.5.23</version>
</dependency>
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-jasper</artifactId>
    <version>8.5.16</version>
</dependency>

使用代码方式启动Tomcat:

package com.morris;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.core.AprLifecycleListener;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardServer;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.webresources.DirResourceSet;
import org.apache.catalina.webresources.StandardRoot;

import javax.servlet.ServletException;
import java.io.File;

public class Application {

    public static void main(String[] args) throws LifecycleException, ServletException {
        Tomcat tomcat = new Tomcat(); // 创建Tomcat容器
        tomcat.setPort(8080); // 端口号设置
        String basePath = System.getProperty("user.dir");
        tomcat.setBaseDir(basePath);

        //改变文件读取路径,从resources目录下去取文件
        StandardContext ctx = (StandardContext) tomcat.addWebapp("/", basePath + File.separator + "src" + File.separator + "main" + File.separator + "resources");
        // 禁止重新载入
        ctx.setReloadable(false);
        // class文件读取地址
        File additionWebInfClasses = new File(basePath, "target/classes");
        // 创建WebRoot
        WebResourceRoot resources = new StandardRoot(ctx);
        // tomcat内部读取Class执行
        resources.addPreResources(new DirResourceSet(resources, "/WEB-INF/classes", additionWebInfClasses.getAbsolutePath(), "/"));

        ctx.setResources(resources);
        tomcat.start();
        // 异步等待请求执行
        tomcat.getServer().await();
    }
}

最后浏览器输入:http://localhost:8080/hello ,就能看到HelloServlet返回的响应结果了。

注入组件的方法

注解

加了@WebServlet、@WebFilter、@WebListener注解的类会自动扫描并加入到容器中,无需配置。

@WebServlet

可以通过@WebServlet向容器中注入Servlet。

package com.morris.servlet;

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

@WebServlet(urlPatterns = "/user")
public class UserServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("user");
        resp.getWriter().write("hello morris");
    }
}

类似在传统的web.xml中进行如下配置:

<servlet>
	<servlet-name>userServlet</servlet-name>
	<servlet-class>com.morris.servlet.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
	<servlet-name>userServlet</servlet-name>
	<url-pattern>/user</url-pattern>
</servlet-mapping>

@WebFilter

可以通过@WebFilter向容器中注入Filter。

package com.morris.servlet;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter(urlPatterns = "/*")
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("myFilter init");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("myFilter doFilter");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        System.out.println("myFilter destroy");
    }
}

类似在传统的web.xml中进行如下配置:

<filter>
    <filter-name>myFilter</filter-name>
    <filter-class>com.morris.servlet.MyFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>myFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

@WebListener

可以通过@WebListener向容器中注入Listener。

package com.morris.servlet;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class MyListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        System.out.println("myListener contextInitialized");
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        System.out.println("myListener contextDestroyed");
    }
}

类似在传统的web.xml中进行如下配置:

<listener>
    <listener-class>com.morris.servlet.MyListener</listener-class>
</listener>

ServletContext注入

除了可以通过注解的方法向容器中注入Servlet、Filter、Listener组件外,还可以通过ServletContext对象手动注入组件。

public class MyServletContainerInitializer implements ServletContainerInitializer {
    public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
        
        System.out.println("MyServletContainerInitializer");

        // 手动添加servlet
        ServletRegistration.Dynamic servlet = servletContext.addServlet("helloServlet", HelloServlet.class);
        servlet.addMapping("/hello");

        // 手动添加filter
        FilterRegistration.Dynamic filter = servletContext.addFilter("myFilter", MyFilter.class);
        filter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");

        // 手动添加listener
        servletContext.addListener(MyListener.class);

    }
}

可以通过两种方法获得ServletContext对象,也就是可以在这两个地方注入Servlet、Filter、Listener组件(必须在项目启动的时候注入):

  • 实现ServletContainerInitializer接口得到ServletContext。
  • 实现ServletContextListener接口得到ServletContext,也就是Listener。

@HandlesTypes

@HandlesTypes不能向容器中注入Servlet、Filter、Listener组件。

容器启动的时候会将@HandlesTypes指定的这个类型下面的子类(实现类,子接口、抽象类等)传递到实现了ServletContainerInitializer接口的onStartup()方法的参数set中。

假设现在HelloService的实现类或子接口如下:

【springmvc】servlet3.0无需配置web.xml配置文件的原理_web.xml

在MyServletContainerInitializer启动类上面增加@HandlesTypes注解指定HelloService.class:

@HandlesTypes(HelloService.class)
public class MyServletContainerInitializer implements ServletContainerInitializer {
    public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
        set.forEach(System.out::println);
    }
}

运行结果如下:

interface com.morris.service.HelloServiceExt
class com.morris.service.HelloServiceImpl
class com.morris.service.AbstractHelloService

可见HelloService的实现类或子接口都加入到了Set集合中。