上文提到项目中有 Servlet 和 JSP 两个类型的文件,其中 JSP 是一种特殊的 Servlet。接下来我们依次了解 Servlet、JSP,再从 Tomcat 服务器角度来看请求是如何被处理的。本文先介绍 Servlet 的执行机制。
Servlet 简介静态网站的出现解决了全球不同地方的人无法快速交流的问题,但静态网站对应的静态资源都必须是事先存在的。比如我发布了一篇文章,想要立马在网站上看到是做不到的,必须编辑网站对应的html,将网站链接添加在html中才可以看到。为了实现这种动态交互,JAVA EE 在出现时就提出了 Servlet,用来实现动态网站。
Servlet 是运行在支持 Servlet 规范、能解释 Servlet 的 web 服务器上的 Java 类。那Servlet 是怎么实现动态的呢?
在介绍之前先要说一下 JavaEE。Java EE 在设计之时就为了解决 web 交互问题提出了分层思想,每层有很多组件。并且规定组件不能直接对外提供服务,必须增加容器层,组件部署到容器上,由容器完成对组件的管理、与外界的交互。
Servlet 就是 Java EE 提出的一个组件,它要提供功能就需要有 Servlet 容器(管理 Servlet 的生命周期),而 Servlet 容器 也是 web 服务器(比较常用的 web 服务器是 tomcat)的一个扩展功能。由此服务器端就形成了以 Servlet 为中心的三层:Servlet、Servlet 容器、web 服务器。web 服务器与客户端的交互是通过请求/响应模型完成。
接下来具体说下如何实现整个交互过程、如何完成动态网站?
- web 浏览器发送 HTTP 请求到 web 服务器
- web 服务器将请求移交给 Servlet 容器
- Servlet 容器根据 Servlet 配置和请求路径决定要处理的 Servlet 实例,将请求分发给该实例
- Servlet 是有业务逻辑的,会根据不同的请求逻辑生成不同的响应页面(虽然一开始页面是动态资源,但 Servlet 处理结束时页面内容是固定的,就是一个静态资源;也就是 Servlet 的执行会将动态资源转换为静态资源)
- Servlet 处理完毕,Servlet 容器刷新响应,将控制权移交给 web 服务器
- web 服务器将响应以静态资源的形式返回给浏览器
- 浏览器解析静态资源并展示(浏览器只能解析静态资源)
这里的整个交互过程只是一个规范,毕竟JAVA EE 本身就是一个规范,具体的交互细节取决于不同的 web 服务器。后面会针对具体的 web 服务器 Tomcat 介绍一些交互细节,比如 web 服务器是怎么把请求转交给 Servlet 容器的,是原封不动的把浏览器请求转交还是进行了处理?这些都会进行介绍。
创建第一个 Servlet(基于 Tomcat 服务器) 创建 Servlet ,继承自 HttpServletimport javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class FirstServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
根据需要实现 doGet 或 doPost 方法
比如我们现在要实现 get 方法,方法体内返回一个一句话 my first servlet!,则代码为:
public class FirstServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
out.println("my first servlet!");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
注册 Servlet
有两种方式可以注册 Servlet,一种是配置,一种是注解。
配置文件方式注册 Servlet在 web.xml 中添加 Servlet 的两个配置信息:servlet和servlet-mapping。
注解方式注册 ServletFirstServlet com.demo.servlet.FirstServlet FirstServlet /firstServlet
为了减少配置文件,从 Servlet 3.0 开始就提出了@WebServlet注解实现 Servlet 的注册,主要使用到 name 和 value两个属性,分别对应配置文件方式中servlet下的servlet-name、servlet-mapping下的url-pattern。在项目部署时,Servlet 容器就会处理该注解。
@WebServlet(name = "FirstServlet", value = "/firstServlet")
public class FirstServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 2. 重写需要的 get 或 post 方法
System.out.println("处理 get 请求");
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
out.println("my first servlet!");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
访问 Servlet
启动项目,请求路径http://localhost:8080/web/firstServlet。Servlet 被成功访问。
当我们通过浏览器请求 Tomcat 上的 Servlet 时整个过程什么样的呢?
- 浏览器发起 get 请求 http://localhost:8080/web/firstServlet
- Tomcat(web服务器) 接收请求,转交给 Servlet 容器
- Servlet 容器根据请求路径 /firstServlet ,从 Servlet 配置信息中确定要请求的 Servlet 实例,也就是 FirstServlet。根据请求方法确定是 FirstServlet 的 get 方法。
其中根据路径http://localhost:8080/web/firstServlet定位 Servlet 实例和方法的具体过程为:
a. ip 地址 localhost 确定服务器
b. 端口号 8080 确定进程,也就是 Tomcat
c. 项目上下文 /web 确定项目
d. 资源路径 /firstServlet 确定 Servlet 实例
e. 请求方法确定 Servlet 实例的方法 - Servlet 容器判断是有存在 FirstServlet 的实例对象,若存在直接将请求转发;否则先加载、实例化、初始化 FirstServlet对象,然后转发。
- FirstServlet 的 get 方法处理请求
- FirstServlet请求结束,Servlet 容器刷新响应数据,控制权移交给 Tomcat(web服务器)
- Tomcat(web服务器)将结果响应给浏览器
- 浏览器解析静态资源,页面显示 my first servlet!
介绍 Servlet 执行流程时有涉及 Servlet 的生命周期,这里完整的介绍一次。上文提到,Servlet 的生命周期是由 Servlet 容器管理的,包括以下四阶段:
- 加载和实例化。当 Servlet 容器刚刚启动、或者请求的 Servlet 实例不在内存中时,Servlet 容器会加载 Servlet 的类并实例化。该方法一般只会执行一次。
- 初始化。在 Servlet 被实例化后,Servlet 容器会请求 init()方法初始化 Servlet,并将ServletConfig对象作为参数传入。每个 Servlet 实例都有一个ServletConfig对象。只有初始化成功, Servlet 才可以对外提供服务。该方法一般只会执行一次。
- 处理请求。当 Servlet 容器收到对这一 Servlet 的请求时,会调用该对象的service()方法,并将请求和响应对象作为参数传入。在Servlet的子类 HttpServlet中,将请求根据请求方法类型又增加了几个方法:doGet()、doPost()…。同时在service()方法中根据请求方法类型转发到具体的方法中。该方法可以被多次请求。
- 销毁。当 Servlet 容器停止服务,destory()方法会被请求以释放资源。
刚刚说到,当 Servlet 容器刚刚启动、或者请求的 Servlet 实例不在内存中时,Servlet 容器会加载 Servlet 的类并实例化。也就是 Servlet 会在两种情况下被实例化:容器启动时、第一次请求到来时。这是通过启动参数配置的。具体看下如何配置。
配置文件方式设置 Servlet 实例化时机web.xml 文件 servlet 中通过属性 load-on-startup 设置是否在 Servlet 容器启动时创建并初始化这个 Servlet实例。负数表示第一次请求时才会被实例化;非负数则在容器启动时被实例化,且值越小越早被实例化,越晚被销毁。默认在第一次请求时被实例化。
注解方式设置 Servlet 实例化时机FirstServlet com.demo.servlet.FirstServlet 1 FirstServlet /firstServlet
@WebServlet注解中通过参数loadOnStartup设置实例化时机。取值含义与上文一致。
@WebServlet(name = "FirstServlet", value = "/firstServlet", loadOnStartup = 1)
public class FirstServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 2. 重写需要的 get 或 post 方法
System.out.println("处理 get 请求");
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
out.println("my first servlet!");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
一个实例化时机的小例子
现有有HelloServlet、FirstServlet两个类,启动项load-on-startup 的值分别为0、1,则 Servlet 容器启动时调用顺序为:
Connected to server HelloServlet 构造函数 HelloServlet init FirstServlet create FirstServlet init
容器销毁时调用顺序为:
FirstServlet destroy HelloServlet destroy总结
JAVA EE 提出 Servlet 的目的就是为了让开发人员聚焦业务逻辑,不必关系过多细节。比如浏览器和服务器如何建立链接、服务器如何接受请求、怎么找到要处理的 Servlet 实例、Servlet 处理的结果如何通知浏览器,这些其实 Servlet 容器、web 服务器都帮我们解决了【后面会简单介绍下 tomcat 的运行机制,敬请期待】,具体来说就是 Tomcat(Tomcat 既充当了 web 服务器,又充当了 Servlet 容器) 都实现了。我们只需要实现 Servlet 就可以了。
而对于 Servlet 来说,刚刚学习的人学会如何创建就可以了,有兴趣的可以了解下 Servlet 容器是如何管理 Servlet 的。



