目录
1. Servlet 运行原理
1.1 Tomcat 的定位

更详细的交互过程可以参考下图:
1) 接收请求:
-用户在浏览器输入一个 URL, 此时浏览器就会构造一个 HTTP 请求。-这个 HTTP 请求会经过网络协议栈逐层进行 封装 成二进制的 bit 流 , 最终通过物理层的硬件设备转换成光信号/ 电信号传输出去。-这些承载信息的光信号 / 电信号通过互联网上的一系列网络设备 , 最终到达目标主机 ( 这个过程也需要网络层和数据链路层参与)。-服务器主机收到这些光信号 / 电信号 , 又会通过网络协议栈逐层进行 分用 , 层层解析 , 最终还原成 HTTP 请求 . 并交给 Tomcat 进程进行处理 ( 根据端口号确定进程 )。-Tomcat 通过 Socket 读取到这个请求 ( 一个字符串 ), 并按照 HTTP 请求的格式来解析这个请求 , 根据请求中的 Context Path 确定一个 webapp, 再通过 Servlet Path 确定一个具体的类 .。 再根据当前请求的方法 (GET/POST/...), 决定调用这个类的 doGet 或者 doPost 等方法 . 此时我们的代码中的 doGet / doPost 方法的第一个参数 HttpServletRequest 就包含了这个 HTTP 请求的详细信息。
2) 根据请求计算响应
在我们的 doGet / doPost 方法中, 就执行到了我们自己的代码. 我们自己的代码会根据请求中的一
1.2 Tomcat 的伪代码
class Tomcat {
// 1.用来存储所有的 Servlet 对象
private List<Servlet> instanceList = new ArrayList<>();
public void start() {
// 根据约定,读取 WEB-INF/web.xml 配置文件;
// 并解析被 @WebServlet 注解修饰的类
// 假定这个数组里就包含了我们解析到的所有被 @WebServlet 注解修饰的类.
Class<Servlet>[] allServletClasses = ...;
// 2.实例化出所有的 Servlet 对象出来;
for (Class<Servlet> cls : allServletClasses) {
// 这里是利用 java 中的反射特性做的
// 实际上还得涉及一个类的加载问题,因为我们的类字节码文件,是按照约定的
// 方式(全部在 WEB-INF/classes 文件夹下)存放的,所以 tomcat 内部是
// 实现了一个自定义的类加载器(ClassLoader)用来负责这部分工作。
Servlet ins = cls.newInstance();
instanceList.add(ins);
}
// 3.调用每个 Servlet 对象的 init() 方法,这个方法在对象的生命中只会被调用这一次;
for (Servlet ins : instanceList) {
ins.init();
}
// 4.启动一个 HTTP 服务器
// 并用线程池的方式分别处理每一个 Request
ServerSocket serverSocket = new ServerSocket(8080);
// 实际上 tomcat 不是用的固定线程池,这里只是为了说明情况
ExecuteService pool = Executors.newFixedThreadPool(100);
while (true) {
Socket socket = ServerSocket.accept();
// 每个请求都是用一个线程独立支持,这里体现了我们 Servlet 是运行在多线程环境下的
pool.execute(new Runnable() {
doHttpRequest(socket);
});
}
// 5.调用每个 Servlet 对象的 destroy() 方法,这个方法在对象的生命中只会被调用这一次;
for (Servlet ins : instanceList) {
ins.destroy();
}
}
public static void main(String[] args) {
new Tomcat().start();
}
}
在上述代码中:
class Tomcat {
void doHttpRequest(Socket socket) {
// 参照HTTP 服务器类似的原理,进行 HTTP 协议的请求解析,和响应构建
HttpServletRequest req = HttpServletRequest.parse(socket);
HttpServletRequest resp = HttpServletRequest.build(socket);
// 判断 URL 对应的文件是否可以直接在我们的根路径上找到对应的文件,如果找到,就是静态内容
// 直接使用我们学习过的 IO 进行内容输出
if (file.exists()) {
// 返回静态内容
return;
}
// 走到这里的逻辑都是动态内容了
// 按照 URL -> servlet-name -> Servlet 对象的链条
// 最终找到要处理本次请求的 Servlet 对象
Servletins = findInstance(req.getURL());
// 调用 Servlet 对象的 service 方法
// 这里就会最终调用到我们自己写的 HttpServlet 的子类里的方法了
try {
ins.service(req, resp);
} catch (Exception e) {
// 返回 500 页面,表示服务器内部错误
}
}
}
在上述代码中
class Servlet {
public void service(HttpServletRequest req, HttpServletResponse resp) {
String method = req.getMethod();
if (method.equals("GET")) {
doGet(req, resp);
} else if (method.equals("POST")) {
doPost(req, resp);
} else if (method.equals("PUT")) {
doPut(req, resp);
} else if (method.equals("DELETE")) {
doDelete(req, resp);
}
......
}
}
1.3 总结
关于Servlet的关键方法,主要有三个,可以理解成Servlet 的生命周期(什么时候该做什么)
1)init: 初始化阶段,对象创建好了之后,就会执行到.用户可以重写这个方法,来执行一-些初始化逻辑。
2)service: 在处理请求阶段来调用.每次来个请求都要调用一次service
3) destroy: 退出主循环,tomcat结束之前会调用,用来释放资源~
2. Servlet API 详解
2.1 HttpServlet

代码示例: 处理 POST 请求
代码中写入 resp.setContentType("text/html; charset = utf8"),是因为Windows默认编码是gbk,而idea的编码是 utf8,因此写入让浏览器使用uft8来读。
@WebServlet("/method")
public class MethodServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//super.doPost(req, resp);
//防止浏览器乱码
resp.setContentType("text/html; charset = utf8");
resp.getWriter().write("POST 响应");
}
}
其中url 就是代码中@WebServlet("/method") 里面的method
<body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
$.ajax({
type: 'post',
url: 'method',
success: function(body) {
console.log(body);
}
});
</script>
</body>
浏览器中的响应如下,按快捷键F12即可看到浏览器控制台的中响应
2.2 HttpServletRequest
HttpServletResponse对应到一个HTTP响应,HTTP响应中有什么,这里就有什么
核心方法
方法
|
描述
|
String getProtocol()
|
返回请求协议的名称和版本。
|
String getMethod()
|
返回请求的
HTTP
方法的名称,例如,
GET
、
POST
或
PUT
。
|
String getRequestURI()
|
从协议名称直到
HTTP
请求的第一行的查询字符串中,返回该请求的 URL
的一部分。
|
String getContextPath()
|
返回指示请求上下文的请求
URI
部分。
|
String getQueryString()
|
返回包含在路径后的请求
URL
中的查询字符串。
|
Enumeration
getParameterNames()
|
返回一个
String
对象的枚举,包含在该请求中包含的参数的名称。
|
String getParameter(String name)
|
以字符串形式返回请求参数的值,或者如果参数不存在则返回null。
|
String[]
getParameterValues(String
name)
|
返回一个字符串对象的数组,包含所有给定的请求参数的值,
如果参数不存在则返回
null
。
|
Enumeration
getHeaderNames()
|
返回一个枚举,包含在该请求中包含的所有的头名。
|
String getHeader(String
name)
|
以字符串形式返回指定的请求头的值
|
String
getCharacterEncoding()
|
返回请求主体中使用的字符编码的名称。
|
String getContentType()
|
返回请求主体的
MIME
类型,如果不知道类型则返回
null
。
|
Int getContentLength()
|
以字节为单位返回请求主体的长度,并提供输入流,或者如果
长度未知则返回
-1
。
|
InputStream
getInputStream()
|
用于读取请求的
body
内容
.
返回一个
InputStream
对象
.
|
@WebServlet("/showRequestServlet")
public class ShowRequestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("<h3>首行部分</h3>");
//请求协议的名称和版本
stringBuilder.append(req.getProtocol());
stringBuilder.append("<br>");
//从协议名称直到 HTTP 请求的第一行的查询字符串中,返回该请求的 URL 的一部分。
stringBuilder.append(req.getRequestURI());
stringBuilder.append("<br>");
//指示请求上下文的请求 URI 部分
stringBuilder.append(req.getContextPath());
stringBuilder.append("<br>");
//包含在路径后的请求 URL 中的查询字符串
stringBuilder.append(req.getQueryString());
stringBuilder.append("<br>");
stringBuilder.append("<h3>header 部分</h3>");
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = req.getHeader(headerName);
stringBuilder.append(headerName + ";" + headerValue + "<br>");
}
//使用utf8编码
resp.setContentType("text/html;charset = utf8");
//读取请求
resp.getWriter().write(stringBuilder.toString());
}
}
但是却没那么常用,更常用的,其实是getParameter这个方法。 (获取到query string中的详细内容)

(2) 代码示例: 获取 GET 请求中的参数
@WebServlet("/getParameter")
public class GetParameterServlet extends HelloServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
// 预期浏览器传来一个形如这样的请求: /getParameter?userId=123&classId=456
//以字符串形式返回请求参数的值,或者如果参数不存在则返回null。
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
resp.getWriter().write("userId=" + userId + ",classId=" + classId);
}
}
以下是query string 给服务器没传参和传参的页面响应,参数是可以手动输入的。

POST 请求的参数一般通过 body 传递给服务器. body 中的数据格式有很多种.
1)采用 form 表单形式
body>
<form action="postGetParameter" method="post">
<input type="text" name="userId">
<input type="text" name="classId">
<input type="submit" value="提交">
</form>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
</body>
创建PostGetParameterServlet 类
@WebServlet("/postGetParameter")
public class PostGetParameterServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 服务器也是通过 req.getParameter 来获取到内容的.
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
resp.getWriter().write("userId=" + userId + ",classId" + classId);
}
}
获取结果
要想构造一个 json 格式的请求, 就需使用 ajax 。
1)创建 text3.html用于发送请求,以下是body 部分
<body>
<!-- post请求,json形式 -->
<input type="text" id="userId">
<input type="text" id="classId">
<input type="submit" value="提交" , id="submit">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<!-- 要想构造一个 json 格式的请求, 就不再使用 form 而是使用 ajax 了 -->
<script>
let userIdInput = document.querySelector('#userId');
let classIdInput = document.querySelector('#classId');
let button = document.querySelector('#submit');
button.onclick = function() {
$.ajax({
type: 'post',
url: 'postJson',
contentType: 'application/json',
data: JSON.stringify({
userId: userIdInput.value,
classId: classIdInput.value
}),
success: function(body) {
console.log(body);
}
});
}
</script>
</body>
2)其中 pom.xml 需要引入新的依赖,在maven 中央仓库里面找
代码片段如下
<dependencies>
<!-- 获取post 请求,json格式的依赖-->
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.6.1</version>
</dependency>
</dependencies>
3)创建 PostJsonServlet 类
class User {
public int userId;
public int classId;
}
@WebServlet("/postJson")
public class PostJsonServlet extends HttpServlet {
//1.创建一个Json 核心对象
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//2. 读取 body 中的请求, 然后使用 ObjectMapper 来解析成需要的对象.
// readValue 就是把 JSON 格式的字符串, 转成 Java 的对象
// 第一个参数, 表示对哪个字符串进行转换. 这个参数可以填写成一个 String, 也可以填一个 InputStream 对象, 还可以填一个 File
// 第二个参数, 表示要把这个 JSON 格式的字符串, 转成哪个 Java 对象
User user = objectMapper.readValue(req.getInputStream(), User.class);
resp.getWriter().write("userId: " + user.userId + ", classId: " + user.classId);
}
}
4)代码解析
在java后端代码中,通过jackson来进行处理。
需要使用jackson ,把请求body中的数据读取出来,并且解析成Java中的对象。
5)请求结果
因为当前使用的是ajax的方式来提交数据,这个操作默认不会产生页面跳转,就和form风格差别很大。
6)使用postman的请求
2.2 HttpServletResponse
方法
|
描述
|
void setStatus(int sc)
|
为该响应设置状态码
|
void setHeader(String name,
String value)
|
设置一个带有给定的名称和值的
header.
如果
name
已经存在
, 则覆盖旧的值
|
void addHeader(String
name, String value)
|
添加一个带有给定的名称和值的
header.
如果
name
已经存在
, 不覆盖旧的值,
并列添加新的键值对
|
void setContentType(String
type)
|
设置被发送到客户端的响应的内容类型。
|
void
setCharacterEncoding(String
charset)
|
设置被发送到客户端的响应的字符编码(
MIME
字符集)例如,UTF-8
。
|
void sendRedirect(String
location)
|
使用指定的重定向位置
URL
发送临时重定向响应到客户端。
|
PrintWriter getWriter()
|
用于往
body
中写入文本格式数据
.
|
OutputStream
getOutputStream()
|
用于往
body
中写入二进制格式数据
.
|
注意: 响应对象是服务器要返回给浏览器的内容, 这里的重要信息都是程序猿设置的. 因此上面的方法都是 "写" 方法。注意: 对于状态码/响应头的设置要放到 getWriter / getOutputStream 之前. 否则可能设置失效。
(1) 代码示例: 设置状态码
实现一个程序, 用户在浏览器通过参数指定要返回响应的状态码。
创建 StatusServlet 类
@WebServlet("/status")
public class StatusServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//状态码自己输入,200、404 等等都可以
resp.setStatus(200);
resp.getWriter().write("hello 200");
}
}
响应效果如下
服务器返回的状态码,只是在告诉浏览器,当前的响应是个什么状态,并不影响浏览器照常去显示body中的内容。
(2)代码示例: 自动刷新
@WebServlet("/autoRefresh")
public class AutoRefreshServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//一毫秒刷新一次
resp.setHeader("Refresh","1");
resp.getWriter().write("timeStamp: " + System.currentTimeMillis());
}
}

(3)代码示例: 重定向
实现一个程序,返回一个重定向 HTTP 响应, 自动跳转到另外一个页面。构造重定向的响应302.
创建 RedirectServlet 类
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 在这里返回一个 302 重定向响应, 让浏览器, 自动跳转到 百度 主页
//resp.setStatus(302);
//resp.setHeader("Location","https://www.baidu.com/" );
// Servlet 提供了一个更简便的实现重定向的写法.
resp.sendRedirect("https://www.baidu.com");
}
}
浏览器页面响应
抓包结果
3.Cookie和Session
3.1 Cookie

3.2 理解会话机制 (Session)

3.3 Cookie 和 Session 的区别
3.4 核心方法
HttpServletRequest 类中的相关方法
方法
|
描述
|
HttpSession
getSession()
|
在服务器中获取会话
.
参数如果为
true,
则当不存在会话时新建会话
;
参数如果为 false,
则当不存在会话时返回
null
|
Cookie[]
getCookies()
|
返回一个数组
,
包含客户端发送该请求的所有的
Cookie
对象
.
会自动把Cookie 中的格式解析成键值对。
|
HttpSession:这个对象本质上也是一个"键值对"的结构。
在调用getSession的时候具体要做的事情:
1)创建会话
首先先获取到请求中cookie里面的sessionld字段~~ (相当于会话的身份标识)
判定这个sessionld是否在当前服务器上存在。
如果不存在,则进入创建会话逻辑。
创建会话,会创建一个HttpSession对象,并且生成一个sessionld (是一个很长的数字,通常是用十六进制来表示,能够保证唯一性)
接下来就会把这个sessionld作为key,把这个HttpSession对象,作为value, 把这个键值对,给保存到服务器内存的一个“哈希表"这样的结构中。
再然后,服务器就会返回-个HTTP响应把sessionld通过Set-Cookie字段返回给浏览器。浏览器就可以保存这个sessionld到Cookie中了。
2)获取会话
先获取到请求中的cookie里面的sessionld字段 (也就是会话的身份标识)
判定这个sessionld是否在当前服务器上存在(也就是在这个哈希表中是否有)
如果有,就直接查询出这个HttpSession对象,并且通过返回值返回回去。
HttpServletResponse类中的相关方法
方法 | 描述 |
void addCookie(Cookie cookie)
|
把指定的
cookie
添加到响应中
|
响应中就可以根据addCookie这个方法,来添加一一个Cookie信息到响应报文中。
这里添加进来的键值对,就会作为HTTP响应中的Set-Cookie字段来表示。
Cookie类中的相关方法
每个 Cookie 对象就是一个键值对.
方法 | 描述 |
String getName()
|
该方法返回
cookie
的名称。名称在创建后不能改变。
(
这个值是
Set-Cooke 字段设置给浏览器的
)
|
String getValue()
|
该方法获取与
cookie
关联的值
|
void setValue(String
newValue)
|
该方法设置与
cookie
关联的值
|
3.4 实现登录页面
实现简单的用户登陆逻辑,这个代码中主要是通过 HttpSession 类完成. 并不需要我们手动操作 Cookie 对象
1)创建 login.html, 放到 webapp 目录中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<form action="login" method="post">
<input type="text" name="username">
<input type="text" name="password">
<input type="submit" value="登录">
</form>
</body>
</html>
2) 创建 LoginServlet 类
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//处理用户请求
String username = req.getParameter("username");
String password = req.getParameter("password");
//判定用户名或者密码是否正确
// 正常来说这个判定操作是要放到数据库中进行存取的.
// 此处为了简单, 就直接在代码里写死了. 假设有效的用户名和密码是 "Fly", "123"
if ("Fly".equals(username) && "123".equals(password)) {
//登录成功
//创建会话,并保存必要的身份信息
HttpSession httpSession = req.getSession(true);
//往会话中存储键值对,必要的身份信息
httpSession.setAttribute("username",username);
//初始情况下,把登录次位设为0
httpSession.setAttribute("count",0);
//重定向,页面跳转
resp.sendRedirect("index");
} else {
//登录失败,使用utf8编码,不然会乱码
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write("登录失败!请重新检查再输入");
}
}
}
在这个代码中是看不到 "哈希表", 也看不到 sessionId 这样的概念的. getSession 操作内部提取到请求中的 Cookie 里的 sessionId, 然后查找哈希表, 获取到对应的 HttpSession 对象。
此处的 getSession 参数为 true, 表示查找不到 HttpSession 时会创建新的 HttpSession 对象, 并生成一个 sessionId, 哈希表中, 并且把 sessionId 通过 Set-Cookie 返回给浏览器.
3) 创建 IndexServlet 类
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/index")
public class IndexServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 返回一个主页. (主页就是一个简单的 html 片段)
// 此处需要得到用户名是啥, 从 HttpSession 中就能拿到.
// 此处 getSession 的参数必须是 false. 前面在登录过程中, 已经创建过会话了. 此处是要直接获取到之前的会话.
HttpSession session = req.getSession(false);
String username = (String) session.getAttribute("username");
// 从会话中取出 count.
Integer count = (Integer) session.getAttribute("count");
count += 1;
// 把自增之后的值写回到会话中.
session.setAttribute("count",count);
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write("<h3>欢迎你! " + username + " 这是第 " + count + " 次访问主页 </h3>");
}
getSession 参数为 false , 不会创建新的 HttpSession, 而是返回上面 LoginServlet 类创建的HttpSession 对象
4)登录成功效果
此处默认username = Fly, password = 123, 所以登录成功,点击刷新页面还可以看到访问的次数
抓包结果,三次交互
5) 登录失败效果
此处输入的 username 不正确,所以登录失败
4.上传文件
4.1核心方法
HttpServletRequest 类方法
方法 | 描述 |
Part getPart(String name)
|
获取请求中给定
name
的文件
|
Collection<Part> getParts()
|
获取所有的文件
|
方法
|
描述 |
String getSubmittedFileName()
|
获取提交的文件名
|
String getContentType()
|
获取提交的文件类型
|
long getSize()
|
获取文件的大小
|
void write(String path)
|
把提交的文件数据写入磁盘文件
|
4.2 代码示例
<body>
<form action="upload" method="post" enctype="multipart/form-data">
<input type="file" name="MyImage">
<input type="submit" value="提交">
</form>
</body>

2) 创建 UploadServlet 类
@MultipartConfig //要给这个类加上这个注解,来开启对于上传文件的支持
//否则getPart调用的时候就会抛出异常!
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取请求给定 name 的名字
Part part = req.getPart("MyImage");
//获取提交的文件名
System.out.println(part.getSubmittedFileName());
//获取提交的文件类型
System.out.println(part.getContentType());
//获取文件的大小
System.out.println(part.getSize());
//把提交的文件数据并重命名写入磁盘文件
part.write("d:/fly/ff.jpg");
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("上传成功!");
}
}
上传效果
此时可以看到服务器端的打印日志