0x10.Servlet和SpringMVC

Based on Java™ Servlet Specification v3.1

[TOC]

Servlet和Servlet容器

Servlet是什么

根据百科的定义,Servlet是用来扩展服务器的功能Java web的服务组件。Java servlet在Java EE中处理或存储符合Java Servlet API的Java类。 Servlet原则上可以通过任何客户端 - 服务器协议进行通信,但它们通常与HTTP一起使用。 因此,“servlet”通常用作“HTTP servlet”的简写。因此,软件开发人员使用servlet将动态内容添加到Java平台的Web服务器上。 生成的内容通常是HTML,但可能是其他数据,如XML,更常见的是JSON。 Servlet可以使用HTTP cookie或URL映射在多个服务器事务中维护会话变量的状态。

其实从本质来看,Servlet只是一个普通的Java类,只是它被用于特定的用途且制定了相应的规范。从web服务的流程看,实际上就是web服务器获取一个socket连接,解析连接来获取请求,然后根据请求和配置的“请求-处理器”规则,找到对应的处理器来处理,处理完成后返回处理结果。Servlet就是这里的处理器,Servlet就是一个Java类或者可以说只是一个方法。抽象来看就是通过网络(socket)进行Java方法的调用,实际上这就是RPC过程。

Servlet容器是什么

Servlet容器简单来说,就是管理Servlet的地方,它知道每个Servlet对应的请求URL(因为请求的形式是通过URL),当一个请求进入容器,容器handle此请求,问清楚它的需求之后,返回静态资源或者是调用Servlet方法(实际上是调用Servlet的service方法)。这就是Servlet容器做的最基础的事情。

规范中的Servlet Container定义:

The servlet container is a part of a Web server or application server that provides the network services over which requests and responses are sent, decodes MIME-based requests, and formats MIME-based responses. A servlet container also contains and manages servlets through their lifecycle.
也就是作为web服务器或应用服务器的一部分提供请求响应的发送,编码请求和格式化响应的网络服务。且容纳和管理servlet。

使用下面伪代码描述Servlet和Servlet的工作原理:

// 容器
public class ServletContainer{
    // URL映射规则
    mappingRules;
    // 容器中的Servlet对象
    servlets;
    
    // 处理请求
    public Response handleRequest(Request req){
        checkRequest();
        if request Static Resource{
            return getStaticResource(req);
        }
        if reqeust a Servlet{
            resolveRequestWithMappingRule(req);
            servlet = getTargetServlet();
            res = servlet.service();
            return res;
        }
        
    }
}

Servlet规范

最基础的web服务原型就是伪代码所述这样,如果自己实现web服务器自己实现请求处理器(类似servlet)起名叫Handler,也使用HTTP协议或者使用其他协议,同样也能表现和Servlet规范下web一样的结果。之所以要设定Servlet规范,是为了在容器和处理器之间能够统一,这样容器和web应用(其实就是Servlet的集合)就能根据一个约定来独立开发,达到通用的目的,这也是规范存在的意义,至于为什么规范要这么设定(比如自定义Servlet要实现Servlet接口,过滤器要实现Filter接口),说到底还是先到达高地的人拥有发言权。

于是统一规范之后,Servlet容器要读取指定的配置文件(包括类)来获取Servlet、获取映射规则、获取过滤器等等监听器来初始化servlet上下文。然后容器接收到请求,根据配置信息拿到Servlet的实例(可能需要创建实例),然后调用接口的方法来执行业务逻辑,最后返回响应,一次请求结束。

Servlet服务组件结构大概像这样:

Servlet是面向请求的或者说面向服务的,其接口方法service就是间接暴露出去的服务。web应用将服务类(servlet)注册到servlet容器中,并配置好请求映射规则,当容器handle 请求时,根据配置信息分发到servlet上,servlet执行服务方法,最后返回结果。这是最基本的servlet服务模型。listener和filter是穿插在服务过程中的插件,如listener监听容器或者servlet或请求的某些事件并作出响应,filter过滤请求和响应,它们为基础的服务模型提供扩展,从而提供完善的服务。

ServletContext

The ServletContext interface defines a servlet’s view of the Web application within
which the servlet is running. The Container Provider is responsible for providing an
implementation of the ServletContext interface in the servlet container. Using the
ServletContext object, a servlet can log events, obtain URL references to resources,
and set and store attributes that other servlets in the context can access.

ServletContext(Servlet 上下文)接口定义了 Web 应用程序正在运行的 servlet 的一个视图。容器供应商负责 􏰁供 Servlet 容器中 ServletContext 接口的实现。Servlet 可以使用 ServletContext 对象记录事件,获取资源的 URL 引用,设置和存储当前上下文中其他 Servlet 可以访问的属性。Context对象相当于一种全局作用域的环境变量,可以提供属性等的公共访问。

部署在容器中的每个web application有一个关联的ServletContext接口实例,如果容器是分布在多个vm上,则web application对应每个vm都有一个ServletContext实例。

在servlet3.0开始,在ServletContext接口中提供了编程方式注册servlet、filter、listener的方法:

    /*
    * @since Servlet 3.0
    */
    // 注册servlet
    ServletRegistration.Dynamic addServlet(String servletName,Class<? extends Servlet> servletClass)
    ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)
    ServletRegistration.Dynamic addServlet(String servletName, String className)
    <T extends Servlet> T createServlet(Class<T> clazz)
    ServletRegistration getServletRegistration(String servletName)
    Map<String,? extends ServletRegistration> getServletRegistrations()
    /**
     * Filter、Listener方法类似
     */

这些方法支持servlet动态注册(对比在web.xml文件中的静态注册),但是只能在初始化时进行注册。在运行时为了安全原因,无法完成注册。在初始化情况下的注册Servlet组件有两种方法:
1.实现ServletContextListener接口,在contextInitialized方法中完成注册(也反过来证明ServletContextListener接口实例在容器启动时回调).
2.在jar文件中放入实现ServletContainerInitializer接口的初始化器(Spring MVC 4的 Java—Based配置方式就是使用的此方式取代web.xml,以插件的方式来初始化web配置)

ServletContainerInitializer 也是 Servlet 3.0 新增的一个接口,容器在启动时使用 JAR 服务 API(JAR Service API) 来发现 ServletContainerInitializer 的实现类,并且容器将 WEB-INF/lib 目录下 JAR 包中的类都交给该类的 onStartup() 方法处理。
后面涉及web启动入口再详细描述。

Servlet

The Servlet interface is the central abstraction of the Java Servlet API.

接口定义:

public interface Servlet {
  // 初始化
  public void init(ServletConfig config) throws ServletException;
  public ServletConfig getServletConfig();
  // 核心服务方法
  public void service(ServletRequest req, ServletResponse res)
    throws ServletException, IOException;
  public String getServletInfo();
  // 销毁方法
  public void destroy();
}

定义一个Servlet要么直接实现Servlet接口,要么继承Servlet接口的实现类,有两个常用的实现类:GenericServlet和HttpServlet。一般都是继承HttpServlet来开发servlet,HttpServlet是GenericServlet的子类,提供了基于http协议的service细分方式,如doGet处理GET请求,doPost处理POST请求等。

一般而言,在请求到达servlet时会创建servlet实例,并调用其init方法来初始化,随后执行service方法执行业务逻辑,当然也可以预初始化在容器中。

service()方法处理Request请求,生成Response响应。

生命周期

Servlet接口API中已经表达了Servlet的生命周期定义:init(),service(),destory();

加载和实例化

servlet容器负责加载和实例化servlet实例,可以在容器启动时加载或者延迟到请求到达时加载;

初始化

容器创建servlet实例之后,必须在servlet接收请求之前完成初始化过程。初始化过程使用ServletConfig的实例参数调用init方法初始化servlet实例。ServletConfig对象允许servlet接收webadpp的配置信息中的name-value形式的初始化参数,以及ServletContext对象。可以通过查看ServletConfig接口了解更多信息:

public interface ServletConfig {
    public String getServletName();
    public ServletContext getServletContext();
    public String getInitParameter(String name);
    public Enumeration<String> getInitParameterNames();
}
  • 初始化出错的处理:

During initialization, the servlet instance can throw an UnavailableException or a
ServletException. In this case, the servlet must not be placed into active service
and must be released by the servlet container. The destroy method is not called as it
is considered unsuccessful initialization.
A new instance may be instantiated and initialized by the container after a failed
initialization. The exception to this rule is when an UnavailableException indicates
a minimum time of unavailability, and the container must wait for the period to pass
before creating and initializing a new servlet instance.

Request Handling

初始化完成之后,有个Servlet对象就可以处理请求了。即service()方法的执行,客户端请求被抽象为ServletRequest实例,响应对象抽象为ServletResponse,在http场景下,对应为HttpServletRequest和HttpServletResponse.

  • 多线程问题

servlet在容器中是单例的,并发的请求会作用在一个实例上,所以开发者需要注意并发问题。有一个过时的同步方式,使servlet实现SingleThreadModel接口,所有在servlet上的请求被串行,这是不推荐的,在3.0版本已经被废除。

  • 异步处理支持

在servlet3.0之前,Servlet的service方法处理是同步的。使用 Servlet 3.0 的异步处理支持,之前的 Servlet 处理流程可以调整为如下的过程:首先,Servlet 接收到请求之后,可能首先需要对请求携带的数据进行一些预处理;接着,Servlet 线程将请求转交给一个异步线程来执行业务处理,线程本身返回至容器,此时 Servlet 还没有生成响应数据,异步线程处理完业务以后,可以直接生成响应数据(异步线程拥有 ServletRequest 和 ServletResponse 对象的引用),或者将请求继续转发给其它 Servlet。如此一来, Servlet 线程不再是一直处于阻塞状态以等待业务逻辑的处理,而是启动异步线程之后可以立即返回。

异步处理特性可以应用于 Servlet 和过滤器两种组件,由于异步处理的工作模式和普通工作模式在实现上有着本质的区别,因此默认情况下,Servlet 和过滤器并没有开启异步处理特性,如果希望使用该特性,则必须按照如下的方式启用:

<servlet> 
    <servlet-name>TestServlet</servlet-name> 
    <servlet-class>test.servlet.TestServlet</servlet-class> 
    <async-supported>true</async-supported> <!-- 这个标签开启异步处理,filter同理 -->
</servlet>
@WebFilter(urlPatterns = "/demo",asyncSupported = true) 
public class DemoFilter implements Filter{...}

除此之外,Servlet 3.0 还为异步处理提供了一个监听器,使用 AsyncListener 接口表示。它可以监控如下四种事件:

1.异步线程开始时,调用 AsyncListener 的 onStartAsync(AsyncEvent event) 方法;

  1. 异步线程出错时,调用 AsyncListener 的 onError(AsyncEvent event) 方法;
  2. 异步线程执行超时,则调用 AsyncListener 的 onTimeout(AsyncEvent event) 方法;
  3. 异步执行完毕时,调用 AsyncListener 的 onComplete(AsyncEvent event) 方法;

异步处理示例可参考 参考资料2

服务结束

servlet规范没有要求servlet容器需要保持servlet active的具体时间,可能是多少毫秒可能是容器的生命周期,或者是二者之间。一般在容器关闭时销毁servlet实例并释放资源,在中途也可以在该servlet上的请求线程执行完成之后销毁servlet,此后不再处理对应的请求,销毁之后不能再重新创建,因为注册servlet只发生在容器启动阶段。

Filter

Filters are Java components that allow on the fly transformations of payload and header information in both the request into a resource and the response from a resource.

Filter做的事情是on the fly transformations in request 和response。即请求在到达servlet之前会经过过滤器,响应从servlet返回处理也会经过过滤器,就像包裹在servlet外面的一张网,进出的对象都有经过它们的查看,甚至能修改。

Filters do not generally create a response or respond to a request as servlets do, rather they modify or adapt the requests for a resource, and modify or adapt responses from a resource.

一般使用过滤器来进行访问控制、记录日志、统计、或者对数据进行编码解码等。
常见的过滤器按照作用分类有:

  • Authentication filters
  • Logging and auditing filters
  • Image conversion filters
  • Data compression filters
  • Encryption filters
  • Tokenizing filters
  • Filters that trigger resource access events
  • XSL/T filters that transform XML content
  • MIME-type chain filters
  • Caching filters

接口定义:

public interface Filter {
    public void init(FilterConfig filterConfig) throws ServletException;
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain)
            throws IOException, ServletException;
    public void destroy();
}

Filter接口定义以及FilterConfig和Servlet很相似。

过滤器的调用是贯穿对应Servlet或者URLpattern的,其作用顺序如下描述:设有两个过滤器fafb,都对应过滤testServlet上的请求,则一个执行时序为:request->fa->fb->testServlet.service()->fb->fa->client.

filter的执行顺序由filter链控制,是责任链的模式的使用.请求通过顺序的filter,响应返回时通过逆序的filter.
如下所示:


filter.png

生命周期

在web应用部署之后请求到达之前,容器必须定位到filter列表并实例化列表中的每个过滤器,然后调用init方法进行过滤器的初始化工作。多个过滤器组成一个过滤器链,请求进来时(过滤器也需要指定mapping),会从第一个filter开始执行doFilter()方法执行过滤逻辑(责任链模式),在doFilter中使用chain.doFilter(request, response);将请求传递下去,直到所有相关的过滤器执行完成才会到达servlet。在doFilter方法内可以处理请求和响应以及拦截请求的传送写结束服务或者修改返回给客户端的response。

如果在filter工作过程中抛出了一次UnavailableException,容器不可以尝试继续处理。Tomcat的实现是直接报错,客户端看到404。

RequestDispatcher

RequestDispatcher是容器的组件,负责请求的分发,servlet提供的接口是RequestDispatcher,tomcat8的ApplicationDispatcher实现了此接口完成分发请求的工作。

dispatcher设置Filter处理请求分发的类型。在枚举类中DispatcherType定义,共有五类,是Filter的一个配置项,默认是REQUEST。

  1. REQUEST:直接来自客户端的请求,默认值。
  2. FORWARD:请求是在一个请求分配器下调用forward()处理的,这个请求表示的Web组件与过滤器的url-patternor或servlet-name匹配。
  3. INCLUDE:请求是在一个请求分配器下调用include()处理的,这个请求表示的Web组件与过滤器的url-patternor或servlet-name匹配。
  4. ERROR: 匹配url-pattern的请求,这个请求被error page机制处理.
  5. ASYNC: 请求由异步上下文分发机制处理,调用了dispatch方法。
  6. Or any combination of 1, 2, 3, 4 or 5 above

Listener

应用事件监听器是实现了一个或多个 Servlet 事件监听器接口的类。它们在部署 Web 应用时,实例化并注册到 Web 容器中。
Servlet 事件监听器支持在 ServletContext、HttpSession 和 ServletRequest 状态改变时进行事件通知。
事件类型和监听器接口用于监控下表所示的事件:

  • Servlet上下文事件
事件类型 描述 监听器接口
生命周期 Servlet 上下文刚刚创建 并可用于服务它的第一个请求,或者 Servlet 上下文即将关闭。 javax.servlet. ServletContextListener
更改属性 在 Servlet 上下文的属性已添加、删除、或替换。 javax.servlet. ServletContextAttributeListener

每一个部署到容器的 Web 应用都有一个 ServletContext 接口的实例对象与之关联。在容器分布在多台虚拟
机的情况下,每个 JVM 的每个 Web 应用将有一个 ServletContext 实例。
ServletContext是web应用的一个全局对象,hold整个web
app生命周期的环境,提供公共属性的全局访问。

  • HTTP 会话事件
事件类型 描述 监听器接口
生命周期 会话已创建、无效或超时。 javax.servlet.http.HttpSessionListener
更改属性 已经在 HttpSession 上添加、移除、或替换属性 javax.servlet.http.HttpSessionAttributeListener
会话迁移 HTTPSession已被激活或钝化 javax.servlet.http. HttpSessionActivationListener
对象绑定 对象已从HTTPSession绑定或解绑 javax.servlet.http. HttpSessionBindingListener
  • Servlet请求事件
事件类型 描述 监听器接口
生命周期 一个 servlet 请求已经开始由 Web 组件处理 javax.servlet. ServletRequestListener
更改属性 已经在 ServletRequest 上添加、移除、或替换属 性。 javax.servlet. ServletRequestAttributeListener
异步事件 超时、连接终止或完成异步处理 javax.servlet.AsyncListener

容器需要在开始执行进入应用的第一个请求之前完成 Web 应用中的监听器类的实例化。容器必须保持每一
个监听器的引用直到为 Web 应用最后一个请求提供完服务。
ServletContext 和 HttpSession 对象的属性改变可能会同时发生。不要求容器同步到属性监听器类产生的通
知。维护状态的监听器类负责数据的完整性且应明确处理这种情况。

Request

Request是客户端请求的抽象,一般使用的是HttpServletRequest,其中涉及Session、cookie等机制,和很多http协议的细节。暂略。

Response

Request是服务器响应的抽象,一般使用的是HttpServletRepsonse。暂略。

Deployment Descriptor

Deployment Descriptor即部署描述符(部署描述者),是web应用的部署描述,也就是web应用的配置所在,要部署一个web应用到web容器中,需要按照规范提供部署配置信息,这样才能让web容器能够解析web应用。显然这个配置信息中需要包含监听器、过滤器和Servlet(包括映射规则)。这是整个web应用的入口,是web容器和webapp协作的第一步。

在Servlet3.0之前,web.xml作为web应用部署描述符表现的唯一方式,web容器通过解析web.xml文件来理解web应用的部署要求。在Servlet3.0,增加了注解方式和框架通过jar包来作为插件的方式,这两种插件特性的方式可取代web.xml,也就是使用这两种方式的任意一种都可以在web app中不使用web.xml配置文件。另外还有一种可插拔的配置方式,是web-fragment.xml的配置方式,和基于jar的编程方式类似,需要绑定到jar包中然后放到web app的指定目录生效.

所有servlet容器的Web应用程序部署描述符需要支持以下类型的配置和部署信息:

  • ServletContext 初始化参数
  • Session 配置
  • Servlet 声明
  • Servlet 映射
  • 应用程序生命周期监听器类
  • 过滤器定义和过滤器映射
  • MIME 类型映射
  • 欢迎文件列表
  • 错误页面
  • 语言环境和编码映射
  • 安全配置,包括 login-config,security-constraint,security-constraint,security-role-ref 和 run-as

Web应用

Web应用启动

Servlet规范中说明了部署一个web应用,有以下要求。
当一个web app部署到一个容器中,在web app开始处理客户端请求之前,以下步骤必须按序执行:

  • 在部署描述符中使用<listener>元素定义的事件监听器,为每个监听是实例化一个实例
  • 调用前面实例化的监听器的contextInitialized()方法
  • 实例化<filter>元素定义的过滤器实例并调用每个过滤器实例的init()方法
  • 按照定义servlet时设置的<load-on-startup>顺序实例化servlet,调用每个Servlet实例的init()方法。

也就是在web app做好处理客户端请求之前,需要初始化好监听器、过滤器和servlet。

web.xml和Web-fragment.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" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 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">
  <filter>
    <filter-name>jfinal</filter-name>
    <filter-class>com.jfinal.core.JFinalFilter</filter-class>
    <init-param>
      <param-name>configClass</param-name>
      <param-value>com.demo.common.DemoConfig</param-value>
    </init-param>
  </filter>
  
  <filter-mapping>
    <filter-name>jfinal</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>

这是使用Jfinal框架的web.xml配置,仅配置了一个过滤器和对应的映射规则。

注解配置

在 web 应用中,使用注解的类仅当它们位于 WEB-INF/classes 目录中,或它们被打包到位于应用的
WEB-INF/lib 中的 jar 文件中时它们的注解才将被处理。

Web 应用部署􏰀述符的 web-app 元素包含一个新的“metadata-complete”属性。“metadata-complete”属性 定义了 web 􏰀述符是否是完整的,或是否应该在部署时检查 jar 包中的类文件和 web fragments。如果 “metadata-complete”设置为“true”,部署工具必须忽略存在于应用的类文件中的所有 servlet 注解和 web fragments。如果 metadata-complete 属性没有指定或设置为“false”,部署工具必须检查应用的类文件的注解, 并扫􏰀 web fragments。
兼容 Servlet3.0 的 web 容器必须支持下面的注解。

使用下面定义的注解,使得使用 web.xml 可选。然而,对于覆盖默认值或使用注解设置的值,需要使用部 署􏰀述符。如前所述,如果 web.xml 􏰀述符中的 metadata-complete 元素设置为 true,将不会处理在 class 文件和绑定在 jar 包中的 web-fragments 中的注解。这意味着,应用的所有元数据通过 web.xml 描述符指定。另外,当使用注解定义 Listener、Servlet 和 Filter,它们调用的顺序是未指定的。如果有关的 Listener、Servlet 和 Filter 的顺序必须指定,那么必须指定在 web-fragment.xml 或 web.xml。

@WebServlet

用于在 Web 应用中定义 Servlet 组件,@WebServlet 注解的类必须继承 javax.servlet.http.HttpServlet 类。必须指定注解的 urlPatterns 或 value 属性,但是两个属性不能同时出现。
在注解中没有指定Servlet名字就默认是有类名作为Servlet名.
前文所述如果在部署描述中定义了Servlet,就会实例化一个对象,也只会实例化一个对象.

  • 同一个Servlet注册多次
    If the same servlet class is declared in the deployment descriptor under a different name, a new instance of the servlet MUST be instantiated. If the same servlet class is added with a different name to the ServletContext via the programmatic API ,the attribute values declared via the @WebServlet annotation MUST be ignored and a new instance of the servlet with the name specified MUST be created.

  • 示例

@WebServlet(”/foo”) // 相当于value="/foo"或urlPatterns="/foo"
public class CalculatorServlet extends HttpServlet{
//...    
}
@WebServlet(name=”MyServlet”, urlPatterns={"/foo", "/bar"}) 
public class SampleUsingAnnotationAttributes extends HttpServlet{
// ...
}

@WebFilter

用于在 Web 应用中定义 Filter,如果没 有指定 Filter 名字则默认是全限定类名。注解的urlPatterns属性, servletNames属性或value 属性必须被指定。在同一注解上同时使用 value 和 urlPatterns 属性 是非法的。

  • 示例
@WebFilter(“/foo”)
public class MyFilter implements Filter {
  public void doFilter(HttpServletRequest req, HttpServletResponse res) { ...
  } 
}

@WebInitParam

该注解用于指定必须传递到 Servlet 或 Filter 的任何初始化参数。它是 WebServlet 和 WebFilter 注解的一个属 性。

@WebListener

WebListener 注解用于注解获得特定 web 应用上下文中的各种操作事件的监听器。
@WebListener 注解的类 必须实现以下接口:

  • javax.servlet.ServletContextListener

  • javax.servlet.ServletContextAttributeListener

  • javax.servlet.ServletRequestListener

  • javax.servlet.ServletRequestAttributeListener

  • javax.servlet.http.HttpSessionListener

  • javax.servlet.http.HttpSessionAttributeListener

  • 示例

@WebListener
public class MyListener implements ServletContextListener{
  public void contextInitialized(ServletContextEvent sce) {
    ServletContext sc = sce.getServletContext();
    sc.addServlet("myServlet", "Sample servlet", "foo.bar.MyServlet", null, -1); 
    sc.addServletMapping("myServlet", new String[] { "/urlpattern/*" });
  } 
}

@MultipartConfig

当指定在 Servlet 上时,表示请求期望是 mime/multipart 类型。相应 servlet 的 HttpServletRequest 对象必须使用 getParts 和 getPart 方法遍历各个 mime 附件以获取 mime 附件。 javax.servlet.annotation.MultipartConfig 的 location 属性和<multipart-config><location>元素被解析为一个绝对路径且默认为 javax.servlet.context.tempdir。如果指定了相对地址,它将是相对于 tempdir 位置。绝对路径 与相对地址的测试必须使用 java.io.File.isAbsolute。

其他注解

使用注解的示例

不使用web.xml配置文件,注意只使用注解的情况下,filter是没有固定顺序的。

  • Servlet定义
/**
 * Servlet implementation class TestServlet
 */
@WebServlet(value = "/TestServlet", initParams = { @WebInitParam(name = "name", value = "test"),
    @WebInitParam(name = "pass", value = "123456") })
public class TestServlet extends HttpServlet {
  private static final long serialVersionUID = 1L;
  // 使用commons-logging && log4j
  private final Log logger = LogFactory.getLog(TestServlet.class);

  @Override
  public void init(ServletConfig config) throws ServletException {
    super.init(config);
    Enumeration<String> params = config.getInitParameterNames();
    while (params.hasMoreElements()) {
      String name = params.nextElement();
      logger.debug("name:" + name + ",value:" + config.getInitParameter(name));
    }
  }
 
  public TestServlet() {
    super();
  }
 
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    logger.debug("doGet invoke");
    response.getWriter().append("Served at: ").append(request.getContextPath());
  }
 
  protected void doPost(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    logger.debug("doPost invoke");
    doGet(request, response);
  }

}
  • Filter定义
@WebFilter(value = "/*")
public class TestFilter implements Filter {
  private final Log logger = LogFactory.getLog(TestFilter.class);

  public TestFilter() {
  }

  public void destroy() {
    logger.debug("TestFilter destroy invoke");
  }

  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
    logger.debug("doFilter invoke");
    // 修改响应内容
    response.getWriter().println("return by filter");
    // throw new UnavailableException("test throw");
    // 注掉可以阻止请求传递,直接返回响应,
    chain.doFilter(request, response); // chain的调用直到Servlet执行结束才从链中返回,返回时已经是响应了
    // 响应回传的过滤逻辑
    // ...
  }

  public void init(FilterConfig fConfig) throws ServletException {
  }

}
  • Listener定义
/**
 * Application Lifecycle Listener implementation class
 * TestServletRequestListener
 * 
 * @see ServletRequestListener实现类,当一个 servlet 请求已经开始由 Web
 *      组件处理时回调
 */
@WebListener
public class TestServletRequestListener implements ServletRequestListener {
  private final Log logger = LogFactory.getLog(TestServletRequestListener.class);

  public TestServletRequestListener() {
  }

  public void requestDestroyed(ServletRequestEvent sre) {

  }

  public void requestInitialized(ServletRequestEvent sre) {
    ServletRequest req = sre.getServletRequest();
    DispatcherType dispatcherType = req.getDispatcherType();
    boolean isSecure = req.isSecure();
    String contextPath = req.getServletContext().getContextPath();
    logger.debug("DispatcherType:" + dispatcherType);
    logger.debug("isSecure:" + isSecure);
    logger.debug("contextPath:" + contextPath);
  }

}
/**
 * Application Lifecycle Listener implementation class
 * TestServletContextListener
 * ServletContext监视器,监听servlet上下文创建和销毁事件
 */
@WebListener
public class TestServletContextListener implements ServletContextListener {
  private final Log logger = LogFactory.getLog(TestServletContextListener.class);

  public TestServletContextListener() {
  }

  public void contextDestroyed(ServletContextEvent sce) {
  }

  /*
   * 初始化ServletContext时回调
   */
  public void contextInitialized(ServletContextEvent sce) {
    logger.debug("contextInitialized");
    logger.debug("可以在这里动态注册servlet和filter和Listener");
    ServletContext servletContext = sce.getServletContext();
    String filterName = "dynamicFilter";
    Dynamic filter = servletContext.addFilter(filterName, new Filter() {

      @Override
      public void init(FilterConfig filterConfig) throws ServletException {
      }

      @Override
      public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
          throws IOException, ServletException {
        logger.debug("来自匿名动态filter的doFilter的log");
        chain.doFilter(request, response);
      }

      @Override
      public void destroy() {
      }
    });
    filter.addMappingForUrlPatterns(null, false, "/*");
    String servletName = "dynamicServlet";
    // 注册Servlet
    javax.servlet.ServletRegistration.Dynamic servlet = servletContext.addServlet(servletName,
        new DynamicServlet());
    servlet.addMapping("/DynamicServlet");
  }

  static class DynamicServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
      resp.getWriter().println("response from DynamicServlet");
    }

  }
}

这是一个很重要的Listener,上下文初始化时回调,这是在容器启动创建ServletContext时触发的,相当于web应用的启动入口函数,在这个时期可以为ServletContext注册Servlet、filter或者Listener,这是Servlet3.0的动态注册特性,但是仅仅在上下文容器完成初始化之前动态添加才有效另外一种方式是在下面“基于Jar的编程方式”(另外前面ServletContext的介绍处也有说明),在ServletContext初始化之后再执行这些动态注册都将无效。

回忆使用web.xml整合Spring MVC时,需要在web.xml中定义ContextLoaderListener
public class ContextLoaderListener extends ContextLoader implements ServletContextListener{},就是它了。

基于Jar的编程方式

除了支持fragments和使用注解,另一个需求是我们不仅可以将那些绑定在WEB-INF/lib中的事物作为插件插入,而且可以插入框架的共享副本,包括能够插入类似JAX-WS,JAX-RS和JSF这些构建在web容器顶层的组件到web容器中。
ServletContainerInitializer正是满足此需求的产物。

JAX-WS,JAX-RS和JSF分别是JavaTM API forXML-Based Web Services ,JavaTM API forRESTful Web Services , JavaServer Faces。
(JSF) 是一种用于构建Java Web 应用程序的标准框架(是Java Community Process 规定的JSR-127标准)

ServletContainerInitializer类是通过jar service API来查找的。对于每一个application,在应用启动时一个ServletContainerInitializer实例被容器创建。

jar文件中的servlet组件注册,需要在jar文件中包含META-INF/services/javax.servlet.ServletContainerInitializer文件,文件内容为已经实现ServletContainerInitializer接口的类,其中@HandlesTypes注解表示CustomServletContainerInitializer 可以处理的类,在onStartup 方法中,可以通过Set<Class<?>> c 获取得到。

jar文件中不但可以包含需要自定义注册的servlet,也可以包含应用注解的servlet,具体怎么做,视具体环境而定。
把处理某类事物的servlet组件打包成jar文件,有利于部署和传输,功能不要了,直接去掉jar即可。

框架jar文件也可以绑定到war文件的WEB_INF/lib中,也可以被自动发现, If the ServletContainerInitializer is bundled in a JAR file inside the WEB-INF/lib directory of an application, it’s onStartup method will be invoked only once during the startup of the bundling application.

除ServletContainerInitializer 外,我们还有一个注解—HandlesTypes。在 ServletContainerInitializer 实现上的 HandlesTypes 注解用于表示感兴趣的一些类,这些类可能在 HandlesTypes 的 value 中指定了注解(类型、 方法或字段级别的注解),或者是这些类继承/实现了该类的超类中的某个类。容器使用@HandlesTypes 注解 来决定什么时候调用 initializer 的 onStartup 方法。当检测一个应用的类看它们是否匹配 ServletContainerInitializer 的HandlesTypes 注解指定的所有条件时,如果应用的一个或多个可选的 JAR 包缺 失,容器可能遇到类装载问题。由于容器不能决定这些类装载失败的类型是否会阻止应用正常工作,它必 须忽略它们,同时也􏰁供一个记录这些问题的配置选项。

如果 ServletContainerInitializer 实现没有@HandlesTypes 注解,或如果没有匹配任何指定的 HandlesType,那 么它会为每个应用使用 null 作为 Set 的值调用一次。这将允许 initializer 来决定基于应用中可用的资源是否 需要初始化一个 Servlet/Filter。

当应用正在启动时,在任何 Listener 的事件被触发之前,ServletContainerInitializer 的 onStartup 方法将被调用。
ServletContainerInitializer的onStartup方法调用是伴随一组类的,这些类要么扩展/实现了初始化器表示感兴趣的类,要么是用@HandlesTypes注释指定的任何类对其进行注释。

框架jar文件也可以捆绑在war文件的WEB-INF/lib目录中。如果ServletContainerInitializer绑定在应用程序的WEB-INF/lib目录中的JAR文件中,那么它的onStartup方法在绑定应用程序启动期间只调用一次。另一方面,如果ServletContainerInitializer绑定在WEB- INF/lib目录之外的JAR文件中,但是仍然可以被运行时的服务提供者查找机制发现,那么每次启动应用程序时都会调用它的onStartup方法。

使用此方式作为web入口配置的具体方式是:

  1. 创建ServletContainerInitializer接口的实现类,如SpringServletContainerInitializer;
  2. 在jar文件内部的META-INFO/services目录下创建一个文件,名为:javax.servlet.ServletContainerInitializer,文件内容为步骤1中创建的实现类,如Spring mvc中的就是:org.springframework.web.SpringServletContainerInitializer

Spring MVC的ServletContainerInitializer实现类SpringServletContainerInitializer:
@Since 3.1

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
  @Override
  public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
      throws ServletException {
    // ...
  }

}

@HandlesTypes注解的作用是声明传递给javax.servlet.ServletContainerInitializer接口的class数组的元素类型,就是onStartup()方法的第一个参数的类型。在Spring mvc的实现中,容器负责把ServletContainerInitializer感兴趣的类WebApplicationInitializer注入到onStartup()方法的第一个参数中,然后执行WebApplicationInitializer实现类的onStratup方法来完成Spring MVC的启动.

此方式实际上是基于ServletContext 编程方式注册组件的方法的方式,和ServletContextListener中编程注册组件的方式相似,不过此方式的触发要早于ServletContextListener监视器的contextInitialized()方法的.

Jar示例

  • 创建project
  • 创建类:
package com.mine;

import java.io.IOException;
import java.util.Set;

import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class MyServletContainerInitializer implements ServletContainerInitializer {
  Log logger = LogFactory.getLog(MyServletContainerInitializer.class);

  @Override
  public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
    logger.debug("start up");
    logger.debug("注册组件start");
    Dynamic servlet = ctx.addServlet("jarServlte", new HttpServlet() {
      /**
       * 
       */
      private static final long serialVersionUID = 1L;

      @Override
      protected void doGet(HttpServletRequest req, HttpServletResponse resp)
          throws ServletException, IOException {
        resp.getWriter().println("return from jarServlet");
      }

    });
    servlet.addMapping("/jar");
  }

}
  • build成class文件
  • 进入class文件根目录,如bin
  • 执行命令,在jar中添加./META-INFO/services/javax.servlet.ServletContainerInitializer文件
jar cvf mine.jar com
jar xvf mine.jar /tmp/mine
cd /tmp/mine
cd META-INFO/
mkdir services
touch javax.servlet.ServletContainerInitializer
echo com.mine.MyServletContainerInitializer > javax.servlet.ServletContainerInitializer
cat javax.servlet.ServletContainerInitializer
cd ../
jar cvf mine.jar * 
  • jar包可以使用了

jar导入到WEB-INFO/lib下,在应用启动的时候会执行我们自定义的onStratup()方法.

混合配置以及配置的有序性

若需要配置的组件是需要有序执行注册以及filter是有序的,需要使用web.xml或web-fragment.xml的部署描述符的方式,或者使用编程方式来自主控制注册流程(因为代码逻辑可以自行控制).

web.xml中的<web-app>内的属性metadata-complete如果设置为true,则注解配置的所有Servlet,Filter,Listener都失效.如果需要使用注解,要确保此属性为false,默认为false.

使用ServletContainerInitializer方式不会受限于web.xml的meta-comolete属性,二者可以混合使用.且ServletContainerInitializer的实现类的onStratup()方法调用实在web.xml的ServletContextListenercontextInitialized()方法触发之前执行的.

重复注册

如果指定了两个相同的URLpattern,会抛出java.lang.IllegalArgumentException: The servlets named [TestServlet2] and [main.TestServlet] are both mapped to the url-pattern [/TestServlet] which is not permitted.如果两个不同的URL指向同一个Servlet,是允许的,需要注意的是,此情况下会生成该Servlet类的两个实例,对应各自的URL.

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,192评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,186评论 1 303
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,844评论 0 252
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,471评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,876评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,891评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,068评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,791评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,539评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,772评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,250评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,577评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,244评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,146评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,949评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,995评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,812评论 2 276

推荐阅读更多精彩内容

  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong阅读 21,812评论 1 92
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,395评论 6 343
  • 本章聊一聊ServletContext 3.0规范中定义的注解以及在web应用中使用的框架和库的可插拔性的提升。 ...
    Lucky_Micky阅读 5,895评论 0 3
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,106评论 18 139
  • 是自己太过自私还是太过贪婪。总想把喜欢的一直保存,保存到永远最好,买喜欢的东西就在想以后能不能让它保存下去,能不能...
    毁笑则以阅读 346评论 0 1