本文概述
- 介绍了Servlet出现的历史背景及Servlet主要解决的问题
- 介绍了Servlet容器以及tomcat的基本工作原理
- 介绍并分析了Servlet类的继承关系及源码分析
本文目录
早期的web环境及CGI
早期的Internet中只有静态html页面,用户无法进行表单查询及动态更改网页内容。类似于现实中的布告栏(只能看不能写),除非网站管理员对网页进行修改,否则用户每次读取到的都是同样的网页。这就导致网页无法实时显示天气或股票信息,也无法实现登录或评论功能。
此时出现了CGI(Common Gateway Interface,通用网关接口)CGI是第一个让web页面具有动态交互能力的技术。 CGI的核心思想是让web服务器调用外部程序,通过这个外部程序动态生成html(后面会发现servlet的核心思想与CGI相同,只是二者的技术细节有所差别)。
引入CGI后的web页面请求过程:
1、用户提交表单
2、web服务器将表单中的参数传递给CGI脚本
3、CGI脚本执行并生成html
4、web服务器将生成的html文件返回给用户
CGI的出现首次实现了用户与服务器的双向交互,使网页实时更新信息成为了可能。
但CGI也具有一定的问题。可总结为以下三个痛点:
1、进程开销大。每接收到一个请求就创建一个进程处理(消耗更多资源,1000个请求就是1000个进程),且进程的创建和销毁需要时间(造成延迟)。
2、无原生会话管理(需借助cookie)
3、跨平台能力受限。不同操作系统下 CGI 脚本可能需要调整
Servlet的出现及Servlet解决的问题
Servlet的出现旨在解决CGI的缺陷。
CGI痛点 | Servlet如何解决 |
---|---|
进程开销大 | 线程池复用,降低开销 |
无原生会话管理 | 内置 HttpSession,简化状态管理 |
跨平台能力受限 | 基于 JVM,实现跨平台 |
简单介绍:
Servlet容器采用单实例多线程方式
- 每个Servlet类只有一个实例。由容器启动或接收到请求时创建(调用init()方法)
- 所有请求共享同一个实例,但通过多线程并发调用service()方法。servlet容器会维护一个线程池,每个HTTP请求由线程池中的一个独立线程处理。
Servlet提供了HttpSession接口来管理会话
HttpSession提供了一种在多个页面请求或访问之间识别用户和存储用户特定数据的机制。其主要特点为:
- 会话跟踪:在无状态的 HTTP 协议上实现有状态的会话
- 数据存储:可以存储任意 Java 对象作为属性
- 超时管理:会话可以配置超时时间
基于jvm实现跨平台
Servlet代码依靠jvm编译执行,不同平台下jvm有不同设计标准,但程序员不必考虑这些细节,只需专注于代码逻辑,即可依靠jvm实现一次编写,处处运行(Write Once, Run Anywhere)。
Servlet容器——tomcat介绍
tomcat既是web服务器,同时也是Servlet容器,其工作流程可以分为以下三步:
- 接收请求
- 处理请求
- 响应请求
其中接收请求及处理请求的流程是固定的。
- 接收请求即解析HTTP请求报文
- 响应请求即根据处理结果构造响应报文并发回给用户
只有处理请求这部分是动态的,针对不同请求有不同的处理方法,于是这部分定义为一个个不同的Servlet,针对不同请求使用不同Servlet进行处理。
具体来说,在tomcat中会配置不同url与servlet间的对应关系。当客户端请求不同url时,tomcat就知道应该调用哪些servlet来对请求进行处理。
如某web服务器地址为:http://www.example.com 其上有三个Servlet,分别为UserServlet、AdminServlet、ProductServlet,各自映射到 /UserServlet、/AdminServlet和 /ProductServlet。
则客户端在请求 http://www.example.com/UserServlet 时,tomcat查询url映射关系,调用UserServlet类处理请求,生成html文档并返回给客户端(和CGI的工作流程其实是一样的)
Servlet的继承关系及源码分析
先看一张关系图
我们从上到下逐层进行介绍
Servlet接口
位于最上层的是Servlet接口,源码如下:
public interface Servlet {
void init(ServletConfig var1) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
String getServletInfo();
void destroy();
}
可以看到有5个方法,以下三个为主要方法,涵盖了Servlet的生命周期:
- init():指示Servlet创建时要做什么
- service():指示Servlet要提供什么样的服务
- destroy():指示Servlet销毁时要执行什么操作
剩余两个为辅助方法,返回Servlet相关信息
- getServletInfo()
- getServletConfig()
让我们编写一个类继承自Servlet接口,并实现这5个方法,来看具体的执行过程
编写如下代码
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebServlet;
import java.io.IOException;
// 使用WebServlet注解就不用配置web.xml文件了
@WebServlet("/myServlet")
public class MyServlet implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
System.out.println("Servlet 正在初始化");
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("Servlet 正在提供服务");
}
@Override
public String getServletInfo() {
return "";
}
@Override
public void destroy() {
System.out.println("Servlet 正在销毁");
}
}
(idea社区版tomcat的配置过程可参考我的这篇博文 servlet获取表单数据以及idea社区版配置servlet)
启动tomcat,并访问 http://localhost:8080/myServlet
我们可以看到
其中init()方法和destroy()方法只会调用一次,service()方法每次请求都会调用
这时候停止tomcat,可以看到调用了destroy()方法
ServletRequest接口和ServletResponse接口
在Servlet接口源码中,我们可以看到service()方法接收两个参数,ServletRequest和ServletResponse
其中ServletRequest封装了客户端发送给服务器的所有请求信息,ServletResponse封装了服务器要发回给客户端的响应信息。
而这两者实际上都由servlet容器(tomcat等容器)在收到新请求时创建,之后传递给servlet类中的service()方法,在service()方法中读取ServletRequest对象中的参数,我们可以了解到客户端的请求信息,之后通过在service()方法中设置ServletResponse对象中的参数,我们就已经构建好了响应报文所需的关键信息。之后tomcat再读取ServletResponse对象中的这些信息,并按照固定流程构建好响应报文,之后返回给客户端。这样就完成了从请求到响应的全过程。
来看这两个接口中的常用方法
ServletRequest中的方法 | 作用 |
---|---|
String getCharacterEncoding() | 返回请求体中使用的字符编码名称 |
int getContentLength() | 返回请求体的长度(字节) |
String getContentType() | 返回请求体的MIME类型 |
String getLocalAddr() | 返回接收请求的接口的IP地址 |
String getLocalName() | 返回接收请求的接口的主机名 |
int getLocalPort() | 返回接收请求的接口的端口号 |
String getParameter(String name) | 返回请求参数的值 |
String getProtocol() | 返回请求使用的协议名称和版本 |
String getServerName() | 返回请求发送到的服务器主机名 |
int getServerPort() | 返回请求发送到的端口号 |
ServletResponse中的方法 | 作用 |
---|---|
void flushBuffer() | 强制将缓冲区中的内容写入客户端 |
int getBufferSize() | 返回用于响应的实际缓冲区大小 |
String getCharacterEncoding() | 返回响应体中使用的字符编码(MIME 字符集)名称 |
String getContentType() | 返回响应中发送的 MIME 内容类型 |
PrintWriter getWriter() | 返回可以向客户端发送字符文本的 PrintWriter 对象 |
void setCharacterEncoding(String charset) | 设置发送给客户端的响应的字符编码(如 UTF-8) |
void setContentLength(int len) | 设置响应体的内容长度 |
void setContentType(String type) | 设置发送给客户端的响应的内容类型 |
让我们修改一下上节中的service()方法,来打印一下其中的一些参数
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String characterEncoding = servletRequest.getCharacterEncoding();
int contentLength = servletRequest.getContentLength();
String contentType = servletRequest.getContentType();
String localAddr = servletRequest.getLocalAddr();
String localName = servletRequest.getLocalName();
int localPort = servletRequest.getLocalPort();
String protocol = servletRequest.getProtocol();
String serverName = servletRequest.getServerName();
int serverPort = servletRequest.getServerPort();
// 设置文件类型和字符编码方式
servletResponse.setContentType("text/html;charset=UTF-8");
servletResponse.getWriter().write("<h1>字符编码类型:" + characterEncoding + "</h1>");
servletResponse.getWriter().write("<h1>请求体长度:" + contentLength + "</h1>");
servletResponse.getWriter().write("<h1>请求体类型:" + contentType + "</h1>");
servletResponse.getWriter().write("<h1>接收请求的ip地址:" + localAddr + "</h1>");
servletResponse.getWriter().write("<h1>接收请求的主机名:" + localName + "</h1>");
servletResponse.getWriter().write("<h1>协议名:" + protocol + "</h1>");
servletResponse.getWriter().write("<h1>服务器主机名:" + serverName + "</h1>");
servletResponse.getWriter().write("<h1>服务器端口:" + serverPort + "</h1>");
}
结果如下
可以看到Servlet接收到了请求,并将相关信息添加成为html并返回
ServletConfig接口
Servlet的init()方法接受一个ServletConfig对象作为参数,主要返回Servlet信息,共有如下4个方法,这里不展开讨论
public interface ServletConfig {
String getServletName();
ServletContext getServletContext();
String getInitParameter(String var1);
Enumeration<String> getInitParameterNames();
}
GenericServlet抽象类
如果说每次使用Servlet都要实现其所有5个方法,确实是一件累人的事情。所以有了GenericServlet抽象类,它帮我们实现了Servlet中的一些方法,并引入了新的方法。
来看源码
public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {
private static final String LSTRING_FILE = "jakarta.servlet.LocalStrings";
private static final ResourceBundle lStrings = ResourceBundle.getBundle("jakarta.servlet.LocalStrings");
private transient ServletConfig config;
public GenericServlet() {
}
public void destroy() {
}
public String getInitParameter(String name) {
ServletConfig sc = this.getServletConfig();
if (sc == null) {
throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
} else {
return sc.getInitParameter(name);
}
}
public Enumeration<String> getInitParameterNames() {
ServletConfig sc = this.getServletConfig();
if (sc == null) {
throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
} else {
return sc.getInitParameterNames();
}
}
public ServletConfig getServletConfig() {
return this.config;
}
public ServletContext getServletContext() {
ServletConfig sc = this.getServletConfig();
if (sc == null) {
throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
} else {
return sc.getServletContext();
}
}
public String getServletInfo() {
return "";
}
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {
}
public void log(String msg) {
ServletContext var10000 = this.getServletContext();
String var10001 = this.getServletName();
var10000.log(var10001 + ": " + msg);
}
public void log(String message, Throwable t) {
this.getServletContext().log(this.getServletName() + ": " + message, t);
}
public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
public String getServletName() {
ServletConfig sc = this.getServletConfig();
if (sc == null) {
throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
} else {
return sc.getServletName();
}
}
}
可以看到GenericServlet类实现了Servlet和ServletConfig接口
并且Servlet中的以下4个方法已经被实现,当然我们也可以进行覆盖
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public ServletConfig getServletConfig() {
return this.config;
}
public String getServletInfo() {
return "";
}
public void destroy() {
}
只有Service()方法保持抽象,需要我们自己实现。
public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
我们编写一个类继承自GenericServlet抽象类,重写service方法。
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebServlet;
import java.io.IOException;
@WebServlet("/myServlet")
public class MyServlet extends GenericServlet {
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("GenericServlet 正在提供服务");
}
}
运行
HttpServlet抽象类
GenericServlet是协议无关的,通常用于非HTTP协议的Servlet开发(如FTP等,但平时很少会这么做,所以GenericServlet的使用场景不高)但Servlet日常使用最多的还是处理HTTP协议。于是就有了HttpServlet抽象类,其继承自GenericServlet抽象类,并添加了对于HTTP协议的支持。
由于HttpServlet的方法较多,这里只给出常用部分
核心HTTP方法(需子类覆盖) | 说明 |
---|---|
protected void doGet(HttpServletRequest req, HttpServletResponse resp) | 处理 GET 请求 |
protected void doPost(HttpServletRequest req, HttpServletResponse resp) | 处理 POST 请求 |
protected void doPut(HttpServletRequest req, HttpServletResponse resp) | 处理 PUT 请求 |
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) | 处理 DELETE 请求 |
protected void doHead(HttpServletRequest req, HttpServletResponse resp) | 处理 HEAD 请求 |
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) | 处理 OPTIONS 请求 |
protected void doTrace(HttpServletRequest req, HttpServletResponse resp) | 处理 TRACE 请求 |
默认实现:这些方法默认返回 405 Method Not Allowed(除非子类覆盖)。
其中我们使用最多的就是doGet()和doPost()方法
并且HttpServlet实现了GenericServlet中的service()方法
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
if (method.equals("GET")) {
long lastModified = this.getLastModified(req);
if (lastModified == -1L) {
this.doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader("If-Modified-Since");
if (ifModifiedSince < lastModified) {
this.maybeSetLastModified(resp, lastModified);
this.doGet(req, resp);
} else {
resp.setStatus(304);
}
}
} else if (method.equals("HEAD")) {
long lastModified = this.getLastModified(req);
this.maybeSetLastModified(resp, lastModified);
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else if (method.equals("PATCH")) {
this.doPatch(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
通过分析源码,可以发现service()会根据HTTP请求报文头部中的方法(GET,POST等),调用我们重写的doGet(),doPost()等方法。也就是说经过HttpServlet类的进一步封装,此时我们连service()方法都不用实现了,只需要关心面对不同HTTP请求时应该如何进行处理即可,这样就大大减少了我们的工作量。
我们来编写一个类,重写doGet方法。当我们在浏览器中访问该Servlet时,会使用GET方法,这样就会触发doGet()进行处理
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/myServlet")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("<h1>" + "It works!" + "</h1>");
}
}
在浏览器中访问,我们可以得到
接下来重写一下doPost()方法,接收客户端输入并显示
创建一个html表单
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/myServlet" method="post">
<span>用户名</span><input type="text" name="username"><br>
<span>密码</span><input type="password" name="password"><br>
<input type="submit" name="submit">
</form>
</body>
</html>
重写doPost()方法
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/myServlet")
public class MyServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
resp.getWriter().write("<h1>" + username + "</h1>");
resp.getWriter().write("<h1>" + password + "</h1>");
}
}
接着在表单中进行输入 admin admin
点击提交后会得到如下结果
HttpServletRequest和HttpServletResponse接口
上一节中,我们看到HttpServlet的核心方法都接受两个参数,分别是HttpServletRequest和HttpServletResponse
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
这二者分别继承自ServletReques和ServletResponse接口,两者在其基础上又增加了对http协议的支持。依旧是由Servlet容器创建,并传递给service()方法
来看这两个接口中常用的方法
HttpServletRequest中的方法 | 作用 |
---|---|
String getContextPath() | 获取请求URI中的上下文路径部分 |
Cookie[] getCookies() | 获取客户端发送的所有Cookie对象 |
String getHeader(String name) | 获取指定请求头的字符串值,如User-Agent或Content-Type等 |
String getMethod() | 获取HTTP请求方法(GET/POST等) |
String getRequestURI() | 获取从协议名到查询字符串的请求URL部分 |
String getServletPath() | 获取请求URL中调用servlet的部分 |
HttpServletResponse中的方法 | 作用 |
---|---|
void addCookie(Cookie cookie) | 添加指定 Cookie 到响应 |
void addHeader(String name, String value) | 添加带字符串值的响应头 |
String encodeURL(String url) | 对普通URL编码(包含会话ID) |
String getHeader(String name) | 获取指定响应头的值 |
int getStatus() | 获取当前响应状态码 |
void sendError(int sc) | 发送错误状态码(清空缓冲区) |
void setHeader(String name, String value) | 设置带字符串值的响应头(覆盖) |
void setStatus(int sc) | 设置响应状态码 |
修改一下上节中的doGet()方法来打印一下其中的参数
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/myServlet")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String contextPath = req.getContextPath();
String method = req.getMethod();
String requestURI = req.getRequestURI();
String servletPath = req.getServletPath();
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("<h1>contextPath:" + contextPath + "</h1>");
resp.getWriter().write("<h1>请求方法:" + method + "</h1>");
resp.getWriter().write("<h1>请求URI:" + requestURI + "</h1>");
resp.getWriter().write("<h1>servlet Path:" + servletPath + "</h1>");
}
}
未完待续
本人是初学者,对于Servlet的学习暂时到这里,后续会补充下Servlet的局限性(等学习到Java Web框架之后)以及Servlet多线程还有HttpSession的相关内容。
参考资料
- JavaWeb——Servlet(全网最详细教程包括Servlet源码分析) 本文的主要思路来源于此,可前往此处进行更进一步的学习
- Servlet、web容器、springmvc之间的关系(知乎)
- servlet——百度百科
- 菜鸟教程——servlet