UP | HOME

Java Servlet 3.1 规范笔记

目录

概述

Servlet 是什么

Servlet 是一个 Java 基于Web技术的组件,通过容器来管理,用于产生动态内容。该容器,有时也称为 Servlet 引擎。它与Web客户端通过一个由容器实现的 请求/响应 范例来进行交互。

Servlet 容器是什么

Servlet 容器是 Web 服务器或应用程序服务器的一部分,用来提供发送 请求和响应 的网络服务,解码基于 MIME 的请求,并格式化基于 MIMIE 的响应。它包含和管理 servlets 的生命周期。

Servlet 接口

处理的方法

service 用来处理客户端请求

HTTP 特定的请求

  • doGet
  • doPost
  • doPut
  • doDelete
  • doHead
  • doOptions
  • doTrace

它们是在 service 方法里,根据HTTP请求的方法再调用相应的代码的。

实例数量

在一个非分布式环境中(默认),servlet 容器必须为每个 servlet 声明仅使用一个实例。然而,如果一个 servlet 实现了 SingleThreadModel 接口,则 servlet 容器可能会实例化多个实例来处理一个非常重的请求,加载和串行化这些请求到一个特定的实例。

在一个分布式环境中,每个JVM中的每个 servlet 容器可能仅会有一个 servlet 实例。如果一个 servlet 在一个分布式环境中并实现了 SingleThreadModel 接口,则在每个JVM中的 servlet 容器可能会实例化多个 servlet 实例。

SingleThreadModel 在该版本中已经被弃用

没有实现 SingleThreadModel

ab -n 100 -c 100 http://localhost:8008/hello

生成的 instance 是 1:

实现了 SingleThreadModel

ab -n 100 -c 100 http://localhost:8008/hello

生成的 instance 是100,这个数是并发数是多少,就是多少,即上面 ab 测试中的 -c 参数相同。

Servlet 生命周期

它在 javax.servlet.Servlet 接口中定义以下的生命周期方法

  1. init
  2. service
  3. destroy

加载和实例化

它是由 servlet 容器来负责的。它可以在 servlet 容器启动时,或延迟到需要为一个请求进行服务时才进行加载和实例化。

初始化

实例化之后,容器必须在为客户端处理请求之前初始化 servlet 。它是通过一个配置的对象 ServletConfig 来保存初始化配置,初始化是调用 init 方法来进行初始化的。

实例化出错

这时 servlet 是不能提供服务的并且必须被 servlet 容器释放。 destroy 方法也不会调用,因为它没有成功地进行初始化过。 开发者不应该假设一个 servlet 在一个容器的 runtime 中是激活的,直到调用了 init 方法之后才可以。

处理请求

注意,一个 servlet 实例在它的生命周期内,是可以不进行处理请求的。

多线程问题

尽管不建议,一个可行的方法是实现 SingleThreadModel 接口来让容器保证在一个 service 方法中仅有一条线程执行。它可以通过将请求进行串行化处理,或者维护一个 servlet 实例池来实现。

如果没有实现 SingleThreadModel 接口,并且 service 方法是使用 synchronized 来定义的话,则 servlet 容器并不能使用实例池的方式,而必须串行化请求。强烈建议开发者不要在 service 方法上使用 synchronized ,它会导致性能问题。

请求时出现异常

ServletException 表示正在处理请求时出现错误,容器这时应该适当清理这些请求。 UnavailableException 表示该 servlet 临时或永久不可用了。(永久不可用应返回 404, 临时不可用则返回 503)

异步处理
  1. 接收到请求并通过一般的 filters 再到达 servlet
  2. servlet 处理请求的参数或内容来决定请求的本质
  3. servlet 发出数据或资源的请求,例如访问远程的 web 服务或 JDBC 链接
  4. servlet 返回一个没有内容的 response
  5. 之后,当请求的资源可用时(第3步),处理该事件的线程继续在同一线程中继续处理或者通过 AsyncContext 来分发到容器的另一个资源中处理。

@WebServlet@WebFilter 都有一个 asyncSupported 的布尔属性,默认为 false 。当为 true 的时候, 可以通过调用 startAsync 开启一个异步处理。响应会直到在 AsyncContext 中调用 complete 才会真正完成。从一个 asyncSupported=true 的 servlet 转发到一个 asyncSupported=false 是允许的。这种情况下,在 asyncSupported=false 的 servlet 退出 service 方法时,就会完成响应了,并且由容器来负责调用 AsyncContextcomplete ,以便让 AsyncListener 收到通知。

从一个 synchronous 的 servlet 转发到一个 asynchronous 的 servlet 是非法的。

线程安全

除了 startAsynccomplete 之外的方法, requestresponse 实现的对象并不保证是线程安全的。

结束服务

并不要求 servlet 容器一直保持 servlet 到特定时期的时间。当 servlet 容器决定一个 servlet 应该中服务移除时,它会调用 destroy 方法。在调用 destroy 方法之前,它必须允许任意正在执行 service 的线程执行完毕或者超出了 server 定义的时间限制。调用完 destroy 方法后,servlet 容器就会释放该 servlet 实例,以便进行 GC

请求(request)对象

HTTP 协议参数

从客户端发送的参数,都是 strings

  • getParameter
  • getParameterNames
  • getParameterValues
  • getParameterMap

getParameter 的值必须是 getParameterValues 返回的 string 数组对象中的第一个值。即:

getParameter("hello") 等同于 getParameterValues("hello")[0]

从 query string 以及Post body 中的数据,都会统一放到 parameter 集中。query string 会在 post body 的数据之前设置。即,如果一个请求中, query string 为 a=hello,并且 post body 为 a=goodbye&a=world ,则参数 a 的结果为 (hello, goodbye, world) 。

Path 的参数是 GET 请求的一部分,并不会放到 parameters 中。Path 参数必须从 getRequestURI 方法或 getPathInfo 方法来获取。

Parameters 可用的条件

Post表单时要满足以下条件时 parameter 集合才可用

  1. 请求是 http 或 https
  2. 请求的方法是 POST
  3. content type 为: application/x-www-form-urlencoded
  4. servlet 已经在 request 对象上调用了相关的 getParameter 方法。

当以上条件不满足时,POST 表单的数据并不会设置到 parameter 集合中,但依然可以通过 request 对象的 inputstream 来获取。 当以上条件满足时,POST 表单的数据在 request 对象的 inputstream 将不再可用了。

下面是具体例子

方法为GET时
  • 没有 query string 时
    • 第一次调用 getParameter 时

      示例的Java代码:

      System.out.println("1." + req.getParameterNames().hasMoreElements());
      System.out.println("2." + convertStreamToString(req.getInputStream()));
      System.out.println("3." + req.getParameterNames().hasMoreElements());
      System.out.println("4." + convertStreamToString(req.getInputStream()));
      
      [22:50:28] emacsist:~ $ curl -X GET  -d "hello=world&hello2=world2" http://localhost:8008/parameters -v
      *   Trying ::1...
      * TCP_NODELAY set
      * Connected to localhost (::1) port 8008 (#0)
      > GET /parameters HTTP/1.1
      > Host: localhost:8008
      > User-Agent: curl/7.51.0
      > Accept: */*
      > Content-Length: 25
      > Content-Type: application/x-www-form-urlencoded
      >
      * upload completely sent off: 25 out of 25 bytes
      < HTTP/1.1 200 OK
      < Date: Mon, 20 Mar 2017 14:54:08 GMT
      < Content-Length: 0
      < Server: Jetty(9.3.7.v20160115)
      <
      * Curl_http_done: called premature == 0
      * Connection #0 to host localhost left intact
      

      它输出:

      1.false
      2.hello=world&hello2=world2
      3.false
      4.
      
    • 第一次调用 getInputStream 时

      示例的Java代码:

      System.out.println("1." + convertStreamToString(req.getInputStream()));
      System.out.println("2." + req.getParameterNames().hasMoreElements());
      System.out.println("3." + convertStreamToString(req.getInputStream()));
      System.out.println("4." + req.getParameterNames().hasMoreElements());
      
      [22:50:28] emacsist:~ $ curl -X GET  -d "hello=world&hello2=world2" http://localhost:8008/parameters -v
      *   Trying ::1...
      * TCP_NODELAY set
      * Connected to localhost (::1) port 8008 (#0)
      > GET /parameters HTTP/1.1
      > Host: localhost:8008
      > User-Agent: curl/7.51.0
      > Accept: */*
      > Content-Length: 25
      > Content-Type: application/x-www-form-urlencoded
      >
      * upload completely sent off: 25 out of 25 bytes
      < HTTP/1.1 200 OK
      < Date: Mon, 20 Mar 2017 14:54:08 GMT
      < Content-Length: 0
      < Server: Jetty(9.3.7.v20160115)
      <
      * Curl_http_done: called premature == 0
      * Connection #0 to host localhost left intact
      

      它输出:

      1.hello=world&hello2=world2
      2.false
      3.
      4.false
      
  • 有 query string 时
    • 第一次调用 getParameter 时

      示例的Java代码:

      System.out.println("1." + req.getParameterNames().hasMoreElements());
      System.out.println("2." + convertStreamToString(req.getInputStream()));
      System.out.println("3." + req.getParameterNames().hasMoreElements());
      System.out.println("4." + convertStreamToString(req.getInputStream()));
      
      [23:08:07] emacsist:~ $ curl -X GET  -d "hello=world&hello2=world2" http://localhost:8008/parameters\?hello\=parameters -v
      *   Trying ::1...
      * TCP_NODELAY set
      * Connected to localhost (::1) port 8008 (#0)
      > GET /parameters?hello=parameters HTTP/1.1
      > Host: localhost:8008
      > User-Agent: curl/7.51.0
      > Accept: */*
      > Content-Length: 25
      > Content-Type: application/x-www-form-urlencoded
      >
      * upload completely sent off: 25 out of 25 bytes
      < HTTP/1.1 200 OK
      < Date: Mon, 20 Mar 2017 15:09:26 GMT
      < Content-Length: 0
      < Server: Jetty(9.3.7.v20160115)
      <
      * Curl_http_done: called premature == 0
      * Connection #0 to host localhost left intact
      

      它输出:

      1.true
      2.hello=world&hello2=world2
      3.true
      4.
      
    • 第一次调用 getInputStream 时

      示例的Java代码:

      System.out.println("1." + convertStreamToString(req.getInputStream()));
      System.out.println("2." + req.getParameterNames().hasMoreElements());
      System.out.println("3." + convertStreamToString(req.getInputStream()));
      System.out.println("4." + req.getParameterNames().hasMoreElements());
      
      [23:12:15] emacsist:~ $ curl -X GET  -d "hello=world&hello2=world2" http://localhost:8008/parameters\?hello\=parameters -v
      *   Trying ::1...
      * TCP_NODELAY set
      * Connected to localhost (::1) port 8008 (#0)
      > GET /parameters?hello=parameters HTTP/1.1
      > Host: localhost:8008
      > User-Agent: curl/7.51.0
      > Accept: */*
      > Content-Length: 25
      > Content-Type: application/x-www-form-urlencoded
      >
      * upload completely sent off: 25 out of 25 bytes
      < HTTP/1.1 200 OK
      < Date: Mon, 20 Mar 2017 15:13:11 GMT
      < Content-Length: 0
      < Server: Jetty(9.3.7.v20160115)
      <
      * Curl_http_done: called premature == 0
      * Connection #0 to host localhost left intact
      

      它的输出:

      1.hello=world&hello2=world2
      2.true
      3.
      4.true
      
方法为POST时
  • 没有 query string 时
    • 第一次调用 getParameter 时

      示例的Java代码:

      System.out.println("1." + req.getParameterNames().hasMoreElements());
      System.out.println("2." + convertStreamToString(req.getInputStream()));
      System.out.println("3." + req.getParameterNames().hasMoreElements());
      System.out.println("4." + convertStreamToString(req.getInputStream()));
      
      [23:16:50] emacsist:~ $ curl -X POST  -d "hello=world&hello2=world2" http://localhost:8008/parameters -v
      Note: Unnecessary use of -X or --request, POST is already inferred.
      *   Trying ::1...
      * TCP_NODELAY set
      * Connected to localhost (::1) port 8008 (#0)
      > POST /parameters HTTP/1.1
      > Host: localhost:8008
      > User-Agent: curl/7.51.0
      > Accept: */*
      > Content-Length: 25
      > Content-Type: application/x-www-form-urlencoded
      >
      * upload completely sent off: 25 out of 25 bytes
      < HTTP/1.1 200 OK
      < Date: Mon, 20 Mar 2017 15:16:55 GMT
      < Content-Length: 0
      < Server: Jetty(9.3.7.v20160115)
      <
      * Curl_http_done: called premature == 0
      * Connection #0 to host localhost left intact
      

      它输出:

      1.true
      2.
      3.true
      4.
      
    • 第一次调用 getInputStream 时

      示例的Java代码:

      System.out.println("1." + convertStreamToString(req.getInputStream()));
      System.out.println("2." + req.getParameterNames().hasMoreElements());
      System.out.println("3." + convertStreamToString(req.getInputStream()));
      System.out.println("4." + req.getParameterNames().hasMoreElements());
      
      [23:17:56] emacsist:~ $ curl -X POST  -d "hello=world&hello2=world2" http://localhost:8008/parameters -v
      Note: Unnecessary use of -X or --request, POST is already inferred.
      *   Trying ::1...
      * TCP_NODELAY set
      * Connected to localhost (::1) port 8008 (#0)
      > POST /parameters HTTP/1.1
      > Host: localhost:8008
      > User-Agent: curl/7.51.0
      > Accept: */*
      > Content-Length: 25
      > Content-Type: application/x-www-form-urlencoded
      >
      * upload completely sent off: 25 out of 25 bytes
      < HTTP/1.1 200 OK
      < Date: Mon, 20 Mar 2017 15:19:26 GMT
      < Content-Length: 0
      < Server: Jetty(9.3.7.v20160115)
      <
      * Curl_http_done: called premature == 0
      * Connection #0 to host localhost left intact
      

      它的输出:

      1.hello=world&hello2=world2
      2.false
      3.
      4.false
      
  • 有 query string 时
    • 第一次调用 getParameter 时

      示例的Java代码:

      System.out.println("1." + req.getParameterNames().hasMoreElements());
      System.out.println("2." + convertStreamToString(req.getInputStream()));
      System.out.println("3." + req.getParameterNames().hasMoreElements());
      System.out.println("4." + convertStreamToString(req.getInputStream()));
      
      [23:19:26] emacsist:~ $ curl -X POST  -d "hello=world&hello2=world2" http://localhost:8008/parameters\?hello\=paratemers -v
      Note: Unnecessary use of -X or --request, POST is already inferred.
      *   Trying ::1...
      * TCP_NODELAY set
      * Connected to localhost (::1) port 8008 (#0)
      > POST /parameters?hello=paratemers HTTP/1.1
      > Host: localhost:8008
      > User-Agent: curl/7.51.0
      > Accept: */*
      > Content-Length: 25
      > Content-Type: application/x-www-form-urlencoded
      >
      * upload completely sent off: 25 out of 25 bytes
      < HTTP/1.1 200 OK
      < Date: Mon, 20 Mar 2017 15:22:25 GMT
      < Content-Length: 0
      < Server: Jetty(9.3.7.v20160115)
      <
      * Curl_http_done: called premature == 0
      * Connection #0 to host localhost left intact
      

      它的输出:

      1.true
      2.
      3.true
      4.
      
    • 第一次调用 getInputStream 时

      示例的Java代码:

      System.out.println("1." + convertStreamToString(req.getInputStream()));
      System.out.println("2." + req.getParameterNames().hasMoreElements());
      System.out.println("3." + convertStreamToString(req.getInputStream()));
      System.out.println("4." + req.getParameterNames().hasMoreElements());
      
      [23:22:25] emacsist:~ $ curl -X POST  -d "hello=world&hello2=world2" http://localhost:8008/parameters\?hello\=paratemers -v
      Note: Unnecessary use of -X or --request, POST is already inferred.
      *   Trying ::1...
      * TCP_NODELAY set
      * Connected to localhost (::1) port 8008 (#0)
      > POST /parameters?hello=paratemers HTTP/1.1
      > Host: localhost:8008
      > User-Agent: curl/7.51.0
      > Accept: */*
      > Content-Length: 25
      > Content-Type: application/x-www-form-urlencoded
      >
      * upload completely sent off: 25 out of 25 bytes
      < HTTP/1.1 200 OK
      < Date: Mon, 20 Mar 2017 15:24:34 GMT
      < Content-Length: 0
      < Server: Jetty(9.3.7.v20160115)
      <
      * Curl_http_done: called premature == 0
      * Connection #0 to host localhost left intact
      

      它的输出:

      1.hello=world&hello2=world2
      2.true
      3.
      4.true
      

文件上传

content-type为 multipart/form-data 时 servlet 容器允许上传文件。 servlet 可以处理带有注解 @MultipartConfig 的请求。部署描述符为那些处理上传文件的请求包含了一个 multipart-config 元素。如果 servlet 容器提供了 multipart/form-data 处理,数据可以通过以下的 HttpServletRequest 对象的方法来处理。

  • getParts()
  • getPart(name)

每一个 part 都可以访问它的 headers, content type 以及通过 Part.getInputStream 来读取内容。 如果是带有这个 form-data 的,但并没有一个 filename ,则它也可以通过 getParametergetParameterValues 来访问。

如果 servlet 容器并不提供 multipart/form-data 处理,但数据仍可以通过 HttpServletRequest.getInputStream 来获取。

属性

它是与 request 对象相关的属性。属性的名字,以 java.javax. 开头的是规范里保留的。以 sun. , com.sun , oracle , com.oracle 是 为 Oracle 公司保留的。

请求头

  • getHeader
  • getHeaders
  • getHeaderNames

getHeader 可以是多个带有同名的 headers ,例如 Cache-Control ,如果是这样的话, getHeader 返回的是第一个。而 getHeaders 则返回同名的所有 header

请求路径元素

Context Path
ServletContext 相关的前缀路径,它是 servlet 的一部分。如果是 default (即 web server 的 root context) ,则它是一个空字符串。否则,它是一个以 / 字符 开头,但并不会以 / 字符结束的字符串。
Servlet Path
它是直接响应请求的 path 部分。以 / 字符开头,除了请求 /*"" pattern 匹配的请求外。
PathInfo
它不是 ContextPath 或 Servlet Path 的一部分。如果没有额外的路径的话,它会是 null 或者是以 / 开头的字符串。
requestURI = contextPath + servletPath + pathInfo

路径转换方法

  • ServletContext.getRealPath
  • HttpServletRequest.getPathTranslated

非阻塞IO

它仅在 Servlet 或 Filter 的异步请求处理工作。否则,当 ServletInputStream.setReadListener 会抛出 IllegalStateExceptionServletOutputStream.setWriteListener 异常。

Cookies

在客户端的每一个请求中,都会带上 Cookies 。如果一个 Cookie 设置了 HttpOnly ,这表示它不要暴露给客户端的脚本代码。

国际化

客户端可以指示Web服务器它更喜欢什么语言。客户端通过 Accept-Language header 告知。然后可以在 ServletRequest 使下面的方法来判断:

  • getLocale
  • getLocales

如果没有指定的话,则返回 Servlet 容器默认的 locale

请求数据编码

当前一些浏览器并没有发送一个 Content-Type header ,来开放编码机制。如果客户端请求没有指定编码的话,默认用来创建 request reader 和解析 post 数据的编码必须是 ISO-8859-1 。然而,在这种情况下,为了指示开发者,客户端并没有指明字符编码,容器应该在 getCharacterEncoding 方法返回 null

如果客户端并没有设置字符编码,并且请求的数据是与上述不同的编码的各方面,就有可能出现损坏数据。为了修复这种情况,已经为 ServletRequest 对象添加了 setCharacterEncoding 方法了。开发者可以通过调用这个方法来覆盖 servlet 容器提供的默认编码。它必须在解析任何 POST 数据或从请求中读取任何数据之前调用。在已经读取了数据之后再调用的话是无效的。

请求对象的生命周期

每个请求对象仅在 servlet 的 service 方法或 filter 的 doFilter 方法范围有效,除非是使用了异步处理。异步处理的话,请求的对象会一直有效,直到调用了 complete 方法。

Servlet Context

概述

它定义了 Web 应用中哪些 servlet 是正在运行的 servlet 视图。使用 ServletContext 对象,一个 servlet 可以记录事件,获取 URL 引用的资源,以及设置和保存可以在 context 中其他 servlet 访问的属性。

一个 ServletContext 是 Web 服务器已知的根路径。例如,一个 servlet context 可以定位为 http://www.mycorp.com/catalog 。则所有以 /catalog 请求路径开头的请求,它就是作为 Context Path ,所有这些请求都会被 Web 应用路由到该与之相关联的 ServletContext 中。

作用域

一个 ServletContext 对象是与容器中每一个部署的 Web 应用相关系的。即在每一个 JVM 中,都有一个 ServletContext 对象与一个 Web 应用关联。

在容器中并不作为 Web 应用的一部分来部署的 Servlet 是隐含于 default Web 应用的一部分,并且有一个默认的 ServletContext

初始参数

通过 ServletContext 的方法来获取

  • getInitParameter
  • getInitParameterNames

配置方法

以下方法是在 Servlet 3.0 时加入到 ServletContext 的,以便可编程性地定义 servlets, filters 以及 url 映射。这些方法仅可以在一个应用初始化期间,从 ServletContextListenercontexInitialized 方法或者 ServletContainerInitializeronStartup 方法中调用。否则要抛出 UnsupportedOperationException

也可以通过查找 Registration 对象来添加 Servlets, Filters 。

Context 属性

  • setAttribute
  • getAttribute
  • getAttributeNames
  • removeAttribute

资源

它是用来获取静态资源的(如 HTML, GIF等文件)而不是动态资源(如 JSP 等)

  • getResource
  • getResourceAsStream

它接受一个以 / 开头的字符串,表示 Context 中的根路径的相对路径 或是相对于 WEB-INF/lib 中的 jar 文件内部的 META-INF/resources 路径。 它首先搜索 Web 应用程序的根目录,然后才到 WEB-INF/lib 目录下的 jar 文件的路径。但是这些 jar 文件的搜索顺序是未定义的。

比如,如果它获取的是 JSP 则返回的是 JSP 的源码,而不是解析之后的内容的。

多个主机和 Servlet Context

每个逻辑主机都有它自己的 Servlet Conext 或多个 ServletContext 。ServletContext 不能跨虚拟主机共享。 ServletContext.getVirtualServerName 可以访问所在的虚拟主机名。

重载注意因素

虽然并不要求容器提供者实现 class reload ,不过,所以这些实现必须保证所有的 servlets, 以及使用的 class 都是使用同一个 class loader 作用域。

临时工作目录

每个 Servlet Context 都要求有一个临时保存目录。Servlet 容器必须为每个 Servlet Context 提供一个私有的临时目录,并让它可以通过 javax.servlet.context.tempdir 的 Context 属性来访问。与之相关的对象,必须是 java.io.File 类型。

当重启容器时,并不要求容器去维护这些临时工作目录的内容,但是要求确保这些 Servlet Context 的临时工作目录是不能被 Web 应用的其他的 ServletContext 访问的。

响应(response)对象

它概括了所有从服务器返回给客户端的信息。对于 HTTP 协议,这些信息是从服务器通过 HTTP 头或 Body 来传递的。

buffering

servlet 容器允许但并不要求将发送到客户端的输出进行缓冲,以便提高效率。可以通过 ServletResponse 以下的方法来访问或设置这些缓冲信息:

  • getBufferSize
  • setBufferSize
  • isCommitted
  • reset
  • resetBuffer
  • flushBuffer

这些在 ServletResponse 提供的方法,允许在使用 ServletOutputStreamWriter 时进行缓冲操作。

getBufferSize 返回正在使用的 buffer 大小。如果没有正在使用的 buffer ,则必须返回 int 类型的 0 (zero) 。可以通过 setBufferSize 来要求 servlet 使用 buffer 大小,但注意,这个并不一定就是这么大的,但至少是会这么大的大小。即实际的大小 >= setBufferSize 的大小。该方法必须要在任何的 ServletOutputStreamWriter 方法执行之前调用,否则它会抛出 IllegalStateException

isCommitted 返回一个布尔值,指明是否响应的字节已经返回给客户端了。

flushBuffer 方法强制在 buffer 的内容写回到客户端。

当响应没有 commited 时, reset 方法会清除在 buffer 中的数据,并且也会清除它的 headers, status code 以及在 reset 之前调用的 getWritergetOutputStream 的状态。 如果已经 commited 的话,*reset* 或 resetBuffer 必须抛出 IllegalStateException

当使用 buffer 时,如果 buffer 已经填充满了,则容器必须立即刷新数据给客户端。如果这是第一次发送给客户端的话,则认为响应是已经 commited 了的。

  • setHeader
  • addHeader

setHeader 会代替之前已经的 header。 addHeader 没有该header时就创建一个新的,如果已经存在,则追加一个。Header 包含的数据 ,可以为 intData 对象。可以通过下面方法来处理。

  • setIntHeader
  • setDateHeader
  • addIntHeader
  • addDateHeader

Servlet 程序员负责确保 Content-Type 已经恰当地设置好了。HTTP 1.1 规范并不要求在 HTTP 响应中设置这个 Header 。如果 Servlet 程序员并没有设置它的话,Servlet 容器不能设置一个默认的 Content-Type

建议容器使用 X-Powered-By HTTP header 来发布它的实现信息,它的值为它实现的类型,比如 Servlet/3.1

非阻塞IO

这个与 request 请求对象类似。 它仅在 Servlet 和 Filters 是异步的情况下才可以使用。否则会在 ServletInputStream.setReadListenerServletInputStream.setReadListener 调用时抛出 IllegalStateException

容器必须用线程安全的方法来访问 WriteListener

便捷方法

  • sendRedierct
  • sendError

这些方法会有一个 side effect —— commit 响应,如果还没有准备 committed 的话,就结束它。在调用这些方法之后,不再会有数据写回给客户端了。如果在这些方法之后向客户端写数据,则这些数据会被忽略。

国际化

Servlet 应该设置响应的 locale 和 字符编码。 locale 是通过 ServletResponse.setLocale 方法来设置的,该方法可以重复调用,但是在 committed 了响应之后再调用的话则是无效的。

如果 Servlet 没有设置 locale ,则容器的 locale 将决定响应的 locale ,但并不会在 HTTP 的 Content-Language 设置返回给客户端。

  • setLocale
  • setCharacterEncoding
  • setContentType

getWriter 或 committed 响应之后再调用这些方法的话,则字符编码并不会生效。

如果 setContentType 中含有 charset 属性的话,则以它来进行字符编码。 如果之前既没有 setCharacterEncoding 也没有 setContentType 来设置字符编码的话,则调用 setLocale 来设置字符编码。

如果在 getWriter 之前没有指定字符编码,则使用 ISO-8859-1

关闭 response 对象

以下条件之一满足时,表示已经关闭了 response 对象

  • 终止了 servlet 的 service 方法
  • 指定了 response 的 setContentLengthsetContentLengthLong 长度,而且它已经大于0并且已经写回了响应。
  • 调用了 sendError 方法
  • 调用了 sendRedierct 方法
  • 调用了 AsyncContext.complete 方法

Filtering 过滤器

filter 是什么

filter 是一段代码片段,它可以改变 HTTP 请求,响应,以及 header 信息。

它可以执行在动态或静态内容上。以下是使用 filters 可以做的功能:

  • 在调用请求之前访问的资源
  • 在调用请求处理资源之前
  • 通过自定义包装的 request 对象版本来修改 request header 和 data
  • 通过自定义包装的 response 对象版本来修改 response header 和 data
  • 在调用访问资源之后拦截它
  • 在 servlet 或静态资源上按指定顺序执行动作

例如

  • 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

主要概念

它是程序员通过实现 javax.servlet.Filter 接口并提供一个公共的,没有参数的构造器创建的。在部署描述中,通过 <filter> 元素来声明一个 filter ,然后通过 <filter-mapping> 来配置 url-pattern 设置规则。

filter 生命周期

它是由容器初始化这些声明的 filter 以及调用它的 init 方法。

当容器接收到一个 request 时,它会首先调用 filter ,然后调用它的 doFilter 方法。

doFilter 典型地会实现以些模式的一部分逻辑:

  1. 检测 request header 信息
  2. 通过实现 ServletRequest 或 HttpServletRequest 来自定义包装这些对象来修改它的 headers 或 data
  3. 通过实现 ServletResponse 或 HttpServletResponse 来自定义包装这些对象来修改它的 headers 或 data
  4. 可能调用一下个 filter 链。 service 方法要求是在同一条线程中执行的。
  5. 在调用下一个 filter 链完成后,filter 可能也要测试 response headers
  6. 或者,filter 可能抛出异常。如果一个 filter 抛出 UnavailableException 则不会处理后面的 filter 链了。
  7. 当最后一个 filter 链已经调用后,下一个就是目标 servlet 或 resource 了。
  8. 在一个 filter 实例从容器中移除服务之前,必须首先要调用它的 destroy 方法,以便让它释放各种资源。

包装 request 和 response

可以继承

  • ServletRequestWrapper
  • HttpServletRequestWrapper
  • ServletResponseWrapper
  • HttpServletResponseWrapper

filter 环境

可以通过在部署描述符中的 <init-params> 设置初始参数。它可以通过 filter 的 FilterConfig 对象的 getInitParametergetInitParameterNames 来访问。

在 Web 应用中配置 filters

  • filter-name
  • filter-class
  • init-params

每声明一个 filter 容器会仅会为它创建一个实例。即不管这个 filter 是什么类型的,只要它声明了多少次,就会创建多少个 filter 。 容器使用的 filter 链是用以下规则的:

  1. <url-pattern> 顺序与部署描述符里出现的顺序一致。
  2. <servlet-name> 顺序与部署描述符里出现的顺序一致

即下面的配置:

<filter-mapping>
    <filter-name>Multipe Mappings Filter</filter-name>
    <url-pattern>/foo/*</url-pattern>
    <servlet-name>Servlet1</servlet-name>
    <servlet-name>Servlet2</servlet-name>
    <url-pattern>/bar/*</url-pattern>
</filter-mapping>

等同于:

<filter-mapping>
    <filter-name>Multipe Mappings Filter</filter-name>
    <url-pattern>/foo/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>Multipe Mappings Filter</filter-name>
    <servlet-name>Servlet1</servlet-name>
</filter-mapping>
<filter-mapping>
    <filter-name>Multipe Mappings Filter</filter-name>
    <servlet-name>Servlet2</servlet-name>
</filter-mapping>
<filter-mapping>
    <filter-name>Multipe Mappings Filter</filter-name>
    <url-pattern>/bar/*</url-pattern>
</filter-mapping>

Filters 和 RequestDispatcher

通过在部署描述符中使用 <dispatcher> 元素,开发者可以指示 filter-mapping 是否应用 filter 到这些请求中:

请求直接来自客户端
如果指示这种请求应用 filter 的话,则设置 <dispatcher> 的值为 REQUEST
通过匹配到 <url-pattern> 或 <servlet-name> 的 Web 组件处理下,使用 forward() 转发的请求
这种指示的话,则设置 <dispatcher> 的值为 FORWARD
通过匹配到 <url-pattern> 或 <servlet-name> 的 Web 组件处理下,使用 include() 转发的请求
这种指示的话,则设置 <dispatcher> 的值为 INCLUDE
通过匹配到 <url-pattern> 处理的请求并产生了 error page 机制
这种指示的话,则设置 <dispatcher> 的值为 ERROR
异步请求处理
这种指示的话,则设置 <dispatcher> 的值为 ASYNC
(no term)
或者是以上的任意组合

比如以下配置:

<filter-mapping>
       <filter-name>Logging Filter</filter-name>
       <url-pattern>/products/*</url-pattern>
</filter-mapping>

它会在客户端以 products 开头的请求中调用这些 filter 。但不会在通过 dispatcherproducts 开头的请求中调用。

下面的配置:

<filter-mapping>
    <filter-name>Logging Filter</filter-name>
    <servlet-name>ProductServlet</servlet-name>
    <dispatcher>INCLUDE</dispatcher>
</filter-mapping>

这会导致该 filter 不会在客户端请求,或者 dispatcher 为 forward() 的请求中调用,但会在 dispatcher 为 include() 的请求中调用。

下面的配置:

<filter-mapping>
    <filter-name>Logging Filter</filter-name>
    <url-pattern>/products/*</url-pattern>
    <dispatcher>FORWARD</dispatcher>
    <dispatcher>REQUEST</dispatcher>
</filter-mapping>

它仅会在请求是客户端 或 dispatcher 为 forward() 的请求中调用。

Sessions 会话

HTTP 是状态的。为了分区不同的客户端,所以使用了 HttpSession 这个接口来定义会话。

Session 跟踪机制

Cookies

通过HTTP cookie 来跟踪 session 是最广泛使用的 session 跟踪机制,并且是要求所有的 servlet 容器都要支持的。

容器发送一个 Cookie 给客户端。客户端在后面的请求中,都返回这个Cookie给服务器,这样子来明确将请求与一个 session 关联。用来跟踪会话的标准Cookie名必须为 JSESSIONID 。容器可能允许自定义这个名字。

URL Rewriting

它是最低限度来进行会话跟踪标准。当客户端并不接受一个 Cookie 时,它是服务器进行会话跟踪的基础。该 SessionID 必须在 URL字符串中作为路径参数中编码。它的参数为必须是 jssessionid 。以下是一个例子:

http://www.myserver.com/catalog/index.html;jsessionid=1234

这种机制不应该在支持 Cookie 的情况下使用。

创建一个 session

由于 HTTP 是一个基于 请求-响应 的协议,一个 HTTP session 被认为是 new 的,直到有一个 client "加入" 它。

session 作用域

HttpSession 对象作用域,必须是在 application (或 servlet context) 级别。底层机制,比如用 Cookie 来建立一个 session ,对于不同的 context 是可以相同的,但对象的引用,包括对象的属性,必须不能在容器内的不同 context 共享。

可以用一个例子来说明这个要求:一个 servlet 使用 RequestDispatcher 来调用另一个Web应用的 servlet ,被调用的servlet 创建的 session 及可见性必须与调用者的可见性不同。

绑定属性到 session

HttpSessionBindingListener 接口可以接收绑定和解绑属性时的事件通知

  • valueBound (在 session.setAttribute 时触发)
  • valueUnbound ( 在 session.getAttribute 不可用时触发,即返回的对象是不可用的)

Session timeouts

在 HTTP 协议中,当客户端不再活动时,并没有显式的终止信号。这意味着唯一用来指示客户端不在活动的机制是使用一个 timeout 时间段。 默认的 timeout 是由 servlet 容器定义的,并且可以通过 HttpSession.getMaxInactiveInterval 来获取。它可以通过开发者调用 HttpSession.setMaxInactiveInterval 来指定 timeout ,它们的单位是 。按定义,如果 timeout 时间段设置为 0 或者更小的值,则该 Session 将永不过期。

Session无效会在所有 servlet 中的 service 退出时才会正式生效。一旦无效了,一个新的请求必须禁止看到那些 session 。

最后访问时间

HttpSession.getLastAccessedTime 当被容器第一次处理请求所属的 session 的一部分时,它就被认为是访问了的。

重要的 session 语义

线程问题

容器必须确保表示 session 内部数据结构的操作执行是线程安全的。 开发者负责线程安全地访问这些对象。

分布式环境

在一个分布式环境中,一个 session 部分中的请求必须是同一时间仅被一个 JVM 处理。容器必须能够恰当地处理 HttpSession 实例的所有使用 setAttributeputValue 对象。为了满足这些条件,以下的限制是强制的:

  • 容器必须接受实现 Serializable 接口的对象
  • 容器可以为 HttpSession 对象选择其他的存储方式
  • 可以通过容器指定的场所来进行迁移这些 session 。

注解和可插拔性

注解和可插拔性

在部署描述文件中的 <web-app> 元素,它有一个属性 metadata-complete ,它定义了该描述符是否已经是完整的。

true
则部署工具必须忽略任意的 servlet 注解的类(这些类是在 WEB-INF/classes 目录下,或者是在 WEB-INF/lib 目录下的 jar 包中)
false
表示扫描这些类(与上述的类的概念相同)来加载含有相应注解的配置

这些注解是下面之一

  • @WebServlet
  • @WebFilter
  • @WebInitParam
  • @WebListener
  • @MultipartConfig
  • 其他相应的注解

从这些注解中加载的 Listener , Servlet 的顺序示指明的。这些顺序仅可以在部署描述符中指定的。

可插拔性

web.xml 的模块化

一个 web fragment 是 web.xml 的一部分,它可以指示和包含在一个库或框架 jar 文件的 META-INF 目录中,一个在 WEB-INF/lib 目录的没有 web-fragment.xml 的也视作是一个 fragment 。它的名字必须是 web-fragment.xml

web.xml 和 web-fragment.xml 的顺序

一个 web-fragment.xml 有一个顶级的 <name> 元素,且仅只能有一个 <name> 元素,如果出现了这个元素,它必须要考虑它的顺序。

web.xml 的绝对顺序: <absolute-ordering> ,仅只有一个该元素在 web.xml 。 在 web-fragment.xml 是相对顺序 : <ordering> ,它的子元素有 <before><after>

转发请求

从一个请求中处理再转发给另一个 servlet 中处理,或包含另一个 servlet 输出的响应是比较常用的,而 RequestDispatcher 接口就是用来实现这种机制的。 当是一个异步请求, AsyncContext 允许用户转发这个请求回到 servlet 容器中。

获取一个 RequestDispatcher

通过 ServletContext 以下方法来获取

  • getRequestDispatcher
  • getNamedDispatcher

在 RequestDispatcher 路径的查询字符串

例如:

String path = “/raisins.jsp?orderno=5”;
RequestDispatcher rd = context.getRequestDispatcher(path);
rd.include(request, response);

用来创建 RequestDispatcher 的查询字符串参数比其他传递的同名参数的优先级高。 与 RequestDispatcher 相关联的参数的作用域仅在 incldueforward 中有效。

使用 RequestDispatcher

调用 RequestDispatcherincludeforward 方法即可。容器应该保证转发的请求的目标与源请求是在JVM的同一条线程上执行的。

include 方法

它可以在任意时候调用。 include 方法的目标 servlet ,可以访问 "request" 对象的所有方面,但是比较多限制地使用 response 对象。

它仅可以写信息到 response 对象的 ServletOutputStreamWriter 以及 commit 一个 response 。 但它不能设置 headers 或者其他会影响 headers 的方法( getSession 除外)。所有试图设置 headers 的方法,必须忽略,并且所有调用 getSession 的方法要求添加一个 Cookie 响应头的,在 commit 响应时必须抛出 IllegalStateException

include 请求参数

除了通过使用 getNamedDispatcher 之外获取的 servlets ,一个被另一个servlet 通过 include 调用的 servlet ,被请求的 servlet 可以通过以下的请求属性来访问是由哪些servlet请求的。 这些属性是设置在 request 对象上的,由 dispatch 机制负责设置

javax.servlet.include.request_uri
javax.servlet.include.context_path
javax.servlet.include.servlet_path
javax.servlet.include.path_info
javax.servlet.include.query_string

forward 方法

它仅可以在没有 commit 输出返回到客户端时才可以调用。如果有输出的数据存放在 response 的 buffer,并且还没有 committed ,但这些内容在调用目标 servlet 的 service 方法之前必须被清理掉。如果 response 已经被 committed ,则必须抛出 IllegalStateException

查询字符串

当 forward 或 include 时, RequestDispatcher 机制负责聚合查询字符串

forward 请求参数

它是设置在 request 对象属性上的,由 dispatch 机制负责设置

javax.servlet.forward.request_uri
javax.servlet.forward.context_path
javax.servlet.forward.servlet_path
javax.servlet.forward.path_info
javax.servlet.forward.query_string

dispatch 方法

它是从 AsyncContext 中调用这些方法的

dispatch 请求参数

它是设置在 request 对象属性上的(由 dispatch 机制负责设置)

javax.servlet.async.request_uri
javax.servlet.async.context_path
javax.servlet.async.servlet_path
javax.servlet.async.path_info
javax.servlet.async.query_string

Web 应用程序

一个 Web 应用程序对应一个 ServletContext 。

目录结构

假设 /catalog 是 Context Path ,则访问 /catalog/index.html 时,Web容器可以通过以下情况下满足这个请求:

  • 在一个 Web 应用程序的基础目录里有一个 index.html 文件 (这个优先级最高)
  • 或者在 WEB-INF/lib 目录的 jar 包里的 META-INF/resources 目录下有 index.html 文件

特殊的目录 : WEB-INF 。该目录下的大部分文档并不是 public 的,除了在 WEB-INF/lib 目录下的 jar 文件里的 META-INF/resources 的静态文件或JSP 文件。

但是,*WEB-INF* 目录的内容,可以通过 servlet 代码 ServletContext 对象的 getResourcegetResourceAsStream 方法来访问。

由于匹配资源的请求映射是大小写敏感的,客户端请求 /WEB-INF/foo /WEb-iNf/foo ,它们不应该返回这些内容,也不应该返回任何的目录列表。

Web应用程序的 class loader 必须首先加载 WEB-INF/classes 目录,然后再到 WEB-INF/lib 目录。除了 Jar 包文件里的静态资源外,其他任何从客户端的请求访问 WEB-INF 目录,必须返回 404 。

Error Pages

部署文件描述里可以有 <error-page> 元素.

Welcome Files

这种机制的目的是当有一个URI请求响应一个 目录 入口但并没有映射的Web组件对应时,允许部署者为容器追加指定一个部分URL顺序列表。这种类型的请求,又称为有效的部分请求( a valid partial request )

例如有一个 welcome file 为 index.html ,有一个请求为 host:port/webapp/directory/directory 是一个入口并没有映射到相应的 servlet 或 jsp 页面,则它会返回给客户端的是 host:port/webapp/directory/index.html

Welcome files 是没有以 / 字符结尾或开头的。

应用程序生命周期事件

事件类型和接口

Servlet Context 事件

event desc interface
Lifecycle The servlet context has just been created and is available to service its first request, or the servlet context is about to be shut down javax.servlet.ServletContextListener
Changes to attributes Attributes on the servlet context have been added, removed, or replaced javax.servlet. ServletContextAttributeListener

HTTP Session 事件

event desc interface
Lifecycle An HttpSession has been created, invalidated, or timed out javax.servlet.http.HttpSessionListener
Changes to attributes Attributes have been added, removed, or replaced on an HttpSession javax.servlet.http HttpSessionAttributeListener
Changes to id The id of HttpSession has been changed. javax.servlet.http HttpSessionIdListener
Session migration HttpSession has been activated or passivated javax.servlet.http HttpSessionActivationListener
Object binding Object has been bound to or unbound from HttpSession javax.servlet.http HttpSessionBindingListener

Servlet Request 事件

event desc interface
Lifecycle A servlet request has started being processed by Web components. javax.servlet.ServletRequestListener
Changes to attributes Attributes have been added, removed, or replaced on a ServletRequest. javax.servlet. ServletRequestAttributeListener
Async events A timeout, connection termination or completion of async processing javax.servlet.AsyncListener

部署声明

Listener 类可以在部署描述文件中用 <listener> 元素来声明。它们的出现顺序,也是它们的调用顺序。如果是 AsyncListener 的 listener, 它只能通过编程的方式来注册(ServletRequest)。

listener 异常

当一个事件的listener 出现异常时,该事件的后面的listener就不会再执行了。

映射请求到Servlet

使用URL路径

收到一个客户端请求时,Web容器要决定将它转发到哪个Web应用程序中。选择的Web程序程序必须从请求的URL中匹配最长的Context路径。匹配的URL部分就是映射到servlet的 Context 路径。

以下是匹配的规则顺序,当其中有一个匹配时,刚不会进行后续的匹配了:

  1. 容器会尝试寻找最精确匹配的路径。当成功找到时,则选择这个 servlet 来处理请求。
  2. 容器会递归尝试匹配最长的路径前缀。它是通过一次一个路径树中的目录来完成的,使用 / 字符作为一个路径的分隔符。最长匹配的则选择该 servlet 。
  3. 如果在URL路径的最后一段包含一个扩展名,则 servlet 容器将会尝试匹配那些处理这些扩展的 servlet 来处理。一个扩展名的定义是路径最后一段的在 . 字符后面的部分。
  4. 如果上面的规则都不匹配,容器将试图提供适当的资源请求。如果应用程序定义了一个 default servlet,它将会被使用。一些容器提供了一个隐式的 default servlet 来服务这些内容。

容器必须使用大小写敏感的字符串来进行比较。

映射规范

  • 一个以 / 字符开头并且以 /* 后缀结尾的字符串,是用作路径映射的。(path mapping)
  • 一个以 *. 前缀开头的字符串,是用作扩展名映射的。(extension mapping)
  • 一个空字符串 "" 是特殊的URL模式,它精确映射到应用程序的 root context ,即,请求形式为 http://host:port/<context- root>/ 。在这情况下,PathInfo 为 / ,Servlet Path 和 Context Path 都是空字符串 ""
  • 一个仅包含 / 的字符串,表示它是应用程序的 default servlet 。在这情况下,Servlet Path 为请求的 URI 减去 Context Path ,并且 PathInfo 为 null
  • 其他字符串,仅用作精确匹配。

隐式映射

如果容器有一个内部的 JSP 容器,则 *.jsp 扩展会映射给它。这些映射就是隐式映射。

作者: emacsist

Created: 2017-03-22 Wed 00:10