乐趣区

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

本系列文章将整理到我在 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” 即可免费无套路获取。

退出移动版