Servlet API 详解

逐梦北京 / 2023-05-19 / 原文

目录

1. Servlet 运行原理

1.1 Tomcat 的定位

1.2 Tomcat 的伪代码

1.3 总结

2. Servlet API 详解

2.1 HttpServlet

2.2 HttpServletRequest

2.2 HttpServletResponse

3.Cookie和Session

3.1 Cookie

3.2 理解会话机制 (Session)

3.3 Cookie 和 Session 的区别

3.4 核心方法

3.4 实现登录页面

4.上传文件

4.1核心方法

4.2 代码示例

1. Servlet 运行原理
在 Servlet 的代码中我们并没有写 main 方法 , 那么对应的 doGet 代码是如何被调用的呢 ? 响应又是如何返回给浏览器的?
这其中就涉及到了 Tomcat 。
1.1 Tomcat 的定位
我们自己的实现是在 Tomcat 基础上运行的。

当浏览器给服务器发送请求的时候 , Tomcat 作为 HTTP 服务器 , 就可以 接收到这个请求。 HTTP 协议作为一个应用层协议 ,, 需要底层协议栈来支持工作。 如下图所示 :

更详细的交互过程可以参考下图:

 

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 方法中, 就执行到了我们自己的代码. 我们自己的代码会根据请求中的一

些信息 , 来给 HttpServletResponse 对象设置一些属性 . 例如状态码 , header, body 等。
3) 返回响应
-我们的 doGet / doPost 执行完毕后 , Tomcat 就会自动把 HttpServletResponse 这个我们刚设置
好的对象转换成一个符合 HTTP 协议的字符串 , 通过 Socket 把这个响应发送出去
-此时响应数据在服务器的主机上通过网络协议栈层层 封装 , 最终又得到一个二进制的 bit 流 , 通过
物理层硬件设备转换成光信号 / 电信号传输出去。
-这些承载信息的光信号 / 电信号通过互联网上的一系列网络设备 , 最终到达浏览器所在的主机 ( 这个
过程也需要网络层和数据链路层参与 )。
-浏览器主机收到这些光信号 / 电信号 , 又会通过网络协议栈逐层进行 分用 , 层层解析 , 最终还原成
HTTP 响应 , 并交给浏览器处理。
-浏览器也通过 Socket 读到这个响应 ( 一个字符串 ), 按照 HTTP 响应的格式来解析这个响应 . 并且把
body 中的数据按照一定的格式显示在浏览器的界面上。
1.2 Tomcat 的伪代码
所谓 "伪代码", 并不是一些语法严谨, 功能完备的代码, 只是通过这种形式来大概表达某种逻辑。
1) 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();
}
}
在上述代码中:

Tomcat 的代码中内置了 main 方法 . 当我们启动 Tomcat 的时候 , 就是从 Tomcat 的 main 方法开
始执行的。
被@webServlet 注释修饰的类会在Tomcat 启动的时候就被获取到,并集中管理。
Tomcat 通过 反射这样的语法机制来创建被 @webServlet 注解修饰的类的实例。
这些实例被创建完了之后 , 会点调用其中的 init 方法进行初始化。 ( 这个方法是 HttpServlet 自带的 ,
我们自己写的类可以重写 init)
这些实例被销毁之前 , 会调用其中的 destory 方法进行收尾工作。 ( 这个方法是 HttpServlet 自带的 ,
我们自己写的类可以重写 destory)
Tomcat 内部也是通过 Socket API 进行网络通信。
Tomcat 为了能同时相应多个 HTTP 请求 , 采取了多线程的方式实现 . 因此 Servlet 是运行在 多线程
环境 下的。
2) Tomcat 处理请求流程
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 页面,表示服务器内部错误
}
}
}
在上述代码中

Tomcat 从 Socket 中读到的 HTTP 请求是一个字符串 , 然后会按照 HTTP 协议的格式解析成一个
HttpServletRequest 对象。
Tomcat 会根据 URL 中的 path 判定这个请求是请求一个静态资源还是动态资源。 如果是静态资源 ,
直接找到对应的文件把文件的内容通过 Socket 返回。 如果是动态资源 , 才会执行到 Servlet 的相关
逻辑。
Tomcat 会根据 URL 中的 Context Path 和 Servlet Path 确定要调用哪个 Servlet 实例的 service
方法, 通过 service 方法 , 就会进一步调用到我们之前写的 doGet 或者 doPost
3) Servlet 的 service 方法的实现
Servlet 的 service 方法内部会根据当前请求的方法 , 决定调用其中的某个 doXXX 方法 .
在调用 doXXX 方法的时候 , 就会触发 多态 机制 , 从而执行到我们自己写的子类中的 doXXX 方法。
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
我们写 Servlet 代码的时候 , 首先第一步就是先创建类 , 继承自 HttpServlet, 并重写其中的某些方法。
HttpServlet 的实例只是在程序启动时创建一次, 而不是每次收到 HTTP 请求都重新创建实例。

核心方法

实 际开发的时候主要通过继承这个HttpServlet 类,重写 doXXX 方法, 来被Tomcat执行到。其中很少会重写 init / destory / service。
通过继承、重写方法,其中就涉及到了多态,如下


代码示例: 处理 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 响应");
}
}
构造POST请求
引入<script>标签依赖 https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js
其中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
Tomcat 通过 Socket API 读取 HTTP 请求 ( 字符串 ), 并且按照 HTTP 协议的格式把字符串解析成
HttpServletRequest 对象。
HttpServletRequest对应到一个HTTP请求,HTTP请求有什么,这里就有什么
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 对象 .
通过这些方法可以获取到一个请求中的各个方面的信息。
注意:请求对象是服务器收到的内容, 不应该修改。因此上面的方法也都只是 "读" 方法, 而不是 "写" 方法。
(1)代码示例 : 打印请求信息
@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());
}

}

上述API能够让我们拿到HTTP请求的各个方面内容。
但是却没那么常用,更常用的,其实是getParameter这个方法。 (获取到query string中的详细内容)

读取到的信息如下

(2) 代码示例: 获取 GET 请求中的参数

GET 请求中的参数一般都是通过 query string 传递给服务器的。
形如https://v.bitedu.vip/personInf/student?userId=123&classId=456,此时浏览器通过 query string 给服务器传递了两个参数 , userId 和 classId, 值分别是 123 和 456。在服务器端就可以通过getParameter 来获取到参数的值。
创建 GetParameterServlet 类
@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 给服务器没传参和传参的页面响应,参数是可以手动输入的。


(3)代码示例 : 获取 POST 请求中的参数
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);
}
}
获取结果

 

(4)代码示例 : 获取 POST 请求中的参数 (2)
要想构造一个 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
Servlet 中的 doXXX 方法的目的就是根据请求计算得到相应 , 然后把响应的数据设置到
HttpServletResponse 对象中。
然后 Tomcat 就会把这个 HttpServletResponse 对象按照 HTTP 协议的格式 , 转成一个字符串 , 并通过 Socket 写回给浏览器。
核心方法
方法
描述
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)代码示例: 自动刷新

实现一个程序 , 让浏览器每秒钟自动刷新一次 . 并显示当前的时间戳。
创建 AutoRefreshServlet 类
@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());
}
}
通过 HTTP 响应报头中的 Refresh 字段,可以控制浏览器自动刷新的时机。
通过 Date 类的 getTime 方法可以获取到当前时刻的毫秒级时间戳。

(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
HTTP 协议自身是属于 " 无状态 " 协议。
" 无状态 " 的含义指的是 :
默认情况下 HTTP 协议的客户端和服务器之间的这次通信 , 和下次通信之间没有直接的联系。

举个例子 :
1. 到了医院先挂号: 挂号时候需要提供身份证 , 同时得到了一张 " 就诊卡 ", 这个就诊卡就相当于
患者的 " 令牌 ".
2. 后续去各个科室进行检查 , 诊断 , 开药等操作 , 都不必再出示身份证了 , 只要凭就诊卡即可识别
出当前患者的身份。
3. 看完病了之后 , 不想要就诊卡了 , 就可以注销这个卡 . 此时患者的身份和就诊卡的关联就销毁
了 .。( 类似于网站的注销操作 )
4. 又来看病 , 可以办一张新的就诊卡 , 此时就得到了一个新的 " 令牌 "。
此时在服务器这边就需要记录令牌信息 , 以及令牌对应的用户信息 , 这个就是 Session 机制所做的工作。
3.2 理解会话机制 (Session)
服务器同一时刻收到的请求是很多的。 服务器需要清除的区分清楚每个请求是从属于哪个用户, 就需要在服务器这边记录每个用户令牌以及用户的信息的对应关系。
在上面的例子中 , 就诊卡就是一张 " 令牌 ". 要想让这个令牌能够生效 , 就需要医院这边通过系统记录
每个就诊卡和患者信息之间的关联关系。
会话的本质就是一个 " 哈希表 ", 存储了一些键值对结构。 key 就是令牌的 ID(token/sessionId),value 就是用户信息( 用户信息可以根据需求灵活设计。
sessionId 是由服务器生成的一个 " 唯一性字符串 ", 从 session 机制的角度来看 , 这个唯一性字符串
称为 "sessionId". 但是站在整个登录流程中看待 , 也可以把这个唯一性字符串称为 "token"。
sessionId 和 token 就可以理解成是同一个东西的不同叫法 ( 不同视角的叫法 )。

当用户登陆的时候 , 服务器在 Session 中新增一个新记录 , 并把 sessionId / token 返回给客户端 .
( 例如通过 HTTP 响应中的 Set-Cookie 字段返回 )。
客户端后续再给服务器发送请求的时候 , 需要在请求中带上 sessionId/ token. ( 例如通过 HTTP 请
求中的 Cookie 字段带上 )。
服务器收到请求之后 , 根据请求中的 sessionId / token 在 Session 信息中获取到对应的用户信息 ,
再进行后续操作。
Servlet 的 Session 默认是保存在内存中的 . 如果重启服务器则 Session 数据就会丢失。
3.3 Cookie 和 Session 的区别
1. Cookie 是客户端的机制。 Session 是服务器端的机制。
2. Cookie 和 Session 经常会在一起配合使用。 但是不是必须配合。
完全可以用 Cookie 来保存一些数据在客户端 . 这些数据不一定是用户身份信息 , 也不一定是
token / sessionId Session 中的 token / sessionId 也不需要非得通过 Cookie / Set-Cookie 传递
3.4 核心方法
HttpServletRequest 类中的相关方法

方法
描述
HttpSession
getSession()
在服务器中获取会话 . 参数如果为 true, 则当不存在会话时新建会话 ; 参数如果为 false, 则当不存在会话时返回 null
Cookie[]
getCookies()
返回一个数组 , 包含客户端发送该请求的所有的 Cookie 对象 . 会自动把Cookie 中的格式解析成键值对。
Cookie[] getCookies():HTTP请求中的cookie字段就是按照键值对的方式来组织的。这里的这些键值对,大概的格式,使用;来分割多个键值对,使用=来分割键和值。这些键值对都会在请求中通过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 关联的值
HTTP 的 Cooke 字段中存储的实际上是多组键值对 . 每个键值对在 Servlet 中都对应了一个 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.上传文件
上传文件也是日常开发中的一类常见需求 . 在 Servlet 中也进行了支持。
4.1核心方法
HttpServletRequest 类方法

方法 描述
Part getPart(String name)
获取请求中给定 name 的文件
Collection<Part> getParts()
获取所有的文件
Part 类方法
方法
描述
String getSubmittedFileName()
获取提交的文件名
String getContentType()
获取提交的文件类型
long getSize()
获取文件的大小
void write(String path)
把提交的文件数据写入磁盘文件
4.2 代码示例
实现程序 , 通过网页提交一个图片到服务器上
1) 创建 upload.html 放到 webapp 目录中。

<body>
<form action="upload" method="post" enctype="multipart/form-data">
<input type="file" name="MyImage">
<input type="submit" value="提交">
</form>
</body>
上传文件一般通过 POST 请求的表单实现

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("上传成功!");
}
}

上传效果

 

在 d 盘中生成了 ff.jpg


此时可以看到服务器端的打印日志


————————————————
版权声明:本文为CSDN博主「Fly upward」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_60494863/article/details/125142108