走进JavaWeb技术世界4Servlet-工作原理详解

37次阅读

共计 10522 个字符,预计需要花费 27 分钟才能阅读完成。

本系列文章将整理到我在 GitHub 上的《Java 面试指南》仓库,更多精彩内容请到我的仓库里查看

https://github.com/h2pl/Java-…

喜欢的话麻烦点下 Star 哈

文章首发于我的个人博客:

www.how2playlife.com

本文是微信公众号【Java 技术江湖】的《走进 JavaWeb 技术世界》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。

该系列博文会告诉你如何从入门到进阶,从 servlet 到框架,从 ssm 再到 SpringBoot,一步步地学习 JavaWeb 基础知识,并上手进行实战,接着了解 JavaWeb 项目中经常要使用的技术和组件,包括日志组件、Maven、Junit,等等内容,以便让你更完整地了解整个 Java Web 技术体系,形成自己的知识框架。

为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。

如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java 技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。

文末赠送 8000G 的 Java 架构师学习资料,需要的朋友可以到文末了解领取方式,资料包括 Java 基础、进阶、项目和架构师等免费学习资料,更有数据库、分布式、微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的 Java 学习指南、Java 程序员面试指南等干货资源)
<!– more –>

什么是 Servlet

Servlet 的作用是 为 Java 程序提供一个统一的 web 应用的规范,方便程序员统一的使用这种规范来编写程序,应用容器可以使用提供的规范来实现自己的特性。比如 tomcat 的代码和 jetty 的代码就不一样,但作为程序员你只需要了解 servlet 规范就可以从 request 中取值,你可以操作 session 等等。不用在意应用服务器底层的实现的差别而影响你的开发。

HTTP 协议只是一个规范,定义服务请求和响应的大致式样。Java servlet 类 将 HTTP 中那些低层的结构包装在 Java 类中,这些类所包含的便利方法使其在 Java 语言环境中更易于处理。

正如您正使用的特定 servlet 容器的配置文件中所定义的,当用户通过 URL 发出一个请求时,这些 Java servlet 类就将之转换成一个 HttpServletRequest,并发送给 URL 所指向的目标。当服务器端完成其工作时,Java 运行时环境(Java Runtime Environment)就将结果包装在一个 HttpServletResponse 中,然后将原 HTTP 响应送回给发出该请求的客户机。在与 Web 应用程序进行交互时,通常会发出多个请求并获得多个响应。所有这些都是在一个会话语境中,Java 语言将之包装在一个 HttpSession 对象中。在处理响应时,您可以访问该对象,并在创建响应时向其添加事件。它提供了一些跨请求的语境。

容器 (如 Tomcat)将为 servlet 管理运行时环境。您可以配置该容器,定制 J2EE 服务器的工作方式,以便将 servlet 暴露给外部世界。正如我们将看到的,通过该容器中的各种配置文件,您在 URL(由用户在浏览器中输入)与服务器端组件之间搭建了一座桥梁,这些组件将处理您需要该 URL 转换的请求。在运行应用程序时,该容器将 加载并初始化 servlet管理其生命周期

Servlet 体系结构

Servlet 顶级类关联图

Servlet

Servlet 的框架是由两个 Java 包组成的:javax.servlet 与 javax.servlet.http。在 javax.servlet 包中定义了所有的 Servlet 类都必须实现或者扩展的通用接口和类。在 javax.servlet.http 包中定义了采用 Http 协议通信的 HttpServlet 类。Servlet 的框架的核心是 javax.servlet.Servlet 接口,所有的 Servlet 都必须实现这个接口。

Servlet 接口

在 Servlet 接口中定义了 5 个方法:

1\. init(ServletConfig)方法:负责初始化 Servlet 对象,在 Servlet 的生命周期中,该方法执行一次;该方法执行在单线程的环境下,因此开发者不用考虑线程安全的问题;2\. service(ServletRequest req,ServletResponse res)方法:负责响应客户的请求;为了提高效率,Servlet 规范要求一个 Servlet 实例必须能够同时服务于多个客户端请求,即 service()方法运行在多线程的环境下,Servlet 开发者必须保证该方法的线程安全性;3\. destroy()方法:当 Servlet 对象退出生命周期时,负责释放占用的资源;4\. getServletInfo:就是字面意思,返回 Servlet 的描述;5\. getServletConfig:这个方法返回由 Servlet 容器传给 init 方法的 ServletConfig。

ServletRequest & ServletResponse

对于每一个 HTTP 请求,servlet 容器会创建一个封装了 HTTP 请求的 ServletRequest 实例传递给 servlet 的 service 方法,ServletResponse 则表示一个 Servlet 响应,其隐藏了将响应发给浏览器的复杂性。通过 ServletRequest 的方法你可以获取一些请求相关的参数,而 ServletResponse 则可以将设置一些返回参数信息,并且设置返回内容。

ServletConfig

ServletConfig 封装可以通过 @WebServlet 或者 web.xml 传给一个 Servlet 的配置信息,以这种方式传递的每一条信息都称做初始化信息,初始化信息就是一个个 K - V 键值对。为了从一个 Servlet 内部获取某个初始参数的值,init 方法中调用 ServletConfig 的 getinitParameter 方法或 getinitParameterNames 方法获取,除此之外,还可以通过 getServletContext 获取 ServletContext 对象。

ServletContext

ServletContext 是代表了 Servlet 应用程序。每个 Web 应用程序只有一个 context。在分布式环境中,一个应用程序同时部署到多个容器中,并且每台 Java 虚拟机都有一个 ServletContext 对象。有了 ServletContext 对象后,就可以共享能通过应用程序的所有资源访问的信息,促进 Web 对象的动态注册,共享的信息通过一个内部 Map 中的对象保存在 ServiceContext 中来实现。保存在 ServletContext 中的对象称作属性。操作属性的方法:

GenericServlet

前面编写的 Servlet 应用中通过实现 Servlet 接口来编写 Servlet,但是我们每次都必须为 Servlet 中的所有方法都提供实现,还需要将 ServletConfig 对象保存到一个类级别的变量中,GenericServlet 抽象类就是为了为我们省略一些模板代码,实现了 Servlet 和 ServletConfig,完成了一下几个工作:

将 init 方法中的 ServletConfig 赋给一个类级变量,使的可以通过 getServletConfig 来获取。

public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();}

同时为避免覆盖 init 方法后在子类中必须调用 super.init(servletConfig),GenericServlet 还提供了一个不带参数的 init 方法,当 ServletConfig 赋值完成就会被第带参数的 init 方法调用。这样就可以通过覆盖不带参数的 init 方法编写初始化代码,而 ServletConfig 实例依然得以保存

为 Servlet 接口中的所有方法提供默认实现。

提供方法来包装 ServletConfig 中的方法。

HTTPServlet

在编写 Servlet 应用程序时,大多数都要用到 HTTP,也就是说可以利用 HTTP 提供的特性,javax.servlet.http 包含了编写 Servlet 应用程序的类和接口,其中很多覆盖了 javax.servlet 中的类型,我们自己在编写应用时大多时候也是继承的 HttpServlet。

Servlet 工作原理

当 Web 服务器接收到一个 HTTP 请求时,它会先判断请求内容——如果是静态网页数据,Web 服务器将会自行处理,然后产生响应信息;如果牵涉到动态数据,Web 服务器会将请求转交给 Servlet 容器。此时 Servlet 容器会找到对应的处理该请求的 Servlet 实例来处理,结果会送回 Web 服务器,再由 Web 服务器传回用户端。

针对同一个 Servlet,Servlet 容器会在第一次收到 http 请求时建立一个 Servlet 实例,然后启动一个线程。第二次收到 http 请求时,Servlet 容器无须建立相同的 Servlet 实例,而是启动第二个线程来服务客户端请求。所以多线程方式不但可以提高 Web 应用程序的执行效率,也可以降低 Web 服务器的系统负担。

Web 服务器工作流程

接着我们描述一下 Tomcat 与 Servlet 是如何工作的,首先看下面的时序图:

Servlet 工作原理时序图

  1. Web Client 向 Servlet 容器(Tomcat)发出 Http 请求;
  2. Servlet 容器接收 Web Client 的请求;
  3. Servlet 容器创建一个 HttpRequest 对象,将 Web Client 请求的信息封装到这个对象中;
  4. Servlet 容器创建一个 HttpResponse 对象;
  5. Servlet 容器调用 HttpServlet 对象的 service 方法,把 HttpRequest 对象与 HttpResponse 对象作为参数传给 HttpServlet 对象;
  6. HttpServlet 调用 HttpRequest 对象的有关方法,获取 Http 请求信息;
  7. HttpServlet 调用 HttpResponse 对象的有关方法,生成响应数据;
  8. Servlet 容器把 HttpServlet 的响应结果传给 Web Client;

Servlet 生命周期

在 Servlet 接口中定义了 5 个方法,其中 3 个方法代表了 Servlet 的生命周期:

1\. init(ServletConfig)方法:负责初始化 Servlet 对象,在 Servlet 的生命周期中,该方法执行一次;该方法执行在单线程的环境下,因此开发者不用考虑线程安全的问题;2\. service(ServletRequest req,ServletResponse res)方法:负责响应客户的请求;为了提高效率,Servlet 规范要求一个 Servlet 实例必须能够同时服务于多个客户端请求,即 service()方法运行在多线程的环境下,Servlet 开发者必须保证该方法的线程安全性;3\. destroy()方法:当 Servlet 对象退出生命周期时,负责释放占用的资源;

编程注意事项说明:

  1. 当 Server Thread 线程执行 Servlet 实例的 init()方法时,所有的 Client Service Thread 线程都不能执行该实例的 service()方法,更没有线程能够执行该实例的 destroy()方法,因此 Servlet 的 init()方法是工作在单线程的环境下,开发者不必考虑任何线程安全的问题。
  2. 当服务器接收到来自客户端的多个请求时,服务器会在单独的 Client Service Thread 线程中执行 Servlet 实例的 service()方法服务于每个客户端。此时会有多个线程同时执行同一个 Servlet 实例的 service()方法,因此必须考虑线程安全的问题。
  3. 虽然 service()方法运行在多线程的环境下,并不一定要同步该方法。而是要看这个方法在执行过程中访问的资源类型及对资源的访问方式。分析如下:
1\. 如果 service()方法没有访问 Servlet 的成员变量也没有访问全局的资源比如静态变量、文件、数据库连接等,而是只使用了当前线程自己的资源,比如非指向全局资源的临时变量、request 和 response 对象等。该方法本身就是线程安全的,不必进行任何的同步控制。2\. 如果 service()方法访问了 Servlet 的成员变量,但是对该变量的操作是只读操作,该方法本身就是线程安全的,不必进行任何的同步控制。3\. 如果 service()方法访问了 Servlet 的成员变量,并且对该变量的操作既有读又有写,通常需要加上同步控制语句。4\. 如果 service()方法访问了全局的静态变量,如果同一时刻系统中也可能有其它线程访问该静态变量,如果既有读也有写的操作,通常需要加上同步控制语句。5\. 如果 service()方法访问了全局的资源,比如文件、数据库连接等,通常需要加上同步控制语句。

在创建一个 Java servlet 时,一般需要子类 HttpServlet。该类中的方法允许您访问请求和响应包装器(wrapper),您可以用这个包装器来处理请求和创建响应。Servlet 的生命周期,简单的概括这就分为四步:

Servlet 类加载 ---> 实例化 ---> 服务 ---> 销毁;

Servlet 生命周期

创建 Servlet 对象的时机:

  1. 默认情况下,在 Servlet 容器启动后:客户首次向 Servlet 发出请求,Servlet 容器会判断内存中是否存在指定的 Servlet 对象,如果没有则创建它,然后根据客户的请求创建 HttpRequest、HttpResponse 对象,从而调用 Servlet 对象的 service 方法;
  2. Servlet 容器启动时:当 web.xml 文件中如果 <servlet> 元素中指定了 <load-on-startup> 子元素时,Servlet 容器在启动 web 服务器时,将按照顺序创建并初始化 Servlet 对象;
  3. Servlet 的类文件被更新后,重新创建 Servlet。Servlet 容器在启动时自动创建 Servlet,这是由在 web.xml 文件中为 Servlet 设置的 <load-on-startup> 属性决定的。从中我们也能看到同一个类型的 Servlet 对象在 Servlet 容器中以单例的形式存在;

注意:在 web.xml 文件中,某些 Servlet 只有 <serlvet> 元素,没有 <servlet-mapping> 元素,这样我们无法通过 url 的方式访问这些 Servlet,这种 Servlet 通常会在 <servlet> 元素中配置一个 <load-on-startup> 子元素,让容器在启动的时候自动加载这些 Servlet 并调用 init(ServletConfig config)方法来初始化该 Servlet。其中方法参数 config 中包含了 Servlet 的配置信息,比如初始化参数,该对象由服务器创建。

销毁 Servlet 对象的时机:

Servlet 容器停止或者重新启动:Servlet 容器调用 Servlet 对象的 destroy 方法来释放资源。以上所讲的就是 Servlet 对象的生命周期。那么 Servlet 容器如何知道创建哪一个 Servlet 对象?Servlet 对象如何配置?实际上这些信息是通过读取 web.xml 配置文件来实现的。

<servlet>
    <!-- Servlet 对象的名称 -->
    <servlet-name>action<servlet-name>
    <!-- 创建 Servlet 对象所要调用的类 -->
    <servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
    <init-param>
        <!-- 参数名称 -->
        <param-name>config</param-name>
        <!-- 参数值 -->
        <param-value>/WEB-INF/struts-config.xml</param-value>
    </init-param>
    <init-param>
        <param-name>detail</param-name>
        <param-value>2</param-value>
    </init-param>
    <init-param>
        <param-name>debug</param-name>
        <param-value>2</param-value>
    </init-param>
    <!-- Servlet 容器启动时加载 Servlet 对象的顺序 -->
    <load-on-startup>2</load-on-startup>
</servlet>
<!-- 要与 servlet 中的 servlet-name 配置节内容对应 -->
<servlet-mapping>
    <servlet-name>action</servlet-name>
    <!-- 客户访问的 Servlet 的相对 URL 路径 -->
    <url-pattern>*.do</url-pattern>
</servlet-mapping>

当 Servlet 容器启动的时候读取 <servlet> 配置节信息,根据 <servlet-class> 配置节信息创建 Servlet 对象,同时根据 <init-param> 配置节信息创建 HttpServletConfig 对象,然后执行 Servlet 对象的 init 方法,并且根据 <load-on-startup> 配置节信息来决定创建 Servlet 对象的顺序,如果此配置节信息为负数或者没有配置,那么在 Servlet 容器启动时,将不加载此 Servlet 对象。当客户访问 Servlet 容器时,Servlet 容器根据客户访问的 URL 地址,通过 <servlet-mapping> 配置节中的 <url-pattern> 配置节信息找到指定的 Servlet 对象,并调用此 Servlet 对象的 service 方法。

在整个 Servlet 的生命周期过程中,创建 Servlet 实例、调用实例的 init()和 destroy()方法都只进行一次 ,当初始化完成后,Servlet 容器会将该实例保存在内存中,通过调用它的 service() 方法,为接收到的请求服务。下面给出 Servlet 整个生命周期过程的 UML 序列图,如图所示:

Servlet 生命周期

如果需要让 Servlet 容器在启动时即加载 Servlet,可以在 web.xml 文件中配置 <load-on-startup> 元素。

Servlet 中的 Listener

Listener 使用的非常广泛,它是基于观察者模式设计的,Listener 的设计对开发 Servlet 应用程序提供了一种快捷的手段,能够方便的从另一个纵向维度控制程序和数据。目前 Servlet 中提供了 5 种两类事件的观察者接口,它们分别是:4 个 EventListeners 类型的,ServletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionAttributeListener 和 2 个 LifecycleListeners 类型的,ServletContextListener、HttpSessionListener。如下图所示:

Servlet 中的 Listener

它们基本上涵盖了整个 Servlet 生命周期中,你感兴趣的每种事件。这些 Listener 的实现类可以配置在 web.xml 中的 <listener> 标签中。当然也可以在应用程序中动态添加 Listener,需要注意的是 ServletContextListener 在容器启动之后就不能再添加新的,因为它所监听的事件已经不会再出现。掌握这些 Listener 的使用,能够让我们的程序设计的更加灵活。

Cookie 与 Session

Servlet 能够给我们提供两部分数据,一个是在 Servlet 初始化时调用 init 方法时设置的 ServletConfig,这个类基本上含有了 Servlet 本身和 Servlet 所运行的 Servlet 容器中的基本信息。还有一部分数据是由 ServletRequest 类提供,从提供的方法中发现主要是描述这次请求的 HTTP 协议的信息。关于这一块还有一个让很多人迷惑的 Session 与 Cookie。

Session 与 Cookie 的作用都是为了保持访问用户与后端服务器的交互状态。它们有各自的优点也有各自的缺陷。然而具有讽刺意味的是它们优点和它们的使用场景又是矛盾的,例如使用 Cookie 来传递信息时,随着 Cookie 个数的增多和访问量的增加,它占用的网络带宽也也会越来越大。所以大访问量的时候希望用 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&JSESSIONID=value3

接着 Request 根据这个 JSESSIONID 参数拿到 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 来获取到这个对象,也就达到了状态的保持。

Session 相关类图

上从图中可以看出从 request.getSession 中获取的 HttpSession 对象实际上是 StandardSession 对象的门面对象,这与前面的 Request 和 Servlet 是一样的原理。下图是 Session 工作的时序图:

Session 工作的时序图

还有一点与 Session 关联的 Cookie 与其它 Cookie 没有什么不同,这个配置的配置可以通过 web.xml 中的 session-config 配置项来指定。

参考文章

https://segmentfault.com/a/11…

https://www.cnblogs.com/hysum…

http://c.biancheng.net/view/9…

https://www.runoob.com/

https://blog.csdn.net/android…

微信公众号

个人公众号:黄小斜

黄小斜是跨考软件工程的 985 硕士,自学 Java 两年,拿到了 BAT 等近十家大厂 offer,从技术小白成长为阿里工程师。

作者专注于 JAVA 后端技术栈,热衷于分享程序员干货、学习经验、求职心得和程序人生,目前黄小斜的 CSDN 博客有百万 + 访问量,知乎粉丝 2W+,全网已有 10W+ 读者。

黄小斜是一个斜杠青年,坚持学习和写作,相信终身学习的力量,希望和更多的程序员交朋友,一起进步和成长!

原创电子书:
关注危险公众号【黄小斜】后回复【原创电子书】即可领取我原创的电子书《菜鸟程序员修炼手册:从技术小白到阿里巴巴 Java 工程师》这份电子书总结了我 2 年的 Java 学习之路,包括学习方法、技术总结、求职经验和面试技巧等内容,已经帮助很多的程序员拿到了心仪的 offer!

程序员 3T 技术学习资源: 一些程序员学习技术的资源大礼包,关注公众号后,后台回复关键字 “资料” 即可免费无套路获取,包括 Java、python、C++、大数据、机器学习、前端、移动端等方向的技术资料。

技术公众号:Java 技术江湖

如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的微信公众号【Java 技术江湖】

这是一位阿里 Java 工程师的技术小站。作者黄小斜,专注 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点 Docker、ELK,同时也分享技术干货和学习经验,致力于 Java 全栈开发!

(关注公众号后回复”Java“即可领取 Java 基础、进阶、项目和架构师等免费学习资料,更有数据库、分布式、微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的 Java 学习指南、Java 程序员面试指南等干货资源)

Java 工程师必备学习资源: 一些 Java 工程师常用学习资源,关注公众号后,后台回复关键字 “Java” 即可免费无套路获取。

正文完
 0

走进JavaWeb技术世界4Servlet-工作原理详解

37次阅读

共计 17566 个字符,预计需要花费 44 分钟才能阅读完成。

微信公众号【黄小斜】大厂程序员,互联网行业新知,终身学习践行者。关注后回复「Java」、「Python」、「C++」、「大数据」、「机器学习」、「算法」、「AI」、「Android」、「前端」、「iOS」、「考研」、「BAT」、「校招」、「笔试」、「面试」、「面经」、「计算机基础」、「LeetCode」等关键字可以获取对应的免费学习资料。

                     

从本篇开始,正式进入 Java 核心技术内容的学习,首先介绍的就是 Java web 应用的核心规范 servlet

转自:https://www.ibm.com/developer…

Servlet 容器的启动过程

Tomcat7 也开始支持嵌入式功能,增加了一个启动类 org.apache.catalina.startup.Tomcat。创建一个实例对象并调用 start 方法就可以很容易启动 Tomcat,我们还可以通过这个对象来增加和修改 Tomcat 的配置参数,如可以动态增加 Context、Servlet 等。下面我们就利用这个 Tomcat 类来管理新增的一个 Context 容器,我们就选择 Tomcat7 自带的 examples Web 工程,并看看它是如何加到这个 Context 容器中的。

清单 2 . 给 Tomcat 增加一个 Web 工程

1234567 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("<h1>Hello World!</h1`>") > 0);

清单 1 的代码是创建一个 Tomcat 实例并新增一个 Web 应用,然后启动 Tomcat 并调用其中的一个 HelloWorldExample Servlet,看有没有正确返回预期的数据。

Tomcat 的 addWebapp 方法的代码如下:

清单 3 .Tomcat.addWebapp

1234567891011121314151617181920 public 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/catalin/startup/NO_DEFAULT_XML");if (host == null) {getHost().addChild(ctx);} else {host.addChild(ctx);}return ctx;}`

前面已经介绍了一个 Web 应用对应一个 Context 容器,也就是 Servlet 运行时的 Servlet 容器,添加一个 Web 应用时将会创建一个 StandardContext 容器,并且给这个 Context 容器设置必要的参数,url 和 path 分别代表这个应用在 Tomcat 中的访问路径和这个应用实际的物理路径,这个两个参数与清单 1 中的两个参数是一致的。其中最重要的一个配置是 ContextConfig,这个类将会负责整个 Web 应用配置的解析工作,后面将会详细介绍。最后将这个 Context 容器加到父容器 Host 中。

接下去将会调用 Tomcat 的 start 方法启动 Tomcat,如果你清楚 Tomcat 的系统架构,你会容易理解 Tomcat 的启动逻辑,Tomcat 的启动逻辑是基于观察者模式设计的,所有的容器都会继承 Lifecycle 接口,它管理者容器的整个生命周期,所有容器的的修改和状态的改变都会由它去通知已经注册的观察者(Listener),关于这个设计模式可以参考《Tomcat 的系统架构与设计模式,第二部分:设计模式》。Tomcat 启动的时序图可以用图 2 表示。

图 2. Tomcat 主要类的启动时序图(查看大图)

上图描述了 Tomcat 启动过程中,主要类之间的时序关系,下面我们将会重点关注添加 examples 应用所对应的 StandardContext 容器的启动过程。

当 Context 容器初始化状态设为 init 时,添加在 Contex 容器的 Listener 将会被调用。ContextConfig 继承了 LifecycleListener 接口,它是在调用清单 3 时被加入到 StandardContext 容器中。ContextConfig 类会负责整个 Web 应用的配置文件的解析工作。

ContextConfig 的 init 方法将会主要完成以下工作:

  1. 创建用于解析 xml 配置文件的 contextDigester 对象
  2. 读取默认 context.xml 配置文件,如果存在解析它
  3. 读取默认 Host 配置文件,如果存在解析它
  4. 读取默认 Context 自身的配置文件,如果存在解析它
  5. 设置 Context 的 DocBase

ContextConfig 的 init 方法完成后,Context 容器的会执行 startInternal 方法,这个方法启动逻辑比较复杂,主要包括如下几个部分:

  1. 创建读取资源文件的对象
  2. 创建 ClassLoader 对象
  3. 设置应用的工作目录
  4. 启动相关的辅助类如:logger、realm、resources 等
  5. 修改启动状态,通知感兴趣的观察者(Web 应用的配置)
  6. 子容器的初始化
  7. 获取 ServletContext 并设置必要的参数
  8. 初始化“load on startup”的 Servlet

Web 应用的初始化工作

Web 应用的初始化工作是在 ContextConfig 的 configureStart 方法中实现的,应用的初始化主要是要解析 web.xml 文件,这个文件描述了一个 Web 应用的关键信息,也是一个 Web 应用的入口。

Tomcat 首先会找 globalWebXml 这个文件的搜索路径是在 engine 的工作目录下寻找以下两个文件中的任一个 org/apache/catalin/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 对象中。如果当前应用支持 Servlet3.0,解析还将完成额外 9 项工作,这个额外的 9 项工作主要是为 Servlet3.0 新增的特性,包括 jar 包中的 META-INF/web-fragment.xml 的解析以及对 annotations 的支持。

接下去将会将 WebXml 对象中的属性设置到 Context 容器中,这里包括创建 Servlet 对象、filter、listener 等等。这段代码在 WebXml 的 configureContext 方法中。下面是解析 Servlet 的代码片段:

清单 4. 创建 Wrapper 实例

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748 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(newMultipartConfigElement(multipartdef.getLocation(),Long.parseLong(multipartdef.getMaxFileSize()),Long.parseLong(multipartdef.getMaxRequestSize()),Integer.parseInt(multipartdef.getFileSizeThreshold()))); } else {wrapper.setMultipartConfigElement(newMultipartConfigElement(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 门面类。所以传给 Servlet 的是 StandardWrapperFacade 对象,这个类能够保证从 StandardWrapper 中拿到 ServletConfig 所规定的数据,而又不把 ServletConfig 不关心的数据暴露给 Servlet。

同样 ServletContext 也与 ServletConfig 有类似的结构,Servlet 中能拿到的 ServletContext 的实际对象也是 ApplicationContextFacade 对象。ApplicationContextFacade 同样保证 ServletContex 只能从容器中拿到它该拿的数据,它们都起到对数据的封装作用,它们使用的都是门面设计模式。

通过 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 和 RequestFacade,这里使用门面模式与前面一样都是基于同样的目的——封装容器中的数据。一次请求对应的 Request 和 Response 的类转化如下图所示:

图 8.Request 和 Response 的转变过程

Servlet 如何工作

我们已经清楚了 Servlet 是如何被加载的、Servlet 是如何被初始化的,以及 Servlet 的体系结构,现在的问题就是它是如何被调用的。

当用户从浏览器向服务器发起一个请求,通常会包含如下信息:http://hostname: port /contextpath/servletpath,hostname 和 port 是用来与服务器建立 TCP 连接,而后面的 URL 才是用来选择服务器中那个子容器服务用户的请求。那服务器是如何根据这个 URL 来达到正确的 Servlet 容器中的呢?

Tomcat7.0 中这件事很容易解决,因为这种映射工作有专门一个类来完成的,这个就是 org.apache.tomcat.util.http.mapper,这个类保存了 Tomcat 的 Container 容器中的所有子容器的信息,当 org.apache.catalina.connector. Request 类在进入 Container 容器之前,mapper 将会根据这次请求的 hostnane 和 contextpath 将 host 和 context 容器设置到 Request 的 mappingData 属性中。所以当 Request 进入 Container 容器之前,它要访问那个子容器这时就已经确定了。

图 9.Request 的 Mapper 类关系图

可能你有疑问,mapper 中怎么会有容器的完整关系,这要回到图 2 中 19 步 MapperListener 类的初始化过程,下面是 MapperListener 的 init 方法代码 :

清单 5. MapperListener.init

12345678910111213 public 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 正常工作:

  1. 基于 URL Path Parameter,默认就支持
  2. 基于 Cookie,如果你没有修改 Context 容器个 cookies 标识的话,默认也是支持的
  3. 基于 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 类型的,ServletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionAttributeListener 和 2 个 LifecycleListeners 类型的,ServletContextListener、HttpSessionListener。如下图所示:

图 13.Servlet 中的 Listener(查看大图)

它们基本上涵盖了整个 Servlet 生命周期中,你感兴趣的每种事件。这些 Listener 的实现类可以配置在 web.xml 中的 <listener> 标签中。当然也可以在应用程序中动态添加 Listener,需要注意的是 ServletContextListener 在容器启动之后就不能再添加新的,因为它所监听的事件已经不会再出现。掌握这些 Listener 的使用,能够让我们的程序设计的更加灵活。

实战 Servlet、Filter 和 Listener

一个例子

我们先看一段简单的 servlet 请求响应的代码片段

//HelloServlet 类
public class HelloServlet extends HttpServlet{

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String name  = req.getParameter("name");
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter printWriter = resp.getWriter();
        printWriter.write("<html><head><head><body><h1>");
        printWriter.write("Hello"+name);
        printWriter.write("</h1></body></html>");
        printWriter.close();}
}

然后在 web.xml 注册此 servlet

<?xml version="1.0" encoding="utf-8"?>
<web-app version="3.0"
        xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

    <display-name>wthfeng 的 mvc 练习项目 </display-name>
    <servlet>
        <servlet-name>hello</servlet-name>
        <servlet-class>com.wthfeng.mymvc.servlet.HelloServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
  <servlet-mapping>
      <servlet-name>hello</servlet-name>
      <url-pattern>/hello</url-pattern>
  </servlet-mapping>
</web-app>

启动项目,效果如下图所示

这样我们就写完了一个简单的 servlet,有关这个例子,我们需要知道:

  1. 编写 Servlet 必须继承 HttpServletGenericServlet或直接实现 Servlet 接口。HttpServlet已经为我们封装了有关 http 请求的相关参数,一般情况下我们直接继承此类即可。
  2. 此 servlet 重写了 doGet 方法,也就是它可以响应 /myprojectName/hello?name=xxx 的请求,并在页面打印xxx 的内容。

Servlet 的创建和初始化

好了,这些逻辑符合上面所说的,浏览器发出请求 /hello,一个名为hello 的 servlet 被命中,接受请求处理后返回。这里我们再深入一些,为什么要把 servlet 的定义在 web.xml 中?web.xml 又和 servlet 容器有什么关系?

web.xml是 web 项目的入口文件 。在项目启动过程中,servlet 容器(如 tomcat)会读取web.xml 并进行相应配置以初始化该项目。

具体容器启动流程如下(以 tomcat 为例):

  1. 先解析 tomcat 路径下的 conf/web.xml 等 web.xml 文件, 它们是全局 web 配置文件,做一些基本配置工作。如注册了 default、jsp 等 servlet。
  2. 解析 web.xml,将其各个配置项(包括 servlet、filter、listener)经处理包装后设在 Tomcat 的 Context 容器中,一个 Web 应用对应一个 Context 容器。
  3. 创建 servlet 并初始化。load-on-startup大于 1 的 servlet 会在此时初始化。初始化 servlet 就是调用 servlet 的 init 方法。注意:若不设置 此 值,init() 方法只在第一次 HTTP 请求命中时才被调用。此时 servlet 容器就算启动了。

简而言之就是,web.xml是项目和 servlet 容器(服务器)关联的桥梁。通过在 web.xml 注册 servlet、filter、listener 等,使得项目具有处理特定请求的功能。

2. filter 和 listener

在有关 web servlet 的配置中,常能在 web.xml 看到 filter、listener 的配置。实际上,filter 是过滤器,listener 是监听器。这两项配置都是 servlet 中的重要部分。我们一一来看。

fliter(过滤器)

filter 可用于拦截请求,在请求处理前进行一些预处理工作,一般用于处理编码问题,记录日志等。Filter 类需实现 javax.servlet.Filter 接口。其中有 3 个方法init()、、dofilter()、destroy(),分别用于过滤器的初始化、过滤处理、销毁。

先来个例子

public class MyFilter implements Filter {

    private String param; 

    // 初始化方法,在容器启动时调用
    public void init(FilterConfig filterConfig) throws ServletException {
        // 做一些初始化操作
        param = filterConfig.getInitParameter("myParam");
        System.out.println("filter:"+param);
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 处理请求
        chain.doFilter(request,response); // 调用下一个过滤器
        // 处理响应
    }

    @Override
    public void destroy() {
      // 在 servlet 销毁后销毁
      // 做一些销毁后的善后工作
    }
}

在 web.xml中添加

     <filter>
        <filter-name>myFilter</filter-name>
        <filter-class>com.wthfeng.mymvc.filter.MyFilter</filter-class>
        <init-param>
            <param-name>myParam</param-name>
            <param-value>myValue</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>myFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

配置后 filter 就会拦截所有 servlet 请求,需注意:

  1. 所有的 filter 在容器启动时即初始化。
  2. filter 的调用顺序为在 web.xml 中的定义顺序。若多余一个会形成过滤链依次处理。

Listener(监听器)

servlet 监听器用于监听 servlet 容器中事件变化,当指定事件变化时,会触发注册该事件的监听器。监听器基于观察者模式。

Servlet 监听器分为 3 类,分别用于监听ServletContent(Servlet 上下文)、HttpSession(Session),HttpRequest(Request)。

主要有以下几个类:

ServletContextListener // 监听 Servlet 容器创建销毁
ServletContextAttributeListener  // 监听 Servlet 容器级别属性的添加及删除

HttpSessionListener  // 监听 Session 创建销毁
HttpSessionAttributeListener // 监听 Session 属性创建删除

ServletRequestListener  // 监听请求创建及销毁
ServletRequestAttributeListener  // 监听请求属性变化

如我们常见的 Spring 项目中的如下片段,就是 Spring 的监听器。其实现了ServletContextListener,用于监听 Servlet 上下文,以便在项目初始化时加载 Spring 的必要配置。

  <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 </listener>

Servlet、Filter、Listerer 执行顺序

分别了解了 servlet、filter、listener 后,来确定一下他们之间的执行顺序。我们已经知道,filter 在 servlet 之前,那 listener 呢?

应该想到,监听器负责监听事件变化,应该有最先执行的权限,测试一下,我把上述三种都加了日志,打印结果如下:

// 开启服务器并请求
listener 容器初始化开始  //servletContent 监听器 contextInitialized()
filter 初始化        //filter init()
初始化 servlet       //servlet init()
listener request 初始化  // request 监听器 requestInitialized()
开始 filter            //fiter doFilter() chain.doFilter 前
执行 servlet        //servlet service()
结束 filter          //fiter doFilter() chain.doFilter 后
listener request 销毁  //request 监听器 requestInitialized()

// 关闭服务器
servlet 销毁
filter 销毁
listener 容器销毁

从此我们可以得出结论:

  1. 对于涉及 3 者的部分,顺序为 listener – filter – servlet
  2. filter 和 servlet 的初始化部分,先 filter 后 servlet
  3. 销毁或结束顺序为加载顺序的反序

3. 有关 Servlet 的注解

Servlet3 后,添加了若干关于 servlet,filter,listener 的注解支持。分别为@WebServlet,@WebFilter,@Webistener。也就是说,可以不用web.xml 配置文件了。注册相关类型可直接在类上添加相应注解。

如,添加一个 servlet 上下文的监听器。可使用如下方式。

@WebListener
public class ContentListener implements ServletContextListener {public void contextInitialized(ServletContextEvent sce) {System.out.println("listener 容器初始化开始");
    }

    public void contextDestroyed(ServletContextEvent sce) {System.out.println("listener 容器销毁");
    }
}

相当方便有木有。。。

总结

本文涉及到内容有点多,要把每个细节都说清楚,似乎不可能,本文试着从 Servlet 容器的启动到 Servlet 的初始化,以及 Servlet 的体系结构等这些环节中找出一些重点来讲述,目的是能读者有一个总体的完整的结构图,同时也详细分析了其中的一些难点问题,希望对大家有所帮助。

正文完
 0