Spring Boot「38」开发基于 Servlet 的应用

80 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 23 天,点击查看活动详情

Servlet 规范中对 Servlet 的定义是:

A servlet is a Java™ technology-based Web component, managed by a container, that generates dynamic content.

简单解释下,Servlet 是基于 Java 技术开发的 Web 组件,托管在容器中,用于生成动态内容。 常见的 Servlet 容器有 Tomcat、Jetty 等。 今天,我将介绍下,如何在 Spring Boot 中开发基于 Servlet 的 Web 应用。

01-Servlet 接口

Servlet 规范中定义了 Servlet 接口,主要是它的生命周期函数:

  • init,仅被调用一次。如果请求的 Servlet 实例不存在,Servlet 容器会尝试加载对应的 Servlet 实现类,创建它的实例,并调用 init 方法。
  • destroy,仅被调用一次。容器通过这个方法,来下线 Servlet 提供的服务。
  • service,容器通过这个方法,将请求、响应交给特定的 Servlet 来处理。

在最初阶段,Web 应用对 Servlet 的声明是在 web.xml 中的,例如:

<servlet>
   <servlet-name>FormServlet</servlet-name>
   <servlet-class>self.samson.example.servlet.FormServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>FormServlet</servlet-name>
    <url-pattern>/calculateServlet</url-pattern>
</servlet-mapping>

上述声明的含义是,声明一个 Servlet,并通过 servlet-class 指定它的实现类。 然后,通过 servlet-mapping 将 Servlet 与某个 servlet path 关联起来。 后续对 servlet path 的请求,都会被 Servlet 容器路由到这个 Servlet 的 service 方法中。

在后续的版本中,Servlet 规范支持了程序化配置,即通过注解来定义 Servlet 及其与 servlet path 的映射关系。 例如,与上述 xml 等价的 Java 注解版本如下:

@WebServlet(name = "FormServlet", urlPatterns = "/calculateServlet")
public class FormServlet extends HttpServlet {
}

注:这里需要注意的是,FormServlet 继承了 HttpServlet,并非是直接通过实现 Servlet 接口。 HttpServlet 是 javax.servlet-api-*.jar 中提供的基于 HTTP Servlet 接口实现,内置了许多 API,例如 doGet/doPost 等。 在开发时,如果你要开发的应用是基于 HTTP 协议的,更建议你使用我这种方式,可以避免编写过多的代码。 如果现有实现不能满足你的业务需求是,再考虑直接实现 Servlet 接口。

我们熟知的 Spring MVC 中的 DispatcherServlet 就是一个典型的 Servlet 实现。 只不过,DispatcherServlet 自己又实现了一个称之为前端控制器的模式,可以让我们把业务写在各种 Controller 中。

02-Spring Boot 与 Servlet

当使用 Spring Boot 开发 Servlet 应用时,有一点需要特别注意。 Spring Boot 默认是使用嵌入式 Tomcat 作为应用容器的,它启动时并不会读取我们前面所说的 web.xml。 那它是如何做到 Servlet 加载的呢?主要靠 @ServletComponentScan 注解,即

@ServletComponentScan
@SpringBootApplication
public class Application {
}

Spring 官方文档对这个注解的解释为:

Enables scanning for Servlet components (filters, servlets, and listeners). Scanning is only performed when using an embedded web server.

可以看到,这个注解只有在使用嵌入式 Tomcat 等嵌入式容器时才会生效。 如果部署在独立的 Tomcat 时,他会自动地加载 web.xml,并且也会自动的扫描 @WebServlet 注解。

@ServletComponentScan 底层的实现依赖 org.springframework.boot.web.servlet.ServletComponentRegisteringPostProcessor。 它是一个 BeanFactoryPostProcessor,它会扫描注解指定的包,查找 @WebServlet 等注解标注的类,作用与 @ComponentScan 类似,只不过查找的注解不同。

注:这里你可能注意到,出了 @WebServlet 注解外,它还会扫描 @WebFilter@WebListener。 后面两个注解是干什么的呢? 如果你开发过 Servlet 应用,并且使用过 web.xml 配置它,你应该注意到过配置文件中存在下面这种配置行:

<listener>
    <listener-class>xxx</listener-class>
</listener>

<filter>
    <filter-name>xxx-filter</filter-name>
    <filter-class>xxx</filter-class>
</filter>
<filter-mapping>
    <filter-name>xxx-filter</filter-name>
    <servlet-name>FormServlet</servlet-name>
</filter-mapping>

Filter 和 EventListener 是 Servlet 规范中定义的两类接口。 Filter 会在请求进入 Servlet#serivce 方法之前,对请求、响应进行处理。 EventListener 能够监听 Servlet 容器中的各类事件,并在事件发生时做相应的动作。

出了上述这种方式外,还可以使用 Spring Boot 特有的接口来完成 Servlet 的注册。 方式一,通过 WebApplicationInitializer 接口。 WebApplicationInitializer#onStartup 是该接口定义的唯一一个方法,它会在容器启动时被调用。 具体的调用过程如下:

  1. Servlet 规范的 API 中定义了一个 SPI 接口,javax.servlet.ServletContainerInitializer。 spring-web 实现了这个接口,实现类是 SpringServletContainerInitializer,并且通过注解 @HandlesTypes(WebApplicationInitializer.class) 指明了它能处理的类是 WebApplicationInitializer。
  2. 容器启动时,会通过 SPI 的 loadService 加载到 spring-web 中的实现,并且会将 classpath 所有 WebApplicationInitializer 实现传递给 SpringServletContainerInitializer。
  3. SpringServletContainerInitializer 会逐个调用 WebApplicationInitializer#onStartup 方法
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
    ServletRegistration.Dynamic servlet = servletContext.addServlet("FormServlet", new FormServlet());
    servlet.setLoadOnStartup(1);
    servlet.addMapping("/calculateServlet");
}

不过,需要注意的是,这种方式在使用嵌入式 Tomcat 时,是不生效的,需要独立的 Tomcat 容器。

方式二,通过 ServletRegistrationBean。

@Bean
public ServletRegistrationBean formServlet() {
    ServletRegistrationBean<Servlet> registrationBean = new ServletRegistrationBean<>(new FormServlet(), "/calculateServlet");
    registrationBean.setLoadOnStartup(1);

    return registrationBean;
}

上述方式与前面介绍的达到的效果是一样的。

03-总结

今天,我介绍了如何在 Spring Boot 中开发基于 Servlet 的应用。 主要内容包括两部分,其一,使用独立部署的 Tomcat 容器时,如何配置应用;其二,使用内嵌的容器,例如 Tomcat Embedded 时,如何使用 Spring Boot 提供的接口配置应用。

希望今天的内容能对你有所帮助。