Servlet 总结什么是 Servlet？Servlet 是 Java EE 规范中的核心组件，它是一种运行在服务器端的 Java 程序，主要用于处理客户端请求并作出响应。Servlet 的执行过程？请求发送：客户端发送请求到服务器端。请求转发：服务器端根据 web.xml 文件中的 Servlet 相关配置信息，将客户端请求转发到相应的 Servlet。业务处理：Servlet 根据 request 对象中封装的用户请求与数据库进行交互；返回数据之后，Servlet 将数据封装到 response 对象中。响应返回：控制权从 Servlet 重新回到服务器端，服务器端将响应信息返回给客户端，并跳转到相应的页面。Servlet 的生命周期？加载和实例化：在第一次请求 Servlet 时，Servlet 容器将会创建 Servlet 实例。初始化：Servlet 容器加载完成 Servlet 之后，必须进行初始化，此时 init() 方法将被调用。服务请求：Servlet 初始化之后，处于响应请求的就绪状态。此时如有客户端请求发送，就会调用 Servlet 实例的 service() 方法，并且根据用户的请求方式，调用 doPost() 或者 doGet() 方法。销毁：最后，Servlet 容器负责将 Servlet 实例进行销毁，调用 destroy() 方法实现。Servlet 与 JSP 的关系及区别？关系：Servlet 是 JSP 的基础，JSP 是 Servlet 技术的扩展。JSP 运行之前首先会被编译为一个 Servlet。区别：JSP 侧重于视图（View）展示；Servlet 主要用于控制业务逻辑（Controller）。Servlet API 中的 forward() 与 redirect() 的区别？特性请求转发 (forward)重定向 (redirect)实现方法Request 对象的方法Response 对象的方法执行位置服务器端执行客户端浏览器执行请求次数1 次请求2 次请求地址栏变化不会改变发生改变数据共享始终在同一个 Request 域中，可共享资源第二次请求将丢失第一次 Request 中的资源性能优于重定向相对较低Servlet 如何同时处理多个请求？Servlet 采用多线程来处理多个请求的同时访问。Servlet 容器通过线程池来管理维护服务请求。所谓线程池，相当于数据库连接池，实际上是等待执行代码的一组线程，叫做工作者线程。Servlet 容器通过一个调度线程来管理工作者线程。当容器收到一个 Servlet 的访问请求，调度者线程就从线程池中选出一个工作者线程，将用户请求传递给该线程，然后由该线程处理 Servlet 的 service() 方法；当这个线程在执行的时候，容器收到一个新的请求，调度者线程再次从线程池中选出一个新的工作者线程；当容器同时收到对同一个 Servlet 的多个请求时，那么 Servlet 的 service() 方法将在多线程中并发执行。注：Servlet 容器默认采用单实例多线程的方式来处理请求。这样减少了产生 Servlet 实例的开销，提升了对请求的响应时间。对于 Tomcat 容器来讲，可以在其 server.xml 中通过 <Connector> 配置设置线程池中的线程数目。问题：Servlet 可不可以采用多实例多线程？或者多实例单线程？如何开发线程安全的 Servlet？Servlet 容器采用多线程来处理请求，提高性能的同时也造成了线程安全问题。要开发线程安全的 Servlet 应该从以下几个方面进行：变量的线程安全：多线程并不共享局部变量，所以我们要尽可能的在 Servlet 中使用局部变量。代码块的线程安全：使用同步块 synchronized，防止可能调用的代码块冲突。但是要注意的是，要尽可能缩小同步代码的范围，不要在 service() 方法和响应方法上直接使用同步，这会严重影响性能。属性的线程安全：注意 ServletContext、HttpSession、ServletRequest 对象中属性的安全性。使用同步集合：使用 Vector 代替 ArrayList，使用 Hashtable 代替 HashMap。避免自定义线程：不要在 Servlet 中创建自己的线程来完成某个功能。Servlet 本身就是多线程的，如果再创建新的线程，将会导致线程执行复杂化，出现线程安全问题。外部对象互斥：在多个 Servlet 中，对外部对象（比如文件）进行修改操作一定要加锁，做到互斥访问。SingleThreadModel 接口？javax.servlet.SingleThreadModel 接口是一个标识接口。如果一个 Servlet 实现了这个接口，那 Servlet 容器将保证在一个时刻仅有一个线程可以在给定的 Servlet 实例的 service() 方法中执行，将其他所有请求进行排队。如何实现 Servlet 的单线程模式？实现方法（针对 JSP）：<%@ page isThreadSafe="false" %>说明：本文内容基于传统 Servlet 规范整理。SingleThreadModel 接口已在 Servlet 2.4 版本中被标记为 deprecated（废弃），并在 Servlet 3.0 版本中被移除。现代开发中不建议使用该模式，应通过优化代码逻辑（如避免使用实例变量）来保证线程安全。关于同步集合的建议（如 Vector/Hashtable）属于早期 Java 并发编程实践，现代开发推荐使用 java.util.concurrent 包下的并发容器。
Tomcat 中 Servlet 的创建初始化体系结构及工作一个 Web 应用对应一个 Context 容器。在 Tomcat 中，Context 容器是管理 Servlet 的核心，也是 Servlet 运行时的容器。当添加一个 Web 应用时，Tomcat 会创建一个 StandardContext 容器，并为其设置必要的参数。其中，url 和 path 分别代表该应用在 Tomcat 中的访问路径和实际的物理路径。Web 应用的初始化工作Tomcat 中 Web 应用的初始化工作主要在 ContextConfig 的 configureStart 方法中实现。初始化的核心任务是解析 web.xml 文件，该文件描述了 Web 应用的关键信息，是应用的入口。解析过程如下：查找全局配置：Tomcat 首先查找 globalWebXml，该文件位于 org/apache/catalina/startup/NO_DEFAULT_XML 或 conf/web.xml 中的任一个。查找宿主配置：接着查找 hostWebXml，即应用的配置文件（例如 examples/WEB-INF/web.xml）。解析与保存：web.xml 文件的各个配置项会被解析成相应的属性，保存在 WebXml 对象中。设置容器属性：随后将 WebXml 对象中的属性设置到 Context 容器中，包括创建 Servlet 对象、Filter、Listener 等。最后，Servlet 会被包装成 StandardWrapper 并作为子容器添加到 Context 中。因此，Context 容器才是真正运行 Servlet 的容器，其配置属性由应用的 web.xml 指定。创建 Servlet 对象如果 Servlet 的 load-on-startup 配置项大于 0，那么在 Context 容器启动时就会被实例化。前文提到，在解析配置文件时会读取默认的 globalWebXml。在 conf 下的 web.xml 文件中定义了一些默认配置项，其中定义了两个 Servlet：org.apache.catalina.servlets.DefaultServletorg.apache.jasper.servlet.JspServlet它们的 load-on-startup 分别是 1 和 3。这意味着当 Tomcat 启动时，这两个 Servlet 会被优先启动。JspServlet 主要用于将 JSP 页面转换成 Servlet，它会拦截所有以 .jsp 为后缀的文件。创建 Servlet 实例的方法是从 Wrapper.loadServlet 开始的。loadServlet 方法的主要任务是获取 servletClass，然后将其交给 InstanceManager 去创建一个基于 servletClass.class 的对象。初始化 Servlet由于 Servlet 在 Web 初始化时已经包装在 StandardWrapper 中，因此 Servlet 的初始化是在 StandardWrapper 的 initServlet 方法中进行的。该方法逻辑简单，主要是调用 Servlet 的 init 方法，同时将包装了 StandardWrapper 对象的 StandardWrapperFacade 作为 ServletConfig 传给 Servlet。如果该 Servlet 关联的是一个 JSP 文件，那么前面初始化的就是 JspServlet。接下来会模拟一次简单请求，调用这个 JSP 文件，以便将其编译为 class 文件，并初始化这个 class。Servlet 体系结构Java Web 应用是基于 Servlet 规范运转的。下面介绍一下 Servlet 的体系结构。顶层类关联从上图可以看出，Servlet 规范主要基于几个核心类运转。与 Servlet 主动关联的是三个类，分别是 ServletConfig、ServletRequest 和 ServletResponse。这三个类都是通过容器传递给 Servlet 的：ServletConfig：在 Servlet 初始化时传给 Servlet，主要用于获取 Servlet 的配置属性，这些属性可能在 Servlet 运行时被用到。ServletRequest 和 ServletResponse：在请求到达调用 Servlet 时传递过来。它们代表了请求和响应的具体对象，通常作为运输工具来传递交互结果。Servlet 的运行模式是一个典型的“握手型的交互式”运行模式。所谓“握手型的交互式”，是指两个模块为了交换数据通常都会准备一个交易场景，这个场景一直跟随交易过程直到交易完成。交易场景的初始化是根据这次交易对象指定的参数来定制的，这些指定参数通常是一个配置类。交易场景由 ServletContext 来描述。定制的参数集合由 ServletConfig 来描述。容器中的类关系下图是 ServletConfig 和 ServletContext 在 Tomcat 容器中的类关系图。上图可以看出，StandardWrapper 和 StandardWrapperFacade 都实现了 ServletConfig 接口，而 StandardWrapperFacade 是 StandardWrapper 的门面类。因此，传给 Servlet 的是 StandardWrapperFacade 对象。这个类能够保证从 StandardWrapper 中拿到 ServletConfig 所规定的数据，而又不把 ServletConfig 不关心的数据暴露给 Servlet。同样，ServletContext 也与 ServletConfig 有类似的结构。Servlet 中能拿到的 ServletContext 的实际对象也是 ApplicationContextFacade 对象。ApplicationContextFacade 同样保证 ServletContext 只能从容器中拿到它该拿的数据。它们都起到了对数据的封装作用，使用的都是门面设计模式（Facade Pattern）。通过 ServletContext 可以拿到 Context 容器中的一些必要信息，比如应用的工作路径、容器支持的 Servlet 最小版本等。Request 相关类结构我们在创建自己的 Servlet 类时，通常使用的是 HttpServletRequest 和 HttpServletResponse，它们继承了 ServletRequest 和 ServletResponse。为何 Context 容器传过来的 ServletRequest、ServletResponse 可以被转化为 HttpServletRequest 和 HttpServletResponse 呢？Tomcat 一接收到请求，首先将会创建 org.apache.coyote.Request 和 org.apache.coyote.Response。这两个类是 Tomcat 内部使用的描述一次请求和相应信息的类，它们是轻量级的类。作用是在服务器接收到请求后，经过简单解析将这个请求快速地分配给后续线程去处理，所以它们的对象很小，很容易被 JVM 回收。接下来，当交给一个用户线程去处理这个请求时，又创建 org.apache.catalina.connector.Request 和 org.apache.catalina.connector.Response 对象。这两个对象一直穿越整个 Servlet 容器，直到要传给 Servlet。传给 Servlet 的是 Request 和 Response 的门面类 RequestFacade 和 ResponseFacade。这里使用门面模式与前面一样，都是基于同样的目的——封装容器中的数据。一次请求对应的 Request 和 Response 的类转化如下图所示：Servlet 如何工作当用户从浏览器向服务器发起一个请求，通常会包含如下信息：http://hostname:port/contextpath/servletpath。hostname 和 port：用来与服务器建立 TCP 连接。后面的 URL：用来选择服务器中那个子容器服务用户的请求。在 Tomcat 7.0 中，这种映射工作用一个专门的类来完成，即 org.apache.tomcat.util.http.mapper。这个类保存了 Tomcat 的 Container 容器中的所有子容器的信息。当 org.apache.catalina.connector.Request 类进入 Container 容器之前，mapper 将会根据这次请求的 hostname 和 contextpath 将 host 和 context 容器设置到 Request 的 mappingData 属性中。因此，当 Request 进入 Container 容器之前，它要访问哪个子容器就已经确定了。当 Request 请求到达了最终的 Wrapper 容器后，此时还需要真正的到达最终的 Servlet，这里还需要一些步骤：必须执行 Filter 链。通知你在 web.xml 中定义的 Listener。接下来就要执行 Servlet 的 service 方法了。通常情况下，我们自己定义的 Servlet 并不是直接去实现 javax.servlet.Servlet 接口，而是去继承更简单的 HttpServlet 类或者 GenericServlet 类，我们可以有选择地覆盖相应方法去实现我们要完成的工作。Servlet 的确已经能够帮我们完成所有的工作了，但是现在的 Web 应用很少有直接将交互全部页面都用 Servlet 来实现，而是采用更加高效的 MVC 框架 来实现。这些 MVC 框架基本的原理都是将所有的请求都映射到一个 Servlet，然后去实现 service 方法，这个方法也就是 MVC 框架的入口。当 Servlet 从 Servlet 容器中移除时，也就表明该 Servlet 的生命周期结束了。这时 Servlet 的 destroy 方法将被调用，做一些扫尾工作。说明：本文内容基于 Tomcat 7.0 版本架构整理，部分类名与内部实现机制在不同版本（如 Tomcat 8/9/10）中可能有所调整，仅供参考学习 Servlet 容器基本原理。
从 Servlet 容器说起要深入理解 Servlet，首先必须厘清 Servlet 容器（Servlet Container）的概念。Servlet 与 Servlet 容器的关系类似于“枪与子弹”：枪为子弹而生，子弹赋予枪杀伤力。二者彼此依存却又独立发展，这是工业化生产与标准化接口的结果。从技术角度而言，这种设计旨在实现解耦（Decoupling），通过标准化接口进行协作。既然接口是连接 Servlet 与 Servlet 容器的关键，我们将从接口层面展开分析。Servlet 容器作为独立发展的标准化产品，种类繁多且各有市场定位，难分优劣。例如，Jetty 在定制化和移动领域表现优异，而本文将以大家最为熟悉的 Tomcat 为例，介绍 Servlet 容器如何管理 Servlet。Tomcat 架构复杂，本文仅聚焦于 Servlet 与容器的接口部分，关于 Tomcat 的详细架构可参考相关文章《Tomcat 系统架构与模式设计分析》。在 Tomcat 的容器等级中，Context 容器直接管理 Servlet 在容器中的包装类 Wrapper。因此，Context 容器的运行方式将直接影响 Servlet 的工作模式。图 1. Tomcat 容器模型从上图可以看出，Tomcat 容器分为四个等级，真正管理 Servlet 的是 Context 容器。一个 Context 对应一个 Web 工程，这在 Tomcat 的配置文件中显而易见，如下所示：清单 1. Context 配置参数<Context path="/projectOne" docBase="D:\projects\projectOne" reloadable="true" />下面将详细介绍 Tomcat 解析 Context 容器的过程，包括如何构建 Servlet。Servlet 容器的启动过程Tomcat 7 开始支持嵌入式功能，增加了一个启动类 org.apache.catalina.startup.Tomcat。创建该类的实例对象并调用 start 方法即可启动 Tomcat。我们还可以通过该对象动态增加和修改配置参数，例如动态添加 Context、Servlet 等。下面利用这个 Tomcat 类来管理新增的一个 Context 容器，选择 Tomcat 7 自带的 examples Web 工程，观察它是如何加载到 Context 容器中的。清单 2. 给 Tomcat 增加一个 Web 工程Tomcat tomcat = getTomcatInstance(); File appDir = new File(getBuildDirectory(), "webapps/examples"); tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); tomcat.start(); ByteChunk res = getUrl("http://localhost:" + getPort() + "/examples/servlets/servlet/HelloWorldExample"); assertTrue(res.toString().indexOf(" Hello World! ") > 0);清单 2 的代码创建了一个 Tomcat 实例并新增一个 Web 应用，然后启动 Tomcat 并调用其中的一个 HelloWorldExample Servlet，验证是否正确返回预期数据。Tomcat 的 addWebapp 方法代码如下：清单 3. Tomcat.addWebapppublic Context addWebapp(Host host, String url, String path) { silence(url); Context ctx = new StandardContext(); ctx.setPath(url); ctx.setDocBase(path); if (defaultRealm == null) { initSimpleAuth(); } ctx.setRealm(defaultRealm); ctx.addLifecycleListener(new DefaultWebXmlListener()); ContextConfig ctxCfg = new ContextConfig(); ctx.addLifecycleListener(ctxCfg); ctxCfg.setDefaultWebXml("org/apache/catalina/startup/NO_DEFAULT_XML"); if (host == null) { getHost().addChild(ctx); } else { host.addChild(ctx); } return ctx; }前文已介绍一个 Web 应用对应一个 Context 容器，即 Servlet 运行时的 Servlet 容器。添加一个 Web 应用时，将会创建一个 StandardContext 容器，并设置必要参数。url 和 path 分别代表应用在 Tomcat 中的访问路径和实际物理路径，这两个参数与清单 1 中的参数一致。其中最重要的配置是 ContextConfig，该类负责整个 Web 应用配置的解析工作（后文详述）。最后，将这个 Context 容器添加到父容器 Host 中。接下来调用 Tomcat 的 start 方法启动 Tomcat。若熟悉 Tomcat 系统架构，便容易理解其启动逻辑：Tomcat 的启动逻辑基于观察者模式（Observer Pattern）设计。所有容器都继承 Lifecycle 接口，它管理容器的整个生命周期。所有容器的修改和状态改变都会通知已注册的观察者（Listener）。关于此设计模式可参考《Tomcat 的系统架构与设计模式，第二部分：设计模式》。Tomcat 启动的时序图如图 2 所示。图 2. Tomcat 主要类的启动时序图（查看大图）上图描述了 Tomcat 启动过程中主要类之间的时序关系。下面重点关注添加 examples 应用所对应的 StandardContext 容器的启动过程。当 Context 容器初始化状态设为 init 时，添加在 Context 容器的 Listener 将会被调用。ContextConfig 继承了 LifecycleListener 接口，它是在调用清单 3 时被加入到 StandardContext 容器中。ContextConfig 类负责整个 Web 应用配置文件的解析工作。ContextConfig 的 init 方法主要完成以下工作：创建用于解析 XML 配置文件的 contextDigester 对象。读取默认 context.xml 配置文件，如果存在则解析它。读取默认 Host 配置文件，如果存在则解析它。读取默认 Context 自身的配置文件，如果存在则解析它。设置 Context 的 DocBase。ContextConfig 的 init 方法完成后，Context 容器会执行 startInternal 方法。这个方法启动逻辑比较复杂，主要包括如下几个部分：创建读取资源文件的对象。创建 ClassLoader 对象。设置应用的工作目录。启动相关的辅助类，如：logger、realm、resources 等。修改启动状态，通知感兴趣的观察者（Web 应用的配置）。子容器的初始化。获取 ServletContext 并设置必要的参数。初始化"load on startup"的 Servlet。Web 应用的初始化工作Web 应用的初始化工作是在 ContextConfig 的 configureStart 方法中实现的。应用初始化的主要任务是解析 web.xml 文件，该文件描述了一个 Web 应用的关键信息，也是 Web 应用的入口。Tomcat 首先会寻找 globalWebXml 文件，搜索路径是在 engine 的工作目录下寻找以下两个文件中的任一个：org/apache/catalina/startup/NO_DEFAULT_XML 或 conf/web.xml。接着会找 hostWebXml 文件，可能位于 System.getProperty("catalina.base")/conf/${EngineName}/${HostName}/web.xml.default。最后寻找应用的配置文件 examples/WEB-INF/web.xml。web.xml 文件中的各个配置项将会被解析成相应的属性保存在 WebXml 对象中。如果当前应用支持 Servlet 3.0，解析还将完成额外 9 项工作，主要是为 Servlet 3.0 新增的特性，包括 jar 包中的 META-INF/web-fragment.xml 的解析以及对 annotations 的支持。接下来将 WebXml 对象中的属性设置到 Context 容器中，这里包括创建 Servlet 对象、Filter、Listener 等等。这段代码在 WebXml 的 configureContext 方法中。下面是解析 Servlet 的代码片段：清单 4. 创建 Wrapper 实例for (ServletDef servlet : servlets.values()) { Wrapper wrapper = context.createWrapper(); String jspFile = servlet.getJspFile(); if (jspFile != null) { wrapper.setJspFile(jspFile); } if (servlet.getLoadOnStartup() != null) { wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue()); } if (servlet.getEnabled() != null) { wrapper.setEnabled(servlet.getEnabled().booleanValue()); } wrapper.setName(servlet.getServletName()); Map<String, String> params = servlet.getParameterMap(); for (Entry<String, String> entry : params.entrySet()) { wrapper.addInitParameter(entry.getKey(), entry.getValue()); } wrapper.setRunAs(servlet.getRunAs()); Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs(); for (SecurityRoleRef roleRef : roleRefs) { wrapper.addSecurityReference( roleRef.getName(), roleRef.getLink()); } wrapper.setServletClass(servlet.getServletClass()); MultipartDef multipartdef = servlet.getMultipartDef(); if (multipartdef != null) { if (multipartdef.getMaxFileSize() != null && multipartdef.getMaxRequestSize() != null && multipartdef.getFileSizeThreshold() != null) { wrapper.setMultipartConfigElement(new MultipartConfigElement( multipartdef.getLocation(), Long.parseLong(multipartdef.getMaxFileSize()), Long.parseLong(multipartdef.getMaxRequestSize()), Integer.parseInt( multipartdef.getFileSizeThreshold()))); } else { wrapper.setMultipartConfigElement(new MultipartConfigElement( multipartdef.getLocation())); } } if (servlet.getAsyncSupported() != null) { wrapper.setAsyncSupported( servlet.getAsyncSupported().booleanValue()); } context.addChild(wrapper); }这段代码清楚地描述了如何将 Servlet 包装成 Context 容器中的 StandardWrapper。这里有个疑问：为什么要将 Servlet 包装成 StandardWrapper 而不是直接使用 Servlet 对象？StandardWrapper 是 Tomcat 容器中的一部分，它具有容器的特征；而 Servlet 作为一个独立的 Web 开发标准，不应该强耦合在 Tomcat 中。除了将 Servlet 包装成 StandardWrapper 并作为子容器添加到 Context 中，其它的所有 web.xml 属性都被解析到 Context 中。所以说，Context 容器才是真正运行 Servlet 的 Servlet 容器。一个 Web 应用对应一个 Context 容器，容器的配置属性由应用的 web.xml 指定，这样我们就能理解 web.xml 到底起到什么作用了。创建 Servlet 实例前面已经完成了 Servlet 的解析工作，并且被包装成 StandardWrapper 添加在 Context 容器中，但它仍然不能为我们工作，因为它还没有被实例化。下面将介绍 Servlet 对象是如何创建以及如何被初始化的。创建 Servlet 对象如果 Servlet 的 load-on-startup 配置项大于 0，那么在 Context 容器启动的时候就会被实例化。前面提到在解析配置文件时会读取默认的 globalWebXml，在 conf 下的 web.xml 文件中定义了一些默认的配置项，其定义了两个 Servlet，分别是：org.apache.catalina.servlets.DefaultServlet 和 org.apache.jasper.servlet.JspServlet。它们的 load-on-startup 分别是 1 和 3，也就是当 Tomcat 启动时这两个 Servlet 就会被启动。创建 Servlet 实例的方法是从 Wrapper.loadServlet 开始的。loadServlet 方法要完成的就是获取 servletClass，然后把它交给 InstanceManager 去创建一个基于 servletClass.class 的对象。如果这个 Servlet 配置了 jsp-file，那么这个 servletClass 就是 conf/web.xml 中定义的 org.apache.jasper.servlet.JspServlet 了。创建 Servlet 对象的相关类结构图如下：图 3. 创建 Servlet 对象的相关类结构初始化 Servlet初始化 Servlet 在 StandardWrapper 的 initServlet 方法中。这个方法很简单，就是调用 Servlet 的 init 方法，同时把包装了 StandardWrapper 对象的 StandardWrapperFacade 作为 ServletConfig 传给 Servlet。Tomcat 容器为何要传 StandardWrapperFacade 给 Servlet 对象，将在后面做详细解析。如果该 Servlet 关联的是一个 JSP 文件，那么前面初始化的就是 JspServlet。接下来会模拟一次简单请求，请求调用这个 JSP 文件，以便编译这个 JSP 文件为 class，并初始化这个 class。这样 Servlet 对象就初始化完成了。事实上，Servlet 从被 web.xml 中解析到完成初始化，这个过程非常复杂，中间有很多过程，包括各种容器状态的转化引起的监听事件的触发、各种访问权限的控制和一些不可预料的错误发生的判断行为等等。我们这里只抓了一些关键环节进行阐述，试图让大家有个总体脉络。下面是这个过程的一个完整的时序图，其中也省略了一些细节。图 4. 初始化 Servlet 的时序图（查看大图）Servlet 体系结构我们知道 Java Web 应用是基于 Servlet 规范运转的，那么 Servlet 本身又是如何运转的呢？为何要设计这样的体系结构？图 5. Servlet 顶层类关联图从上图可以看出，Servlet 规范就是基于这几个类运转的。与 Servlet 主动关联的是三个类，分别是 ServletConfig、ServletRequest 和 ServletResponse。这三个类都是通过容器传递给 Servlet 的，其中 ServletConfig 是在 Servlet 初始化时就传给 Servlet 了，而后两个是在请求达到时调用 Servlet 时传递过来的。我们很清楚 ServletRequest 和 ServletResponse 在 Servlet 运行中的意义，但是 ServletConfig 和 ServletContext 对 Servlet 有何价值？仔细查看 ServletConfig 接口中声明的方法发现，这些方法都是为了获取这个 Servlet 的一些配置属性，而这些配置属性可能在 Servlet 运行时被用到。而 ServletContext 又是干什么的呢？Servlet 的运行模式是一个典型的“握手型的交互式”运行模式。所谓“握手型的交互式”就是两个模块为了交换数据通常都会准备一个交易场景，这个场景一直跟随这个交易过程直到交易完成为止。这个交易场景的初始化是根据这次交易对象指定的参数来定制的，这些指定参数通常就是一个配置类。所以对号入座，交易场景就由 ServletContext 来描述，而定制的参数集合就由 ServletConfig 来描述。而 ServletRequest 和 ServletResponse 就是要交互的具体对象了，它们通常都是作为运输工具来传递交互结果。ServletConfig 是在 Servlet init 时由容器传过来的，那么 ServletConfig 到底是个什么对象呢？下图是 ServletConfig 和 ServletContext 在 Tomcat 容器中的类关系图。图 6. ServletConfig 在容器中的类关联图上图可以看出 StandardWrapper 和 StandardWrapperFacade 都实现了 ServletConfig 接口，而 StandardWrapperFacade 是 StandardWrapper 的门面类（Facade）。所以传给 Servlet 的是 StandardWrapperFacade 对象，这个类能够保证从 StandardWrapper 中拿到 ServletConfig 所规定的数据，而又不把 ServletConfig 不关心的数据暴露给 Servlet。同样，ServletContext 也与 ServletConfig 有类似的结构，Servlet 中能拿到的 ServletContext 的实际对象也是 ApplicationContextFacade 对象。ApplicationContextFacade 同样保证 ServletContext 只能从容器中拿到它该拿的数据。它们都起到对数据的封装作用，使用的都是门面设计模式（Facade Pattern）。通过 ServletContext 可以拿到 Context 容器中一些必要信息，比如应用的工作路径、容器支持的 Servlet 最小版本等。Servlet 中定义的两个 ServletRequest 和 ServletResponse，它们实际的对象又是什么呢？我们在创建自己的 Servlet 类时通常使用的都是 HttpServletRequest 和 HttpServletResponse，它们继承了 ServletRequest 和 ServletResponse。为何 Context 容器传过来的 ServletRequest、ServletResponse 可以被转化为 HttpServletRequest 和 HttpServletResponse 呢？图 7. Request 相关类结构图上图是 Tomcat 创建的 Request 和 Response 的类结构图。Tomcat 一接收到请求，首先将会创建 org.apache.coyote.Request 和 org.apache.coyote.Response。这两个类是 Tomcat 内部使用的描述一次请求和相应信息的类，它们是轻量级的类，作用就是在服务器接收到请求后，经过简单解析将这个请求快速地分配给后续线程去处理。所以它们的对象很小，很容易被 JVM 回收。接下来当交给一个用户线程去处理这个请求时，又创建 org.apache.catalina.connector.Request 和 org.apache.catalina.connector.Response 对象。这两个对象一直穿越整个 Servlet 容器直到要传给 Servlet。传给 Servlet 的是 Request 和 Response 的门面类 RequestFacade 和 ResponseFacade。这里使用门面模式与前面一样，都是基于同样的目的——封装容器中的数据。一次请求对应的 Request 和 Response 的类转化如下图所示：图 8. Request 和 Response 的转变过程Servlet 如何工作我们已经清楚了 Servlet 是如何被加载的、Servlet 是如何被初始化的，以及 Servlet 的体系结构，现在的问题就是它是如何被调用的。当用户从浏览器向服务器发起一个请求，通常会包含如下信息：http://hostname:port/contextpath/servletpath。hostname 和 port 是用来与服务器建立 TCP 连接，而后面的 URL 才是用来选择服务器中那个子容器服务用户的请求。那服务器是如何根据这个 URL 来达到正确的 Servlet 容器中的呢？Tomcat 7.0 中这件事很容易解决，因为这种映射工作有专门一个类来完成，这就是 org.apache.tomcat.util.http.mapper。这个类保存了 Tomcat 的 Container 容器中的所有子容器的信息。当 org.apache.catalina.connector.Request 类在进入 Container 容器之前，mapper 将会根据这次请求的 hostname 和 contextpath 将 host 和 context 容器设置到 Request 的 mappingData 属性中。所以当 Request 进入 Container 容器之前，它要访问哪个子容器这时就已经确定了。图 9. Request 的 Mapper 类关系图可能你有疑问，mapper 中怎么会有容器的完整关系？这要回到图 2 中第 19 步 MapperListener 类的初始化过程。下面是 MapperListener 的 init 方法代码：清单 5. MapperListener.initpublic void init() { findDefaultHost(); Engine engine = (Engine) connector.getService().getContainer(); engine.addContainerListener(this); Container[] conHosts = engine.findChildren(); for (Container conHost : conHosts) { Host host = (Host) conHost; if (!LifecycleState.NEW.equals(host.getState())) { host.addLifecycleListener(this); registerHost(host); } } }这段代码的作用就是将 MapperListener 类作为一个监听者加到整个 Container 容器中的每个子容器中。这样只要任何一个容器发生变化，MapperListener 都将会被通知，相应的保存容器关系的 MapperListener 的 mapper 属性也会修改。for 循环中就是将 host 及下面的子容器注册到 mapper 中。图 10. Request 在容器中的路由图上图描述了一次 Request 请求是如何达到最终的 Wrapper 容器的。我们现在知道了请求是如何达到正确的 Wrapper 容器，但是请求到达最终的 Servlet 还要完成一些步骤，必须要执行 Filter 链，以及要通知你在 web.xml 中定义的 Listener。接下来就要执行 Servlet 的 service 方法了。通常情况下，我们自己定义的 Servlet 并不是直接去实现 javax.servlet.Servlet 接口，而是去继承更简单的 HttpServlet 类或者 GenericServlet 类，我们可以有选择地覆盖相应方法去实现我们要完成的工作。Servlet 的确已经能够帮我们完成所有的工作了，但是现在的 Web 应用很少有直接将交互全部页面都用 Servlet 来实现，而是采用更加高效的 MVC 框架来实现。这些 MVC 框架基本的原理都是将所有的请求都映射到一个 Servlet，然后去实现 service 方法，这个方法也就是 MVC 框架的入口。当 Servlet 从 Servlet 容器中移除时，也就表明该 Servlet 的生命周期结束了。这时 Servlet 的 destroy 方法将被调用，做一些扫尾工作。Session 与 Cookie前面我们已经说明了 Servlet 如何被调用。我们基于 Servlet 来构建应用程序，那么我们能从 Servlet 获得哪些数据信息呢？Servlet 能够给我们提供两部分数据：一部分是在 Servlet 初始化时调用 init 方法时设置的 ServletConfig。这个类基本上含有了 Servlet 本身和 Servlet 所运行的 Servlet 容器中的基本信息。根据前面的介绍，ServletConfig 的实际对象是 StandardWrapperFacade，到底能获得哪些容器信息可以看看这类提供了哪些接口。还有一部分数据是由 ServletRequest 类提供，它的实际对象是 RequestFacade。从提供的方法中发现主要是描述这次请求的 HTTP 协议的信息。所以要掌握 Servlet 的工作方式，必须要很清楚 HTTP 协议。关于这一块还有一个让很多人迷惑的 Session 与 Cookie。Session 与 Cookie 不管是对 Java Web 的熟练使用者还是初学者来说，都是一个令人头疼的东西。Session 与 Cookie 的作用都是为了保持访问用户与后端服务器的交互状态。它们有各自的优点也有各自的缺陷。然而具有讽刺意味的是，它们的优点和使用场景又是矛盾的。例如使用 Cookie 来传递信息时，随着 Cookie 个数的增多和访问量的增加，它占用的网络带宽也很大。试想假如 Cookie 占用 200 个字节，如果一天的 PV 有几亿的时候，它要占用多少带宽。所以大访问量的时候希望用 Session，但是 Session 的致命弱点是不容易在多台服务器之间共享，所以这也限制了 Session 的使用。不管 Session 和 Cookie 有什么不足，我们还是要用它们。下面详细讲一下，Session 如何基于 Cookie 来工作。实际上有三种方式可以让 Session 正常工作：基于 URL Path Parameter：默认就支持。基于 Cookie：如果你没有修改 Context 容器的 cookies 标识的话，默认也是支持的。基于 SSL：默认不支持，只有 connector.getAttribute("SSLEnabled") 为 TRUE 时才支持。第一种情况下，当浏览器不支持 Cookie 功能时，浏览器会将用户的 SessionCookieName 重写到用户请求的 URL 参数中。它的传递格式如 /path/Servlet;name=value;name2=value2?Name3=value3，其中"Servlet;"后面的 K-V 对就是要传递的 Path Parameters，服务器会从这个 Path Parameters 中拿到用户配置的 SessionCookieName。关于这个 SessionCookieName，如果你在 web.xml 中配置 session-config 配置项的话，其 cookie-config 下的 name 属性就是这个 SessionCookieName 值。如果你没有配置 session-config 配置项，默认的 SessionCookieName 就是大家熟悉的"JSESSIONID"。接着 Request 根据这个 SessionCookieName 到 Parameters 拿到 Session ID 并设置到 request.setRequestedSessionId 中。请注意，如果客户端也支持 Cookie 的话，Tomcat 仍然会解析 Cookie 中的 Session ID，并会覆盖 URL 中的 Session ID。如果是第三种情况的话，将会根据 javax.servlet.request.ssl_session 属性值设置 Session ID。有了 Session ID，服务器端就可以创建 HttpSession 对象了。第一次触发是通过 request.getSession() 方法。如果当前的 Session ID 还没有对应的 HttpSession 对象，那么就创建一个新的，并将这个对象加到 org.apache.catalina.Manager 的 sessions 容器中保存。Manager 类将管理所有 Session 的生命周期，Session 过期将被回收，服务器关闭，Session 将被序列化到磁盘等。只要这个 HttpSession 对象存在，用户就可以根据 Session ID 来获取到这个对象，也就达到了状态的保持。图 11. Session 相关类图从图中可以看出，从 request.getSession 中获取的 HttpSession 对象实际上是 StandardSession 对象的门面对象，这与前面的 Request 和 Servlet 是一样的原理。下图是 Session 工作的时序图：图 12. Session 工作的时序图（查看大图）还有一点与 Session 关联的 Cookie 与其它 Cookie 没有什么不同，这个配置可以通过 web.xml 中的 session-config 配置项来指定。Servlet 中的 Listener整个 Tomcat 服务器中 Listener 使用得非常广泛，它是基于观察者模式设计的。Listener 的设计对开发 Servlet 应用程序提供了一种快捷的手段，能够方便地从另一个纵向维度控制程序和数据。目前 Servlet 中提供了 5 种两类事件的观察者接口，它们分别是：4 个 EventListeners 类型：ServletContextAttributeListenerServletRequestAttributeListenerServletRequestListenerHttpSessionAttributeListener2 个 LifecycleListeners 类型：ServletContextListenerHttpSessionListener如下图所示：图 13. Servlet 中的 Listener（查看大图）它们基本上涵盖了整个 Servlet 生命周期中你感兴趣的每种事件。这些 Listener 的实现类可以配置在 web.xml 中的 <listener> 标签中。当然也可以在应用程序中动态添加 Listener。需要注意的是，ServletContextListener 在容器启动之后就不能再添加新的，因为它所监听的事件已经不会再出现。掌握这些 Listener 的使用，能够让我们的程序设计得更加灵活。总结本文涉及的内容较多，要把每个细节都说清楚似乎不可能。本文试着从 Servlet 容器的启动到 Servlet 的初始化，以及 Servlet 的体系结构等这些环节中找出一些重点来讲述，目的是让读者有一个总体的完整结构图，同时也详细分析了其中的一些难点问题，希望对大家有所帮助。相关主题查看文章 Tomcat 系统架构与设计模式：了解 Tomcat 中容器的体系结构，基本的工作原理，以及 Tomcat 中使用的经典的设计模式介绍。Java Servlet 技术简介：介绍并解释 Servlet 是什么，它们是如何工作的，如何使用它们来创建您能够想象到的任意复杂度的 Web 应用程序，以及作为一名专业编程人员，您如何才能最有效地使用 Servlet。参考 Apache Tomcat 官网，了解 Tomcat 最新动态，以及开发人员参考手册。Servlet 最新规范：本文是基于 Servlet 3.0 规范讲解的，这里有最新的 Servlet 规范，以及 API 的介绍。HTTP 协议：W3C 关于 HTTP 协议的详细描述。IBM Developer（原 developerWorks）中国 Web development 专区：通过专门关于 Web 技术的文章和教程，扩展您在网站开发方面的技能。说明：本文内容基于 Tomcat 7 及 Servlet 3.0 规范编写，部分实现细节在不同版本容器中可能存在差异，但核心原理依然适用。部分外部链接可能因源站调整而无法访问，请以官方最新文档为准。
Servlet 3.0 新特性详解（servlet是单实例多线程的，线程池数量有限）
Servlet 3.0 新特性概述Servlet 3.0 作为 Java EE 6 规范体系中的一员，随 Java EE 6 规范一起发布。该版本在前一版本（Servlet 2.5）的基础上提供了若干新特性，旨在简化 Web 应用的开发和部署。其中有几项特性的引入让开发者感到非常兴奋，同时也获得了 Java 社区的一片赞誉之声：异步处理支持：有了该特性，Servlet 线程不再需要一直阻塞，直到业务处理完毕才能再输出响应，最后才结束该 Servlet 线程。在接收到请求之后，Servlet 线程可以将耗时的操作委派给另一个线程来完成，自己在不生成响应的情况下返回至容器。针对业务处理较耗时的情况，这将大大减少服务器资源的占用，并且提高并发处理速度。新增的注解支持：该版本新增了若干注解，用于简化 Servlet、过滤器（Filter）和监听器（Listener）的声明，这使得 web.xml 部署描述文件从该版本开始不再是必选的了。可插性支持：熟悉 Struts2 的开发者一定会对其通过插件的方式与包括 Spring 在内的各种常用框架的整合特性记忆犹新。将相应的插件封装成 JAR 包并放在类路径下，Struts2 运行时便能自动加载这些插件。现在 Servlet 3.0 提供了类似的特性，开发者可以通过插件的方式很方便地扩充已有 Web 应用的功能，而不需要修改原有的应用。下面我们将逐一讲解这些新特性。通过下面的学习，读者将能够明晰了解 Servlet 3.0 的变化，并能够顺利使用它进行日常的开发工作。异步处理支持Servlet 3.0 之前，一个普通 Servlet 的主要工作流程大致如下：首先，Servlet 接收到请求之后，可能需要对请求携带的数据进行一些预处理；接着，调用业务接口的某些方法，以完成业务处理；最后，根据处理的结果提交响应，Servlet 线程结束。其中第二步的业务处理通常是最耗时的，这主要体现在数据库操作，以及其它的跨网络调用等。在此过程中，Servlet 线程一直处于阻塞状态，直到业务方法执行完毕。在处理业务的过程中，Servlet 资源一直被占用而得不到释放，对于并发较大的应用，这有可能造成性能的瓶颈。对此，在以前通常是采用私有解决方案来提前结束 Servlet 线程，并及时释放资源。Servlet 3.0 针对这个问题做了开创性的工作。现在通过使用 Servlet 3.0 的异步处理支持，之前的 Servlet 处理流程可以调整为如下的过程：首先，Servlet 接收到请求之后，可能首先需要对请求携带的数据进行一些预处理；接着，Servlet 线程将请求转交给一个异步线程来执行业务处理，线程本身返回至容器，此时 Servlet 还没有生成响应数据；异步线程处理完业务以后，可以直接生成响应数据（异步线程拥有 ServletRequest 和 ServletResponse 对象的引用），或者将请求继续转发给其它 Servlet。如此一来，Servlet 线程不再是一直处于阻塞状态以等待业务逻辑的处理，而是启动异步线程之后可以立即返回。异步处理特性可以应用于 Servlet 和过滤器两种组件。由于异步处理的工作模式和普通工作模式在实现上有着本质的区别，因此默认情况下，Servlet 和过滤器并没有开启异步处理特性。如果希望使用该特性，则必须按照如下的方式启用：对于使用传统的部署描述文件 (web.xml) 配置 Servlet 和过滤器的情况，Servlet 3.0 为 <servlet> 和 <filter> 标签增加了 <async-supported> 子标签。该标签的默认取值为 false，要启用异步处理支持，则将其设为 true 即可。以 Servlet 为例，其配置方式如下所示：<servlet> <servlet-name>DemoServlet</servlet-name> <servlet-class>footmark.servlet.DemoServlet</servlet-class> <async-supported>true</async-supported> </servlet>对于使用 Servlet 3.0 提供的 @WebServlet 和 @WebFilter 进行 Servlet 或过滤器配置的情况，这两个注解都提供了 asyncSupported 属性。默认该属性的取值为 false，要启用异步处理支持，只需将该属性设置为 true 即可。以 @WebFilter 为例，其配置方式如下所示：@WebFilter(urlPatterns = "/demo", asyncSupported = true) public class DemoFilter implements Filter { // ... }一个简单的模拟异步处理的 Servlet 示例如下：@WebServlet(urlPatterns = "/demo", asyncSupported = true) public class AsyncDemoServlet extends HttpServlet { @Override public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { resp.setContentType("text/html;charset=UTF-8"); PrintWriter out = resp.getWriter(); out.println("进入 Servlet 的时间：" + new Date() + "."); out.flush(); // 在子线程中执行业务调用，并由其负责输出响应，主线程退出 AsyncContext ctx = req.startAsync(); new Thread(new Executor(ctx)).start(); out.println("结束 Servlet 的时间：" + new Date() + "."); out.flush(); } } public class Executor implements Runnable { private AsyncContext ctx = null; public Executor(AsyncContext ctx) { this.ctx = ctx; } public void run() { try { // 等待十秒钟，以模拟业务方法的执行 Thread.sleep(10000); PrintWriter out = ctx.getResponse().getWriter(); out.println("业务处理完毕的时间：" + new Date() + "."); out.flush(); ctx.complete(); } catch (Exception e) { e.printStackTrace(); } } }除此之外，Servlet 3.0 还为异步处理提供了一个监听器，使用 AsyncListener 接口表示。它可以监控如下四种事件：异步线程开始时，调用 AsyncListener 的 onStartAsync(AsyncEvent event) 方法；异步线程出错时，调用 AsyncListener 的 onError(AsyncEvent event) 方法；异步线程执行超时，则调用 AsyncListener 的 onTimeout(AsyncEvent event) 方法；异步执行完毕时，调用 AsyncListener 的 onComplete(AsyncEvent event) 方法。要注册一个 AsyncListener，只需将准备好的 AsyncListener 对象传递给 AsyncContext 对象的 addListener() 方法即可，如下所示：AsyncContext ctx = req.startAsync(); ctx.addListener(new AsyncListener() { public void onComplete(AsyncEvent asyncEvent) throws IOException { // 做一些清理工作或者其他 } // ... });新增的注解支持Servlet 3.0 的部署描述文件 web.xml 的顶层标签 <web-app> 有一个 metadata-complete 属性。该属性指定当前的部署描述文件是否是完全的。如果设置为 true，则容器在部署时将只依赖部署描述文件，忽略所有的注解（同时也会跳过 web-fragment.xml 的扫描，亦即禁用可插性支持，具体请看后文关于 可插性支持 的讲解）；如果不配置该属性，或者将其设置为 false，则表示启用注解支持（和可插性支持）。@WebServlet@WebServlet 用于将一个类声明为 Servlet，该注解将会在部署时被容器处理，容器将根据具体的属性配置将相应的类部署为 Servlet。该注解具有下表给出的一些常用属性（以下所有属性均为可选属性，但是 value 或者 urlPatterns 通常是必需的，且二者不能共存，如果同时指定，通常是忽略 value 的取值）：表 1. @WebServlet 主要属性列表属性名类型描述nameString指定 Servlet 的 name 属性，等价于 <servlet-name>。如果没有显式指定，则该 Servlet 的取值即为类的全限定名。valueString[]该属性等价于 urlPatterns 属性。两个属性不能同时使用。urlPatternsString[]指定一组 Servlet 的 URL 匹配模式。等价于 <url-pattern> 标签。loadOnStartupint指定 Servlet 的加载顺序，等价于 <load-on-startup> 标签。initParamsWebInitParam[]指定一组 Servlet 初始化参数，等价于 <init-param> 标签。asyncSupportedboolean声明 Servlet 是否支持异步操作模式，等价于 <async-supported> 标签。descriptionString该 Servlet 的描述信息，等价于 <description> 标签。displayNameString该 Servlet 的显示名，通常配合工具使用，等价于 <display-name> 标签。下面是一个简单的示例：@WebServlet(urlPatterns = {"/simple"}, asyncSupported = true, loadOnStartup = -1, name = "SimpleServlet", displayName = "ss", initParams = {@WebInitParam(name = "username", value = "tom")} ) public class SimpleServlet extends HttpServlet { // … }如此配置之后，就可以不必在 web.xml 中配置相应的 <servlet> 和 <servlet-mapping> 元素了，容器会在部署时根据指定的属性将该类发布为 Servlet。它的等价的 web.xml 配置形式如下：<servlet> <display-name>ss</display-name> <servlet-name>SimpleServlet</servlet-name> <servlet-class>footmark.servlet.SimpleServlet</servlet-class> <load-on-startup>-1</load-on-startup> <async-supported>true</async-supported> <init-param> <param-name>username</param-name> <param-value>tom</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>SimpleServlet</servlet-name> <url-pattern>/simple</url-pattern> </servlet-mapping>@WebInitParam该注解通常不单独使用，而是配合 @WebServlet 或者 @WebFilter 使用。它的作用是为 Servlet 或者过滤器指定初始化参数，这等价于 web.xml 中 <servlet> 和 <filter> 的 <init-param> 子标签。@WebInitParam 具有下表给出的一些常用属性：表 2. @WebInitParam 的常用属性属性名类型是否可选描述nameString否指定参数的名字，等价于 <param-name>。valueString否指定参数的值，等价于 <param-value>。descriptionString是关于参数的描述，等价于 <description>。@WebFilter@WebFilter 用于将一个类声明为过滤器，该注解将会在部署时被容器处理，容器将根据具体的属性配置将相应的类部署为过滤器。该注解具有下表给出的一些常用属性（以下所有属性均为可选属性，但是 value、urlPatterns、servletNames 三者必需至少包含一个，且 value 和 urlPatterns 不能共存，如果同时指定，通常忽略 value 的取值）：表 3. @WebFilter 的常用属性属性名类型描述filterNameString指定过滤器的 name 属性，等价于 <filter-name>valueString[]该属性等价于 urlPatterns 属性。但是两者不应该同时使用。urlPatternsString[]指定一组过滤器的 URL 匹配模式。等价于 <url-pattern> 标签。servletNamesString[]指定过滤器将应用于哪些 Servlet。取值是 @WebServlet 中的 name 属性的取值，或者是 web.xml 中 <servlet-name> 的取值。dispatcherTypesDispatcherType指定过滤器的转发模式。具体取值包括：ASYNC、ERROR、FORWARD、INCLUDE、REQUEST。initParamsWebInitParam[]指定一组过滤器初始化参数，等价于 <init-param> 标签。asyncSupportedboolean声明过滤器是否支持异步操作模式，等价于 <async-supported> 标签。descriptionString该过滤器的描述信息，等价于 <description> 标签。displayNameString该过滤器的显示名，通常配合工具使用，等价于 <display-name> 标签。下面是一个简单的示例：@WebFilter(servletNames = {"SimpleServlet"}, filterName = "SimpleFilter") public class LessThanSixFilter implements Filter { // ... }如此配置之后，就可以不必在 web.xml 中配置相应的 <filter> 和 <filter-mapping> 元素了，容器会在部署时根据指定的属性将该类发布为过滤器。它等价的 web.xml 中的配置形式为：<filter> <filter-name>SimpleFilter</filter-name> <filter-class>xxx</filter-class> </filter> <filter-mapping> <filter-name>SimpleFilter</filter-name> <servlet-name>SimpleServlet</servlet-name> </filter-mapping>@WebListener该注解用于将类声明为监听器，被 @WebListener 标注的类必须实现以下至少一个接口：ServletContextListenerServletContextAttributeListenerServletRequestListenerServletRequestAttributeListenerHttpSessionListenerHttpSessionAttributeListener该注解使用非常简单，其属性如下：表 4. @WebListener 的常用属性属性名类型是否可选描述valueString是该监听器的描述信息。一个简单示例如下：@WebListener("This is only a demo listener") public class SimpleListener implements ServletContextListener { // ... }如此，则不需要在 web.xml 中配置 <listener> 标签了。它等价的 web.xml 中的配置形式如下：<listener> <listener-class>footmark.servlet.SimpleListener</listener-class> </listener>@MultipartConfig该注解主要是为了辅助 Servlet 3.0 中 HttpServletRequest 提供的对上传文件的支持。该注解标注在 Servlet 上面，以表示该 Servlet 希望处理的请求的 MIME 类型是 multipart/form-data。另外，它还提供了若干属性用于简化对上传文件的处理。具体如下：表 5. @MultipartConfig 的常用属性属性名类型是否可选描述fileSizeThresholdint是当数据量大于该值时，内容将被写入文件。locationString是存放生成的文件地址。maxFileSizelong是允许上传的文件最大值。默认值为 -1，表示没有限制。maxRequestSizelong是针对该 multipart/form-data 请求的最大数量，默认值为 -1，表示没有限制。可插性支持如果说 3.0 版本新增的注解支持是为了简化 Servlet/ 过滤器 / 监听器的声明，从而使得 web.xml 变为可选配置，那么新增的可插性 (pluggability) 支持则将 Servlet 配置的灵活性提升到了新的高度。熟悉 Struts2 的开发者都知道，Struts2 通过插件的形式提供了对包括 Spring 在内的各种开发框架的支持，开发者甚至可以自己为 Struts2 开发插件，而 Servlet 的可插性支持正是基于这样的理念而产生的。使用该特性，现在我们可以在不修改已有 Web 应用的前提下，只需将按照一定格式打成的 JAR 包放到 WEB-INF/lib 目录下，即可实现新功能的扩充，不需要额外的配置。Servlet 3.0 引入了称之为"Web 模块部署描述符片段”的 web-fragment.xml 部署描述文件。该文件必须存放在 JAR 文件的 META-INF 目录下，该部署描述文件可以包含一切可以在 web.xml 中定义的内容。JAR 包通常放在 WEB-INF/lib 目录下，除此之外，所有该模块使用的资源，包括 class 文件、配置文件等，只需要能够被容器的类加载器链加载的路径上，比如 classes 目录等。现在，为一个 Web 应用增加一个 Servlet 配置有如下三种方式（过滤器、监听器与 Servlet 三者的配置都是等价的，故在此以 Servlet 配置为例进行讲述，过滤器和监听器具有与之非常类似的特性）：编写一个类继承自 HttpServlet，将该类放在 classes 目录下的对应包结构中，修改 web.xml，在其中增加一个 Servlet 声明。这是最原始的方式；编写一个类继承自 HttpServlet，并且在该类上使用 @WebServlet 注解将该类声明为 Servlet，将该类放在 classes 目录下的对应包结构中，无需修改 web.xml 文件。编写一个类继承自 HttpServlet，将该类打成 JAR 包，并且在 JAR 包的 META-INF 目录下放置一个 web-fragment.xml 文件，该文件中声明了相应的 Servlet 配置。web-fragment.xml 文件示例如下：<?xml version="1.0" encoding="UTF-8"?> <web-fragment xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd" metadata-complete="true"> <servlet> <servlet-name>fragment</servlet-name> <servlet-class>footmark.servlet.FragmentServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>fragment</servlet-name> <url-pattern>/fragment</url-pattern> </servlet-mapping> </web-fragment>从上面的示例可以看出，web-fragment.xml 与 web.xml 除了在头部声明的 XSD 引用不同之外，其主体配置与 web.xml 是完全一致的。由于一个 Web 应用中可以出现多个 web-fragment.xml 声明文件，加上一个 web.xml 文件，加载顺序问题便成了不得不面对的问题。Servlet 规范的专家组在设计的时候已经考虑到了这个问题，并定义了加载顺序的规则。web-fragment.xml 包含了两个可选的顶层标签，<name> 和 <ordering>。如果希望为当前的文件指定明确的加载顺序，通常需要使用这两个标签。<name> 主要用于标识当前的文件，而 <ordering> 则用于指定先后顺序。一个简单的示例如下：<web-fragment...> <name>FragmentA</name> <ordering> <after> <name>FragmentB</name> <name>FragmentC</name> </after> <before> <others/> </before> </ordering> ... </web-fragment>如上所示，<name> 标签的取值通常是被其它 web-fragment.xml 文件在定义先后顺序时引用的，在当前文件中一般用不着，它起着标识当前文件的作用。在 <ordering> 标签内部，我们可以定义当前 web-fragment.xml 文件与其他文件的相对位置关系，这主要通过 <ordering> 的 <after> 和 <before> 子标签来实现的。在这两个子标签内部可以通过 <name> 标签来指定相对应的文件。比如：<after> <name>FragmentB</name> <name>FragmentC</name> </after>以上片段则表示当前文件必须在 FragmentB 和 FragmentC 之后解析。<before> 的使用于此相同，它所表示的是当前文件必须早于 <before> 标签里所列出的 web-fragment.xml 文件。除了将所比较的文件通过 <name> 在 <after> 和 <before> 中列出之外，Servlet 还提供了一个简化的标签 <others/>。它表示除了当前文件之外的其他所有的 web-fragment.xml 文件。该标签的优先级要低于使用 <name> 明确指定的相对位置关系。ServletContext 的性能增强除了以上的新特性之外，ServletContext 对象的功能在新版本中也得到了增强。现在，该对象支持在运行时动态部署 Servlet、过滤器、监听器，以及为 Servlet 和过滤器增加 URL 映射等。以 Servlet 为例，过滤器与监听器与之类似。ServletContext 为动态配置 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()其中前三个方法的作用是相同的，只是参数类型不同而已；通过 createServlet() 方法创建的 Servlet，通常需要做一些自定义的配置，然后使用 addServlet() 方法来将其动态注册为一个可以用于服务的 Servlet。两个 getServletRegistration() 方法主要用于动态为 Servlet 增加映射信息，这等价于在 web.xml (抑或 web-fragment.xml) 中使用 <servlet-mapping> 标签为存在的 Servlet 增加映射信息。以上 ServletContext 新增的方法要么是在 ServletContextListener 的 contextInitialized 方法中调用，要么是在 ServletContainerInitializer 的 onStartup() 方法中调用。ServletContainerInitializer 也是 Servlet 3.0 新增的一个接口，容器在启动时使用 JAR 服务 API (JAR Service API) 来发现 ServletContainerInitializer 的实现类，并且容器将 WEB-INF/lib 目录下 JAR 包中的类都交给该类的 onStartup() 方法处理。我们通常需要在该实现类上使用 @HandlesTypes 注解来指定希望被处理的类，过滤掉不希望给 onStartup() 处理的类。HttpServletRequest 对文件上传的支持此前，对于处理上传文件的操作一直是让开发者头疼的问题，因为 Servlet 本身没有对此提供直接的支持，需要使用第三方框架来实现，而且使用起来也不够简单。如今这都成为了历史，Servlet 3.0 已经提供了这个功能，而且使用也非常简单。为此，HttpServletRequest 提供了两个方法用于从请求中解析出上传的文件：Part getPart(String name)Collection<Part> getParts()前者用于获取请求中给定 name 的文件，后者用于获取所有的文件。每一个文件用一个 javax.servlet.http.Part 对象来表示。该接口提供了处理文件的简易方法，比如 write()、delete() 等。至此，结合 HttpServletRequest 和 Part 来保存上传的文件变得非常简单，如下所示：Part photo = request.getPart("photo"); photo.write("/tmp/photo.jpg"); // 可以将两行代码简化为 request.getPart("photo").write("/tmp/photo.jpg") 一行。另外，开发者可以配合前面提到的 @MultipartConfig 注解来对上传操作进行一些自定义的配置，比如限制上传文件的大小，以及保存文件的路径等。其用法非常简单，故不在此赘述了。需要注意的是，如果请求的 MIME 类型不是 multipart/form-data，则不能使用上面的两个方法，否则将抛异常。总结Servlet 3.0 的众多新特性使得 Servlet 开发变得更加简单，尤其是异步处理特性和可插性支持的出现，必将对现有的 MVC 框架产生深远影响。虽然我们通常不会自己去用 Servlet 编写控制层代码，但是也许在下一个版本的 Struts 中，您就能切实感受到这些新特性带来的实质性改变。说明：本文基于 Servlet 3.0 (Java EE 6) 规范编写。随着技术发展，Java EE 已演进为 Jakarta EE，Servlet 规范版本也已更新（如 Servlet 5.0/6.0）。部分包名（javax.servlet 变为 jakarta.servlet）及特性在最新版本中可能有所调整，请在实际开发中参考对应版本的官方文档。相关主题JSR-000315 Java Servlet 3.0 规范：这里除了可以下载 Servlet 3.0 的规范文档，还可以了解关于与该规范相关的一些其他信息。GlassFish 项目主页：可以在这里下载 GlassFish V3 版本，这是 SUN 提供的 Java EE 6 规范的参考实现。"Servlet 2.2 的新特征"（developerWorks，2000 年 12 月）：讨论 Servlet 2.2 一些新的比较重要的特征，并给出了一些简单的例子来演示这些特征的用途及用法。developerWorks Java 技术专区：数百篇关于 Java 编程各个方面的文章。
概述Spring MVC 3.2 M1 引入了基于 Servlet 3.0 支持的异步请求处理功能。我将针对 Spring MVC 3.2 的新特性发布一系列文章，通过对背景知识和相关内容的充分介绍，帮助你理解为什么需要这些新特性，以及如何使用它们。这是系列文章中的第一篇。Spring MVC 3.2 的更新内容已经可以在 Spring Framework Github 中查看。你也可以将 http://repo.springsource.org/snapshot 配置到你的项目仓库中，以获取快照版本。在后续文章中，我将提供更多的源码示例链接。如果你想现在就尝试这些新特性，可以在 GitHub 上签出 spring-mvc-async 分支中的 spring-mvc-showcase 项目，并通过 提交记录 查看其中的更新信息。特性预览从编程模型的角度来看，新功能看似简单。现在，控制器（Controller）的方法可以返回 Callable 类型来完成异步请求处理。Spring MVC 3.2 会在 TaskExecutor 的帮助下，在一个独立的线程中调用这个返回值。示例代码如下：@RequestMapping(method = RequestMethod.POST) public Callable<String> processUpload(final MultipartFile file) { return new Callable<String>() { public Object call() throws Exception { // ... return "someView"; } }; }另一种方式是在控制器方法中返回 DeferredResult 类型（这是 Spring MVC 3.2 中的新成员），并在任意线程中完成异步处理。例如，对某个外部事件（如 JMS 消息、AMQP 消息、Redis 消息等）作出反应。下面是另一个代码片段：@RequestMapping("/quotes") @ResponseBody public DeferredResult quotes() { DeferredResult deferredResult = new DeferredResult(); // Add deferredResult to a Queue or a Map... return deferredResult; } // In some other thread.. // Set the return value on the deferredResult deferredResult.set(data);大家可能对上面的代码片段有很多疑问，我会在后续文章中给出更多的细节信息。在深入了解之前，我先介绍一些相关的技术背景知识。异步请求背景：长连接与线程模型当前一些网络应用最常用的异步处理方式就是长连接方式，例如运行一个缓慢的数据库查询、调用一个外部的 REST API 或者执行其他 I/O 操作。这些方式很快就会消耗光 Servlet 容器的线程池，影响程序的可扩展性。在一些情况下，你可能需要等待一个处理完成，例如发送邮件、执行数据库操作等。在这种“即发即忘”（fire-and-forget）情况下，你可以使用 Spring 注解 @Async 或设置 Spring Integration 事件并迅速返回，也许还可以返回一个用于确认的 ID，为后续的响应所用。这在 Spring MVC 3.2 之前就可以实现，并且可以避免请求死锁。对于需要在结果返回之前释放资源的其他情况，你需要先释放处理请求的 Servlet 容器线程来提高程序的可扩展性。为了实现这个功能，Servlet 3 允许一个 Servlet 在返回请求之后声明保持响应为打开状态，这样请求就可以在一个独立线程中完成。具体来说，可以调用 Servlet 3 中的 request.startAsync() 方法，并使用返回的 AsyncContext 在一个独立线程内继续写入（并最终完成）响应。这在客户端看来没有任何变化，请求仍然看起来像是其他 HTTP 标准的“请求—响应”一样。但是，在服务器端看来，异步请求处理可以让你以扩展性更好的方式处理请求。Servlet 3 规范定义了异步请求处理的标准流程。关于 Servlet 对异步请求处理的支持，还有很多内容，你可以找到 很多 示例 和 文章（注：部分外链可能因网络原因无法访问），但是上面总结的这些是所需要的最基本的概念。总结在下一篇文章中，我将介绍第二种异步请求处理的方式：客户端浏览器无延时的实时获取服务器的更新信息。过去已经发展出了很多方式实现这个功能，一些停留在 HTTP 标准的“请求—响应”的语义环境下，另一些则以更好的方式实现。原文地址：http://blog.springsource.org/2012/05/06/spring-mvc-3-2-preview-introducing-servlet-3-async-support/说明：本文基于 Spring MVC 3.2 预览版编写，属于历史技术文章。当前 Spring Framework 已演进至 6.x 版本，Servlet 规范也已更新至 6.0。文中提到的 Callable 与 DeferredResult 异步处理机制在现代版本中依然适用且更为成熟，但具体配置与依赖版本请以官方最新文档为准。