Servlet 开发学习
一、HelloWorld - Servlet 代码示例
0. 创建 Maven 项目,引入 Servlet 依赖
在 maven 仓库的官网查找 servlet,进入第一个搜索结果,找到版本 3.1 ,将 xml 文件格式复制到maven项目中 pom.xml 文件里,<dependencies>
标签下。
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
1. 创建目录
前两个是目录,最后一个是 xml 文件
然后在 web.xml
文件里面放入下面的内容:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>
2. 编写代码
- 创建一个类,继承
HttpServlet
public class HelloServlet extends HttpServlet {
}
- 重写
doGet
方法
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}
注意:super.doGet(req, resp);
在后面写代码的时候要删除掉,否则会有问题。
其中,doGet
方法的参数 HttpServletRequest
和 HttpServletResponse
这两个对象,分别代表着 HTTP 请求
和 HTTP 响应
(HTTP 协议格式中包含的信息,都在这两个对象里面)
doGet
方法不是我们手动调用的,而是 Tomcat
自动调用的。Tomcat 回自动识别出合适的时机,来自动调用 doGet 方法。(其中合适的时机比较复杂,但是能明确的是,一定是通过 GET
请求触发的!)
doGet
做的工作,就是 Tomcat
收到请求之后,到返回响应之前,中间的过程~
回顾下网络编程的过程:
- 读取请求并解析
- 根据请求计算响应
- 把响应返回给客户端
其中,步骤 1、3 是 Tomcat 自动完成的,步骤 2 是 doGet 完成的。
doGet
参数中的 HttpServletRequest
就是 Tomcat
根据收到的 HTTP 请求
,生成的对象。doGet
里面根据当前的业务逻辑,依据 HttpServletRequest
生成一个 HttpServletResponse
,Tomcat
再把 HttpServletResponse
对象构造成 HTTP 响应报文
,返回给客户端。
代码示例:
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 把自动生成的调用父类的方法要删掉,否则会有问题
//super.doGet(req, resp);
// 当前写一个 hello world!
// 这个打印是打印在服务器的控制台上
System.out.println("hello world!");
// 这个打印就是打印在响应报文中,显示到页面上
// 把 "hello world!" 字符串作为响应报文的 body 了,浏览器就会把这个 body 显示到页面中
resp.getWriter().write("hello world!");
}
}
注意:要加上 @WebServlet
注解
3. 打包
看到下面的字样,说明打包成功:
如果出错了,这里就会提示错误信息。
打包完成后,会生成一个 压缩包,在 target 目录下可以查看:
注意:生成的默认是 jar
包,但是 Tomcat 能识别的是一个 war
包。
所以此处需要修改 pom.xml
,让生成工作生成 war
包,在 <project>
标签下添加如下:
<packaging>war</packaging>
通过 packaging
标签来设置打包的格式~
也可以在 pom.xml
通过 build
标签中的 finalName
标签,设置一下生成包的名字,把名字改简单点(可选):
<build>
<finalName>hello_servlet</finalName>
</build>
通过上述操作,再进行一次打包:
4. 部署
上面的示例代码没有 main
方法,不能单独执行。main 方法在 Tomcat 里面,上述代码需要部署到 Tomcat 中,由 Tomcat 进行调用~
将上一步得到的 war
包,拷贝到 Tomcat 的 webapps
目录中
5. 启动 Tomcat
进入 Tomcat 目录中的 bin
目录,找到 startup.bat
并双击运行:
看到下面的字样,说明 Tomcat 启动完毕
启动 Tomcat 的时候,能在 Tomcat 的日志中,看到一个提示:
意思为:Tomcat 发现了这个 war 包,就会对这个 war 包进行解压缩和加载。
在 Tomcat 中的 webapps 目录下就会发现解压好的 war 包
6. 验证
通过浏览器构造 HTTP 请求访问 Tomcat 服务器。
URL路径 = Context Path(war 包名字) + Servlet Path(注解里写的路径)
在浏览器中输入http://127.0.0.1:8080/hello_servlet/hello
,得到结果:
我们对这个浏览器地址进行分析:
一旦你的请求的路径写错了(和服务器这里的情况不匹配)就会出现 404
二、更方便的部署 Tomcat
可以借助 IDEA 中的一个插件:Smart Tomcat
,来简化 打包 和 部署。
1. 安装插件
在 IDEA 的 setting 中:
2. 使用 Smart Tomcat 插件
点击工具栏下方的 add configuration
点击 +
,然后选择 Smart Tomcat
进入界面后:
创建完成后,点击绿色箭头运行即可:
在浏览器中打开下面的地址
并在后面加上 /hello
即可看到效果:
可以在控制台上看到:
smart tomcat 的工作原理,其实和前面的手动拷贝部署不太一样。
此时你打开 tomcat 的 webapps 目录,看不到刚才的 war 包。
其实 tomcat 的运行方式有很多种,smart tomcat 是在运行 tomcat 的时候,通过其他手段,让 tomcat 直接加载了你代码中的 webapp 目录。这个时候其实跳过了打包+拷贝的过程,到那时也起到了部署的效果。
三、Servlet API
重点掌握 HttpServlet
、HttpServletRequest
和 HttpServletResponse
1. HttpServlet
核心方法
这些方法都是可以在创建子类的时候,进行重写的,上述方法的调用时机是不同的。
init
:只会在该 Servlet 类第一次被使用的时候调用到,相当于是用来初始化的。destroy
:在 Servlet 对象被销毁的时候才会调用到,也只是调用一次,相当于收尾操作。service
:每次收到请求(无论是什么方法)都会调用 service。默认父类的 service 里面就会根据方法来调用 doGet/doPost....doGet/doPost....
:收到对应的 HTTP 方法的请求才会被调用。
前面已经通过浏览器里面输入 url 构造 get 请求了,post 请求咋办?
可以使用 form表单(只支持 get 和 post)和 ajax。
下面通过 ajax 来演示:
创建 test.html
放到 webapp 目录,不能随便乱放!
使用 ajax 需要 jquery 的引入
<!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>
<!-- src 放入jquery -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script>
$.ajax({
type: 'post',
url: 'hello', // 相对路径
// url: '/Servlet/hello' 绝对路径
success: function(body) {
console.log(body);
}
});
</script>
</body>
</html>
注意1: 在 ajax 中属性 url,'hello' 是相对路径的写法,要访问的是 /Servlet/hello 这个路径,这个 ajax 的代码是处在 /Servlet/test.html 里面。如何判断相对路径是相对谁呢?看当前的执行 ajax 的文件是在哪个路径下。
注意2: /Servlet/hello 主要是 /
开头,为绝对路径,就和当前文件所处位置无关了。但是 java 代码中的 @WebServlet
注解,里面的路径必须带/
,如果/
忘记了,tomcat 就会认为是无效路径,不能启动/加载。
在带有 @WebServlet("/hello")
的类中重写 doPost
方法:
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("doPost");
resp.getWriter().write("doPost");
}
这时候我们重启 Tomcat 服务
,然后在浏览器中输入地址:
把 html 放到 webapp 目录里的时候,此时该 html 也就相当于该 webapp 的一部分了。要访问这个页面,则必须要带上这里的 context path。
上面的页面是空的,此时我们 F12 打开控制台:
下面是简化过程:
在浏览器控制台上显示的 doPost,来自于 ajax 中的 success 方法(里面的打印),而 body 部分则来自服务器返回的数据(doPost 方法里面的)。
问题:浏览器输入url,返回的 body 是直接打印到界面上吗?
是的,直接通过浏览器输入 url 得到的响应的内容就是直接被浏览器渲染到界面上的。
但是 ajax 不是,拿到的响应数据,是由你的回调方法来处理,想让他显示就显示,不想显示就不显示。
比如,下面我们让他显示在浏览器上:
<!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>
<style>
.one {
font-size: 50px;
}
</style>
</head>
<body>
<div class="one">
</div>
<!-- src 放入jquery -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script>
$.ajax({
type: 'post',
url: 'hello', // 相对路径
// url: '/Servlet/hello' 绝对路径
success: function(body) {
// console.log(body);
let div = document.querySelector('.one');
div.innerHTML = body;
}
});
</script>
</body>
</html>
重启tomcat,打开浏览器:
当前使用 ajax 构造出上述请求,但还是比较麻烦(要写代码),我们还可以通过第三方工具来构造 HTTP 请求,如 Postman。
面试题:谈谈 HttpServlet 的生命周期
这题主要考察的就是什么时机调用什么方法。
- 首次使用,先调用一次 init
- 每次收到请求,调用 service,在 service 内部通过方法来决定调用哪个 doXXX
- 销毁之前调用 destory
2. HttpServletRequest
当 Tomcat 通过 Socket API 读取 HTTP 请求(字符串), 并且按照 HTTP 协议的格式把字符串解析成 HttpServletRequest 对象.
这个类就提供了一组方法,让我们能够获取到 HTTP 请求中的这些信息。
核心方法
注意 : 里面有个方法叫做 getRequestURI。
URL:唯一资源定位符。URI:唯一资源标识符。
这两个术语表达的含义类似,两个概念很多时候是重合的。url 表示资源的位置,uri 表示资源的 id。
String getQueryString()
:得到的是完整的查询字符串,如:?a=10&b=20
获取到其中的 a=10&b=20
下面的三个方法,相当于把 查询字符串 给解析成键值对:
Enumeration getParameterNames()
:得到所有的 key,以 Enum(枚举)的方式来表示。String getParameter(String name)
:根据 key 得到 value。String[] getParameterValues(String name)
:如果存在多个 key 相同的情况下,得到的value就是一个数组的形式。
下面的两个方法是获取请求报头(请求报头也是键值对):
Enumeration getHeaderNames()
:获取请求报头中所有的 key。String getHeader(String name)
:根据 key 获取 value。
String getCharacterEncoding()
:获取到请求的字符编码是什么。(其实字符编码,就包含在getContentType
里面)String getContentType()
:获取到整个 ContentType 的键值对,值里面可能包含 字符编码。int getContentLength()
:获取到 body 的长度。InputStream getInputStream()
:得到一个输入流对象。从这个对象中读取数据,其实就是读到了请求的body。请求body里面可能有些数据,可能会被这里的getInputStream
来获取。
注意:
关于 header,URL,这些方法,我们拿到的结果都是 String类型 的 数据。
至于有关 body 的方法,由于 body的长度可能会更长一些,我们并没有通过字符串方式来获取。
而是通过 流对象的方式,来获取。
下面通过代码示例来加深对上述方法的理解:
示例代码一:打印请求信息
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 java.io.IOException;
import java.util.Enumeration;
@WebServlet("/showRequest")
public class ShowRequestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 把 api 的执行结果,放到 StringBuilder 中
StringBuilder stringBuilder = new StringBuilder();
// 首行部分
stringBuilder.append("<h3> 首行部分</h3>");
stringBuilder.append(req.getProtocol()); // 获取 版本协议
stringBuilder.append("<br>"); // 换行
stringBuilder.append(req.getMethod()); // 获取 请求方法
stringBuilder.append("<br>"); // 换行
stringBuilder.append(req.getRequestURI()); // 获取 请求路径
stringBuilder.append("<br>"); // 换行
stringBuilder.append(req.getContextPath()); // 获取 请求的第一级路径
stringBuilder.append("<br>"); // 换行
stringBuilder.append(req.getQueryString()); // 获取 完整的查询字符串
stringBuilder.append("<br>"); // 换行
// header 部分
stringBuilder.append("<h3> header 部分</h3>");
Enumeration<String> headerNames = req.getHeaderNames(); // 获取报头中所有 key 值
// 使用迭代器方式来遍历 获取到的 header 中 key 值
while (headerNames.hasMoreElements()) {
// 获取到 headerNames 中的一个 key 元素
String headerName = headerNames.nextElement();
// 通过 header 中 key 值,获取到对应的 value
String headerValue = req.getHeader(headerName);
// 将 key 和 value 组成键值对,放入 stringbuilder
stringBuilder.append(headerName + ": " + headerValue + "<br>");
}
//在展示结果之前,需要指定浏览器的读取编码的方式,防止乱码
resp.setContentType("text/html; charset=utf-8");
resp.getWriter().write(stringBuilder.toString());
}
}
演示结果:
注意到此处为 null:,因为在目前构造的GET请求里是不包含查询字符串,所以结果为 null。接下来我们手动添加查询字符串:
示例代码2:获取 GET 请求中的参数
创建一个新类 GetParameterServlet
继承 HttpServlet 类
@WebServlet("/getParameter")
public class GetParameterServlet extends HttpServlet {
}
重写 doGet
方法,下面来演示获取到 GET 请求中的参数
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 我们手动从浏览器传来这样的一个请求:/getParameter/userId=1&classId=2
// 这里的查询字符串 userId=1&classId=2 就是键值对的结构
// 这里我们希望根据 key,拿到对应的 value
String userId = req.getParameter("userId"); // key 值需要和请求中的相匹配,才能获取对应的 value 值
String classId = req.getParameter("classId");
// 打印数据
resp.getWriter().write("userId: " + userId + " classId: " + classId);
}
重启 Tomcat,访问页面,得到如下结果:
如果我们不在 url 中添加 queryString,那么输出的值就是一个 null 值。
代码示例3:获取 POST 请求中的参数
前端除了通过 queryString 来传参,还有其他传参方式,如通过 post 请求,通过 body 来传参到服务器这里。
回顾下,POST 请求的 body 的格式:
application/x-www-form-urlencoded
form-data
application/json
接下来我们主要演示第一种和第三种格式:
1. 使用 x-www-form-urlencoded 请求格式
如果使用的是 x-www-form-urlencoded
的请求格式,服务器该如何获取参数呢?
获取参数的方式,跟 GET 是一样的,也是使用 getParameter
!
如何在前端构造一个 x-www-form-urlencoded
格式的请求呢?
有两种方式:
- 第三方工具 postman
- form 表单
创建一个类 PostGetParameterServlet
继承 HttpServlet
类
@WebServlet("/postGetParameter")
public class PostGetParameterServlet extends HttpServlet {
}
重写 doPost
方法处理 POST 请求
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 加入前端传过来的参数是 userId=1&classId=2
// 此时服务器也是通过 req.getParameter 来获取参数的
resp.setContentType("text/html; charset=utf-8"); // 指定浏览器读取方式
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
resp.getWriter().write("userId = " + userId + " classId = " + classId);
}
postman 构造 x-www-form-urlencoded 请求格式
重启 Tomcat 服务,打开 postman 构造请求:
通过 fiddler 抓包可以看到,约定 body 使用 application/x-www-form-urlencoded 来传参,它的数据是在body里面的:
form 表单构造 x-www-form-urlencoded 请求格式
在 webapp 目录下创建 html 文件 :test1.html:
使用 vscode 来编写文件:
<!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>使用 form 表单提交post请求</title>
</head>
<body>
<form action="postGetParameter" method="post">
<input type="text" name="userId">
<input type="text" name="classId">
<input type="submit" value="提交">
</form>
</body>
</html>
form表单里的action为相对路径
重启 Tomcat,打开网页:
我们在里面写上数据,然后点击提交按钮:
通过 fiddler 抓包:
2. 使用 json 请求格式
json 格式示例:
{
userId: 123,
classId: 456
}
对于body为json的格式来说,手动解析并不容易,因为 json 里面的字段是可以嵌套的,如下情况:
{
userId: 123,
classId: {
userId: 456
}
}
因为手动解析 json 数据很麻烦,我们可以借助第三方库来处理 json 格式的数据。
在 java 的生态中,处理 json 的第三方库的种类有很多。
我们使用的是 Jackson(Spring 官方推荐的库),Spring 默认就是使用 jackson
来处理 json 格式的数据。
我们通过 Maven 中央仓库,引入 Jackson。
在 Maven 仓库里搜索 Jackson
,然后选择第一个:
随便选择一个版本,复制其 xml 格式,此处我选择的是2.13.1:
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.1</version>
</dependency>
引入 Jackson:
使用 Jackson 的方式非常简单,只需要掌握两个操作:
- 把 json 格式的字符串 转成 java对象
- 把 java 对象 转成 json 字符串
这两个操作对应到一个类 ObjectMapper
(来自 Jackson),提供了两个方法:
objectMapper.readValue()
:json 字符串转成 Java 对象。objectMapper.writeValue()
:Java 对象转成 json 字符串。
示例:
创建一个新类 PostJsonServlet
继承 HttpServlet
@WebServlet("/postGetParameter2")
public class PostJsonServlet extends HttpServlet {
}
创建 Student 类:
class Student {
public int userId;
public int classId;
}
注意:
- 这里两个属性的名称,要和前端构造的 json 格式数据中的 key 要一致。否则下面进行 readValue 的时候,是无法转换成功的!
- 当前这个两个属性都设置成 public,如果设为 private,但是同时提供了 getter 和 setter 方法,效果是一样的。
重写 doPost
方法
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 创建一个 Jackson 的核心对象
ObjectMapper objectMapper = new ObjectMapper();
// 从请求的 body 中进行读取,并解析
// 使用 readValue 来吧 json 字符串转成 Java 对象
// 第一个参数是一个 String 或者是一个 InputStream
// 第二个参数是转换的结果对应的 java 类对象
Student student = objectMapper.readValue(req.getInputStream(),Student.class);
resp.getWriter().write(student.userId+", " + student.classId);
}
重启 Tomcat,使用Postman构造 json 请求:
问题:此时可能会出现一个疑惑:如何拿着 key 和 Student 类中的属性名作比较的呢?
注意第二个参数,这是一个类对象,类对象里面会存储这个 Student 类的详细信息。
这就是反射(根据类对象获取里面关键信息的过程),类对象就是我们的 .class
文件(字节码文件)
源代码是 .java
编译成 二进制字节码文件.class
,这个 .class
文件被加载到内存中的时候,就成了 class 对象,此对象包含了源代码中的关键信息,自然也会包含属性名称。
总代码:
class Student {
public int userId;
public int classId;
}
@WebServlet("/postGetParameter2")
public class PostJsonServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 创建一个 Jackson 的核心对象
ObjectMapper objectMapper = new ObjectMapper();
// 从请求的 body 中进行读取,并解析
// 使用 readValue 来吧 json 字符串转成 Java 对象
// 第一个参数是一个 String 或者是一个 InputStream
// 第二个参数是转换的结果对应的 java 类对象
Student student = objectMapper.readValue(req.getInputStream(),Student.class);
resp.getWriter().write(student.userId+", " + student.classId);
}
}
3. HttpServletResponse
核心方法
void sendRedirect(String location)
:返回一个重定向的响应,不是 set,而是 send,3xx 开头的响应。浏览器会自动的跳转到对应的新页面,String location
就是你要跳转到的页面。
PrintWriter getWriter()
:得到字符流OutputStream getOutputStream()
:得到字节流
得到流对象之后,下一步就是要 write。
代码示例1:设置状态码
创建一个新类 StatusCodeServlet
继承 HttpServlet
@WebServlet("/status")
public class StatusCodeServlet extends HttpServlet {
}
这里我们重写 doGet
方法
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(200); // 设置状态码
resp.getWriter().write("hello"); // 将 hello 写入到响应的 body
}
重启 Tomcat,启动浏览器
使用 Fiddler 抓包,查看响应报文
我们再来演示下设置状态码为 404
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(404); // 设置状态码
resp.getWriter().write("hello"); // 将 hello 写入到响应的 body
}
重启 Tomcat,打开浏览器
发现,返回的响应确实是 404,但是 hello 还是显示出来了。但是 404 不是表示 资源不存在/路径错误/路径缺失..的情况嘛,按理说是不显示任何数据的。
注意: 服务器返回的状态码,只是在告诉浏览器,当前的响应是一个什么状态,并不影响浏览器照常显示 body 中的内容。因此,不管响应的状态码是多少,都不会影响浏览器去显示 body 中的内容。
比如,我们打开 B站 一个不存在的页面:
显示了内容,通过抓包发现状态码是 404:
代码示例2:自动刷新
实现一个程序,让浏览器每秒自动刷新一次,并显示当前的时间戳。
实现这个功能,只需要给 HTTP 响应中设置一个 header:Refresh 即可。
创建新类 AutoRefreshServlet
继承 HttpServlet
@WebServlet("/autoRefresh")
public class AutoRefreshServlet extends HttpServlet {
}
这里我们重写 doGet
方法
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setHeader("Refresh","1"); // 每隔 1 s刷新一次响应内容
resp.getWriter().write("timeStamp: " + System.currentTimeMillis());
}
重启 Tomcat,打开浏览器
第二次刷新
通过抓包可以看到,这个页面一直在刷新:
看到响应报文,发现响应中就会存在一个 Refresh 的键值对,其 value 值,就是我们设置的秒数
也就是说,setHeader
就是在 请求报头中新增一个键值对。如果你想要添加其他的键值对,也是一样的操作,直接填入 key 和 value 即可。
代码示例3:重定向
实现一个程序,返回一个重定向的 HTTP 响应,自动跳转到另一个页面
下面来构造一个 302,让浏览器触发页面跳转。
创建新类 RedirectServlet
继承 HttpServlet
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
}
重写 doGet
方法
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(302);
resp.setHeader("Location","https://cn.bing.com");
}
重启 Tomcat,打开浏览器,下面是原页面:
输入路径后回车,直接跳转
抓包结果:
跳转更为简洁的写法:
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// resp.setStatus(302);
// resp.setHeader("Location","https://cn.bing.com");
// 更简洁的写法
resp.sendRedirect("https://cn.bing.com");
}
效果是一样的。
四、实战示例
实现服务器版本的表白墙 - 非持久化存储
先导入前端代码:
<!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>告白墙</title>
</head>
<body>
<!-- 通过内部样式style标签,引入CSS样式 -->
<style>
*{
/* 首先先去除浏览器样式 */
/* 将 内外边距设置为0,设置盒子模型为向内挤压 */
margin: 0;
padding: 0;
box-sizing: border-box;
}
.containner{
width: 100%;
}
h3{
/* 文本居中 */
text-align: center;
/* 上下边距为 20px,左右边距为0 */
padding: 20px 0;
font-size: 24px;
}
p{
text-align: center;
color: #666;
padding: 10px 0;
}
.row{
width: 400px;
height: 50px;
/* 上下外边距为零,左右外边距自适应 */
/* 就是元素水平居中操作 */
margin: 0 auto;
/* 弹性布局 */
display: flex;
/* 水平居中 */
justify-content: center;
/* 垂直居中 */
align-items: center;
}
.row span{
width: 60px;
font-size: 17px;
}
.row input{
width: 300px;
height: 40px;
line-height: 40px;
font-size: 20px;
text-indent: 0.5em;
outline: none;
}
.row #submit{
width: 360px;
height: 40px;
font-size: 20px;
line-height: 40px;
margin: 0 auto;
color: white;
background-color: orange;
border: none;
border-radius: 15px;
outline: none;
}
/* 当鼠标点击按钮的时候,会改变按钮颜色 */
.row #submit:active{
background-color: grey;
}
</style>
<div class="container">
<h3>表白墙</h3>
<p>输入后点击提交,会将信息显示在表格中</p>
<br>
<div class="row">
<span>谁: </span>
<input type="text">
</div>
<div class="row">
<span>对谁: </span>
<input type="text">
</div>
<div class="row">
<span>说什么: </span>
<input type="text">
</div>
<div class="row">
<button id="submit">提交</button>
</div>
</div>
<script>
let submitBtn = document.querySelector('#submit');
submitBtn.onclick = function(){
// 1、获取 3个input 文本框中的数据
let inputs = document.querySelectorAll('input');
let from = inputs[0].value;
let to = inputs[1].value;
let say = inputs[2].value;
if(from == ''|| to == '' || say == ''){
// 用户填写的数据,并不完整。所以不提交。
return;
}
// 2、生成一个新的 div,内容就是 三个 input 文本框的内容拼接
// 再把这个 元素,挂在DOM树上。
let newDiv = document.createElement('div');
newDiv.innerHTML = from + "对" + to +"说:" + say;
newDiv.className = 'row';
// 将新建节点,挂在 container 这个节点下面
let container = document.querySelector('.container');
container.appendChild(newDiv);
}
</script>
</body>
</html>
效果如下:
在写后端代码之前,我们首先与前端约定交互的接口,对于这个程序我们主要提供两个接口:
- 告诉服务器,当前留言了一条什么样的数据。
- 从服务器获取到当前的留言数据。
第一个接口的触发条件:
当用户点击 “提交”按钮的时候,就会发送一个 HTTP 请求,让服务器把这个数据存下来。
为了实现这个效果,客户端发送一个什么样的 HTTP 请求,服务器返回一个什么样的 HTTP 响应,我们做如下约定:
第二个接口的触发条件:
当页面加载的时候,需要从从服务器获取到曾经存储的消息内容。
约定如下:
现在来编写后端代码:
- 创建新类
messageWall
继承HttpServlet
@WebServlet("/message")
public class messageWall extends HttpServlet {
}
这里面的@WebServlet("/message")
路径不是乱写的,而是之前约定好的。
- 定义一个类
Message
用来读取 json 格式的数据:
class Message {
public String from;
public String to;
public String message;
}
- 编写第一个触发条件:当用户点击 “提交”按钮的时候,就会发送一个 HTTP 请求,让服务器把这个数据存下来。重写
doPost
方法:
// 用于读取请求并解析数据
private ObjectMapper objectMapper = new ObjectMapper();
// 用于存储信息
private List<Message> messages = new ArrayList<>();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 解析数据
Message message = objectMapper.readValue(req.getInputStream(),Message.class);
// 保存数据
messages.add(message);
// 返回响应,主要告诉页面,信息是否存入成功
// 因为内容比较简单,我就直接输出了
resp.setContentType("application/json; charset=utf-8"); // 约定浏览器返回数据的格式与读取方式
// 注意:如果不加上 setContentType,浏览器就会把下面返回的数据当作普通字符串来读取
// 我们通过 setContentType,告诉浏览器数据是一个 json 形式的数据,jQuery 的 ajax 就会将数据转换成 json 形式
resp.getWriter().write("{"ok": True}");
}
首先第一步获取POST请求中的 body 信息:
这里引入服务器的目的:当刷新页面之后,以往发送的信息记录不至于会被清空(丢弃),这些数据可以持久化的存储在服务器上,我们希望保存在服务器的内存中:
- 编写第二个触发条件:当页面加载的时候,需要从从服务器获取到曾经存储的消息内容。重写
doGet
方法:
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取整个消息列表中的元素,将其全部返回给客户端即可。
// 此处我们使用 ObjectMapper 把 Java 对象,转换成 json 形式字符串
String jsonString = objectMapper.writeValueAsString(messages);
resp.setContentType("application/json; charset=utf-8");
resp.getWriter().write(jsonString);
}
注意:ObjectMapper
既可以解析数据,又可以转换数据。readValue
将数据形式转换为指定形式。writeValueAsString
将参数中的数据转为字符串的格式。
后端总程序:
class Message {
public String from;
public String to;
public String message;
}
@WebServlet("/message")
public class messageWall extends HttpServlet {
// 用于读取请求并解析数据
private ObjectMapper objectMapper = new ObjectMapper();
// 用于存储信息
private List<Message> messages = new ArrayList<>();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取整个消息列表中的元素,将其全部返回给客户端即可。
// 此处我们使用 ObjectMapper 把 Java 对象,转换成 json 形式字符串
String jsonString = objectMapper.writeValueAsString(messages);
resp.setContentType("application/json; charset=utf-8");
resp.getWriter().write(jsonString);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 解析数据
Message message = objectMapper.readValue(req.getInputStream(),Message.class);
// 保存数据
messages.add(message);
// 返回响应,主要告诉页面,信息是否存入成功
// 因为内容比较简单,我就直接输出了
resp.setContentType("application/json; charset=utf-8"); // 约定浏览器返回数据的格式与读取方式
// 注意:如果不加上 setContentType,浏览器就会把下面返回的数据当作普通字符串来读取
// 我们通过 setContentType,告诉浏览器数据是一个 json 形式的数据,jQuery 的 ajax 就会将数据转换成 json 形式
resp.getWriter().write("{"ok": true}");
}
}
注意: 这里的 true 要小写的,不能写成 True。因为要使用 json 格式的true,而不能用 java 格式的 True。
启动Tomcat服务,然后使用 Postman 来测试下程序:
- 首先使用 POST 来提交数据
- 使用 GET 来读取数据
此时,服务器端口的代码就写完了!
现在来编写前端代码的页面,基于上面发的前端代码进行修改:
将前端代码命名为 message.html
放到 idea 里的 webapp 目录下:
- 首先引入 jquery
<script src="http://code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
- 实现第一个接口触发条件:当用户点击 “提交”按钮的时候,就会发送一个 HTTP 请求,让服务器把这个数据存下来。
// 4. 把当前获取到的输入框的内容,构造成一个 HTTP POST 请求
// 通过 ajax 发送给服务器
let body = {
// 为了方便区分 key 与 value,我给 key 加上了双引号
// 其实在 json 格式的数据中,key 一般都是要加上双引号的
// 也可以省略双引号
// 如果 key 带某些特殊字符,如空格,则必须带双引号
"from":from,
"to":to,
"message":say
}
$.ajax({
type:"post",
url:"message",
contentType:"application/json; charset=utf-8",
data: JSON.stringify(body),
success:function(body){
alert("消息提交成功!");
},
error:function(){
alert("消息提交失败!");
}
});
- 实现第二个触发条件:当页面加载的时候,需要从从服务器获取到曾经存储的消息内容。
// 当页面加载的时候,需要从从服务器获取到曾经存储的消息内容。
function getMessage() {
$.ajax({
type: "get",
url: "message",
success: function(body) {
}
})
}
当前的 body 已经是一个 js 对象数组了。ajax 会根据响应的 content Type 来自动进行解析。如果服务器返回的 content Type 已经是 application/json 了,ajax 就会把 body 自动转换成 js 对象。如果客户端没有自动转,也可以通过 JSON.parse 函数来手动转换。
拓展:
对象 和 JSON 字符串之间的转换,有两组:
- JAVA
objectMapper.readValue
把 json 字符串转成 java 对象
objectMapper.writeValueAsString
把 java 对象转成 json 字符串
- JS
JSON.parse
把 json 字符串转成 JS 对象
JSON.stringify
把 JS 对象转成 json 字符串
body 是一个数组了,我们接下来遍历它:
// 当页面加载的时候,需要从从服务器获取到曾经存储的消息内容。
function getMessage() {
$.ajax({
type: "get",
url: "message",
success: function(body) {
let container = document.querySelector(".container");
// 依次来获取数组中的每个元素
for(let message of body) {
// 创建一个 div 标签来存放一条记录(消息)
let newDiv = document.createElement('div');
// 将元素中的关键信息进行提取,然后构造成一个字符串
newDiv.innerHTML = message.from + "对" + message.to
+ "说: " + message.message; // 这里不是 say 了,而是对象中的message了
// 添加 css 样式
newDiv.className = 'row';
// 将新节点挂在 container 这个节点下面
}
}
})
}
// 加上函数调用,一旦访问页面就回去访问服务器,就把里面的数据显示出来
getMessage();
下面我们来看下,前后端代码结合的执行效果:
重启 Tomcat,先打开抓包工具 Fiddler,然后在浏览器中打开message.html
文件:
抓包结果:
那么问题来了,之前再测试代码的时候,已经 send 三条数据了,为什么现在没了?
这是因为我们重启了 Tomcat 服务器,而我们是将消息存储在服务器的内存中的,因此,随着服务器的重启,内存中的数据也就丢失了。
上述的逻辑操作,只能保证页面刷新后,数据不丢失,而不能保证服务器重启后,数据不丢失。
下面来看下网页效果:
来看下抓包结果:
接着提交第二次数据,下方的记录仍然存在:
只要服务器不重启,数据就会一直在。服务器重启,数据就会被清空。
前后端交互流程:
前端总程序
<!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>告白墙</title>
</head>
<body>
<!-- 通过内部样式style标签,引入CSS样式 -->
<style>
*{
/* 首先先去除浏览器样式 */
/* 将 内外边距设置为0,设置盒子模型为向内挤压 */
margin: 0;
padding: 0;
box-sizing: border-box;
}
.containner{
width: 100%;
}
h3{
/* 文本居中 */
text-align: center;
/* 上下边距为 20px,左右边距为0 */
padding: 20px 0;
font-size: 24px;
}
p{
text-align: center;
color: #666;
padding: 10px 0;
}
.row{
width: 400px;
height: 50px;
/* 上下外边距为零,左右外边距自适应 */
/* 就是元素水平居中操作 */
margin: 0 auto;
/* 弹性布局 */
display: flex;
/* 水平居中 */
justify-content: center;
/* 垂直居中 */
align-items: center;
}
.row span{
width: 60px;
font-size: 17px;
}
.row input{
width: 300px;
height: 40px;
line-height: 40px;
font-size: 20px;
text-indent: 0.5em;
outline: none;
}
.row #submit{
width: 360px;
height: 40px;
font-size: 20px;
line-height: 40px;
margin: 0 auto;
color: white;
background-color: orange;
border: none;
border-radius: 15px;
outline: none;
}
/* 当鼠标点击按钮的时候,会改变按钮颜色 */
.row #submit:active{
background-color: grey;
}
</style>
<div class="container">
<h3>表白墙</h3>
<p>输入后点击提交,会将信息显示在表格中</p>
<br>
<div class="row">
<span>谁: </span>
<input type="text">
</div>
<div class="row">
<span>对谁: </span>
<input type="text">
</div>
<div class="row">
<span>说什么: </span>
<input type="text">
</div>
<div class="row">
<button id="submit">提交</button>
</div>
</div>
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<script>
let submitBtn = document.querySelector('#submit');
submitBtn.onclick = function(){
// 1、获取 3个input 文本框中的数据
let inputs = document.querySelectorAll('input');
let from = inputs[0].value;
let to = inputs[1].value;
let say = inputs[2].value;
if(from == ''|| to == '' || say == ''){
// 用户填写的数据,并不完整。所以不提交。
return;
}
// 2、生成一个新的 div,内容就是 三个 input 文本框的内容拼接
// 再把这个 元素,挂在DOM树上。
let newDiv = document.createElement('div');
newDiv.innerHTML = from + "对" + to +"说:" + say;
newDiv.className = 'row';
// 将新建节点,挂在 container 这个节点下面
let container = document.querySelector('.container');
container.appendChild(newDiv);
// 3.清空之前输入框的内容
container.appendChild(newDiv);
for(let i = 0; i < inputs.length;i++) {
inputs[i].value = '';
}
// 4. 把当前获取到的输入框的内容,构造成一个 HTTP POST 请求
// 通过 ajax 发送给服务器
let body = {
// 为了方便区分 key 与 value,我给 key 加上了双引号
// 其实在 json 格式的数据中,key 一般都是要加上双引号的
// 也可以省略双引号
// 如果 key 带某些特殊字符,如空格,则必须带双引号
"from":from,
"to":to,
"message":say
}
$.ajax({
type:"post",
url:"message",
contentType:"application/json; charset=utf-8",
data: JSON.stringify(body),
success:function(body){
alert("消息提交成功!");
},
error:function(a,b){
console.log(a,b);
alert("消息提交失败!");
}
});
}
// 当页面加载的时候,需要从从服务器获取到曾经存储的消息内容。
function getMessage() {
$.ajax({
type: "get",
url: "message",
success: function(body) {
console.log(body);
let container = document.querySelector(".container");
// 依次来获取数组中的每个元素
for(let message of body) {
// 创建一个 div 标签来存放一条记录(消息)
let newDiv = document.createElement('div');
// 将元素中的关键信息进行提取,然后构造成一个字符串
newDiv.innerHTML = message.from + "对" + message.to
+ "说: " + message.message; // 这里不是 say 了,而是对象中的message了
// 添加 css 样式
newDiv.className = 'row';
// 将新节点挂在 container 这个节点下面
container.appendChild(newDiv);
}
}
})
}
// 加上函数调用,一旦访问页面就回去访问服务器,就把里面的数据显示出来
getMessage();
</script>
</body>
</html>
表白墙进阶:持久化存储
在上面的程序中,当服务器重启的时候,List 里面的内容就会丢失!如何解决这个问题呢?关键是要把数据放在服务器的 硬盘 上存储!
有两种方法:
- 存文件。使用 IO 流来写文件/读文件。(麻烦,不同场景下可能不适用)
- 数据库。使用 MySQL + JDBC。
下面我们使用数据库的方法来改进代码:
- 引入 MySQL 依赖
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
- 创建数据库和数据表
在 idea 的 main 目录下创建 sql 文件db.sql
# 创建数据库
create database messageWall;
# 使用数据库
use messageWall;
drop table if exists message;
# 创建表,对于字段为sql关键字的话,需要给字段加上反引号``
create table message(`from` varchar(100), `to` varchar(100), message varchar(100));
- 调整后端代码
- 删去
private List<Message> messages = new ArrayList<>();
代码
在 方法doGet 和 doPost,相应的操作也要删除
- 和数据库建立连接
单独创建一个类 DBUtil
,来实现与数据库的连接。
// 期望通过这个类来完成数据库的建立连接的过程
// 建立连接需要使用 DataSource,并且一个程序有一个 DataSource 实例即可。此处我们使用 单例模式 来实现。
public class DBUtil {
private static DataSource dataSource = null;
private static DataSource getDataSource() {
if (dataSource == null) {
dataSource = new MysqlDataSource();
((MysqlDataSource)dataSource).setURL("jdbc:mysql://127.0.0.1:3306/messageWall?characterEncoding=utf8&useSSL=false");
((MysqlDataSource)dataSource).setUser("root");
((MysqlDataSource)dataSource).setPassword("1234");
}
return dataSource;
}
public static Connection getConnection() throws SQLException {
return (Connection) getDataSource().getConnection();
}
public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {
// 此处建议分开写 try catch
// 保证及时反馈一个地方的 close 异常,而不会影响到其他的 close 的执行
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
- 封装数据库操作
在 messageWall
类中添加函数 save()
和 load()
把当前的信息存到数据库中save()
private void save(Message message) {
Connection connection = null;
PreparedStatement statement = null;
try {
// 1. 和数据库建立连接
connection = DBUtil.getConnection();
// 2. 构造 SQL 语句
String sql = "insert into messagewall.message values(?,?,?)";
statement = connection.prepareStatement(sql);
statement.setString(1,message.from);
statement.setString(2,message.to);
statement.setString(3,message.message);
// 3. 执行 sql 语句
int ret = statement.executeUpdate();
if (ret != 1) {
System.out.println("插入失败!");
} else {
System.out.println("插入成功!");
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 4. 关闭连接
DBUtil.close(connection,statement,null);
}
}
从数据库查询到记录load()
private List<Message> load() {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
List<Message> messageList = new ArrayList<>();
try {
// 1. 建立连接
connection = DBUtil.getConnection();
// 2. 构造 sql 语句
String sql = "select * from messageWall.message";
statement = connection.prepareStatement(sql);
// 3. 执行 sql
resultSet = statement.executeQuery();
// 4. 遍历结果集
while (resultSet.next()) {
Message message = new Message();
message.from = resultSet.getString("from");
message.to = resultSet.getString("to");
message.message = resultSet.getString("message");
messageList.add(message);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 5. 释放资源
DBUtil.close(connection,statement,resultSet);
}
return messageList;
}
- 修改相应的后端代码
效果
此时我们来查看效果,重启 Tomcat,进入浏览器:
一开始页面是空的,什么记录也没有,同时 sql 表中也没有记录:
现在我们插入数据:
查看数据表:
插入成功!
重启 Tomcat 打开浏览器(之前又提交了几次数据):
后端总代码:
class Message {
public String from;
public String to;
public String message;
}
@WebServlet("/message")
public class messageWall extends HttpServlet {
// 用于读取请求并解析数据
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取整个消息列表中的元素,将其全部返回给客户端即可。
// 此处我们使用 ObjectMapper 把 Java 对象,转换成 json 形式字符串
List<Message> messageList = load();
String jsonString = objectMapper.writeValueAsString(messageList);
resp.setContentType("application/json; charset=utf-8");
resp.getWriter().write(jsonString);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 解析数据
Message message = objectMapper.readValue(req.getInputStream(),Message.class);
// 保存数据
save(message);
// 返回响应,主要告诉页面,信息是否存入成功
// 因为内容比较简单,我就直接输出了
resp.setContentType("application/json; charset=utf-8"); // 约定浏览器返回数据的格式与读取方式
// 注意:如果不加上 setContentType,浏览器就会把下面返回的数据当作普通字符串来读取
// 我们通过 setContentType,告诉浏览器数据是一个 json 形式的数据,jQuery 的 ajax 就会将数据转换成 json 形式
resp.getWriter().write("{"ok": true}");
}
// 把当前的信息存到数据库中
private void save(Message message) {
Connection connection = null;
PreparedStatement statement = null;
try {
// 1. 和数据库建立连接
connection = DBUtil.getConnection();
// 2. 构造 SQL 语句
String sql = "insert into messagewall.message values(?,?,?)";
statement = connection.prepareStatement(sql);
statement.setString(1,message.from);
statement.setString(2,message.to);
statement.setString(3,message.message);
// 3. 执行 sql 语句
int ret = statement.executeUpdate();
if (ret != 1) {
System.out.println("插入失败!");
} else {
System.out.println("插入成功!");
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 4. 关闭连接
DBUtil.close(connection,statement,null);
}
}
// 从数据库查询到记录
private List<Message> load() {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
List<Message> messageList = new ArrayList<>();
try {
// 1. 建立连接
connection = DBUtil.getConnection();
// 2. 构造 sql 语句
String sql = "select * from messageWall.message";
statement = connection.prepareStatement(sql);
// 3. 执行 sql
resultSet = statement.executeQuery();
// 4. 遍历结果集
while (resultSet.next()) {
Message message = new Message();
message.from = resultSet.getString("from");
message.to = resultSet.getString("to");
message.message = resultSet.getString("message");
messageList.add(message);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 5. 释放资源
DBUtil.close(connection,statement,resultSet);
}
return messageList;
}
}
五、Cookie 和 Session
关于 Cookie 的介绍可以看这篇文章的内容 www.yuque.com/justencount…
理解会话机制(Session)
服务器同一时刻收到的请求是很多的. 服务器需要区分清楚每个请求是从属于哪个用户, 就需要在服务器这边记录每个用户令牌以及用户的信息的对应关系。
会话的本质就是一个 "哈希表", 存储了一些键值对结构. key 就是令牌ID(token/sessionId), value 就是用户信息(用户信息可以根据需求灵活设计).
sessionId 是由服务器生成的一个“唯一性字符串”,从 session 机制的角度来看,这个唯一性字符串称为“sessionId”。但是站在整个登录流程中看待,也可以把这个唯一性字符串称为“token”。
Cookie 和 Session 的区别
- Cookie 是客户端的机制,Session 是服务器端的机制。
- Cookie 和 Session 经常会在一起配合使用,但是不是必须配合。
-
- 完全可以用 Cookie 来保存一些数据在客户端,这些数据不一定是用户身份信息, 也不一定是 token / sessionId
- Session 中的 token / sessionId 也不需要非得通过 Cookie / Set-Cookie 传递
核心方法
HttpServletRequest 类中相关方法
方法 | 描述 |
---|---|
HttpSession getSession() | 在服务器中获取会话. 参数如果为 true, 则当不存在会话时新建会话; 参数如果为 false, 则当不存在会话时返回 null |
Cookie[] getCookies() | 返回一个数组, 包含客户端发送该请求的所有的 Cookie 对象. 会自动把Cookie 中的格式解析成键值对. |
HttpServletResponse 类中相关方法
方法 | 描述 |
---|---|
void addCookie(Cookie cookie) | 把指定的 cookie 添加到响应中 |
HttpSession 类中相关方法
一个 HttpSession 对象里面包含多个键值对. 我们可以往 HttpSession 中存任何我们需要的信息 。
方法 | 描述 |
---|---|
Object getAttribute(String name) | 该方法返回在该 session 会话中具有指定名称的对象,如果没有指定名称的对象,则返回 null. |
void setAttribute(String name, Object value) | 该方法使用指定的名称绑定一个对象到该session 会话 |
boolean isNew() | 判定当前是否是新创建出的会话 |
Cookie 类中相关方法
每个 Cookie 对象就是一个键值对。
方法 | 描述 |
---|---|
String getName() | 该方法返回 cookie 的名称。名称在创建后不能改变。(这个值是 SetCooke 字段设置给浏览器的) |
String getValue() | 该方法获取与 cookie 关联的值 |
void setValue(String newValue) | 该方法设置与 cookie 关联的值 |
实战案例:网页登录
登录界面
<!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>登录页面</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>
服务器程序:处理登录页面发送的请求
类名为 loginServlet
,路径为/login
// 处理登录页请求
@WebServlet("/login")
public class loginServlet extends HttpServlet {
// 由于请求的 form 中的一个 POST 请求,因此这里使用 doPost 来处理
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf8");
// 读取请求中的参数,判断当前用户的身份信息是否正确
String username = req.getParameter("username");
String password = req.getParameter("password");
if (username == null || username.equals("") || password == null || password.equals("")) {
resp.getWriter().write("用户名或密码不完整!登录失败!");
return;
}
// 验证用户名和密码是否正确,一般来说这些东西是存在数据库的
if (!username.equals("zhangsan") || !password.equals("123")) {
// 登录失败
resp.getWriter().write("用户名或密码错误!登录失败!");
return;
}
// 登录成功
// 需要一个 会话(session)把用户信息存进去
HttpSession session = req.getSession(true);
session.setAttribute("username","zhangsan");
session.setAttribute("password","123");
// 第二个参数是 Object,虽然写的是0,但是自动装箱成 Integer了,所以取的时候也是Integer
Integer visitCount = (Integer)session.getAttribute("visitCount");
if (visitCount == null) {
session.setAttribute("visitCount",0);
}
// 页面跳转到登录页面
resp.sendRedirect("index");
}
}
如果会话存在,直接获取(根据请求的 cookie 中的 sessionId 来查哈希表)
如果会话不存在,则会创建新的。
由于是新用户登录,因此这里的 cookie 中没有 sessionId,所以就查询不到,也就需要创建出新的会话对象。
同时 getSession 还会生成一个 sessionId 并且把这个 sessionId 作为 Set-Cookie 中的字段返回给浏览去保存。
问题:一个 sessionId 可以对应多个键值对吗?
一个 sessionId 对应到一个 HttpSession 对象,一个 HttpSession 对象里可以包含多个键值对。
每个登录的用户都有自己的会话,有多少个用户,就有多少个 session 对象。
服务器程序:返回主页信息
类名为 indexServlet
,路径为 /index
// 用这个 servlet 返回主页信息
@WebServlet("/index")
public class indexServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 要做的工作,把当前用户信息展示到页面上
HttpSession session = req.getSession(false);
if (session == null) {
// 用户未登录,则跳转到登录页面,要求用户重新登录
resp.sendRedirect("login.html");
return;
}
// 已经登录成功过,就会获取到会话中的数据
// 由于 getAttribute 返回的是 Object,需要强转
String username = (String)session.getAttribute("username");
Integer visitCount = (Integer)session.getAttribute("visitCount");
visitCount = visitCount+1;
session.setAttribute("visitCount",visitCount);
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("当前用户为:" + username + " 访问次数:"+visitCount);
}
}
效果展示
重启 Tomcat,打开浏览器:
代码基本流程
fiddler 抓包情况
第一次请求:获取到 login.html 页面
初始情况下,没有 cookie
初始情况的响应,也是没有Set-Cookie的
这些是未登录状态,不会涉及到任何用户身份信息。
第二次请求:输入用户名和密码,点击登录
这个Post请求中也没有 cookie
但是登录成功之后,响应报文则是由 set-cookie
key名字为 JSESSIONID(servlet自动生成的key的名字)
value为十六进制数字,这个就是浏览器生成的 sessionId
此时这个sessionId就会保存到浏览器中:
这个代码,实现了创建会话,生成 sessionId 并且把 sessionId 通过 Set-Cookie 返回这个操作
第三次请求:登录成功之后,自动重定向访问主页
可以看到此处有 cookie,这个 cookie 就是上次响应 set-cookie 中的内容。
后续再反复请求,请求中都会带上这个 cookie 了。
六、上传文件
上传文件也是日常开发中的一类常见需求. 在 Servlet 中也进行了支持。
核心方法
HttpServletRequest 类相关方法
方法 | 描述 |
---|---|
Part getPart(String name) | 获取请求中给定 name 的文件 |
Collection getParts() | 获取所有的文件 |
,这个
name
不是上传文件的真实文件名,而是 html 里写的一个 name
的属性值。
最常用的为 getPart
方法:
上传文件的时候,在前端需要使用 form 表单。form 表单中需要使用特殊的类型 form-data
此时提交文件的时候,浏览器就会把文件内容以 form-data
的格式构造到 HTTP 请求中。然后,服务器就可以通过 getPart
来获取了。
注意:一个 HTTP 请求,可以一次性提交多个文件的。
每个文件都称为一个 Part
,每个 Part
都有 name
(身份标识)。服务器代码中就可以根据 name
找到对应的 Part
了。
基于这个 Part 就可以进一步来获取文件信息,进行下一步的操作了。
Part 类相关方法
方法 | 描述 |
---|---|
String getSubmittedFileName() | 获取提交的文件名 |
String getContentType() | 获取提交的文件类型 |
long getSize() | 获取文件的大小 |
void write(String path) | 把提交的文件数据写入磁盘文件 |
简单案例:上传一个图片文件
前端代码
在 webapp 目录下创建 upload.html
<!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>上传文件</title>
</head>
<body>
<form action="upload" method="post" enctype="multipart/form-data">
<input type="file" name="MyFile">
<input type="submit" name="提交">
</form>
</body>
</html>
注意:写 form 表单时,要加上,这个语句单独给 “上传文件”提供的。
中,
type
属性为 file
,name
属性命名为 "MyFile"(此名字可以随便命名,用于和后端的交互)
前端效果:
后端:处理上传请求
创建 UploadServlet
类
@MultipartConfig
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Part part = req.getPart("MyFile");
// 获取文件的真实名字
System.out.println(part.getSubmittedFileName());
// 获取文件大小
System.out.println(part.getSize());
// 获取文件类型
System.out.println(part.getContentType());
// 把文件写入到服务器这边的磁盘中
part.write("e:/result.jpg");
resp.getWriter().write("upload ok!");
}
}
注意:
-
此处的 "MyFile" 正是前端代码命名的:
-
要加注解
@MultipartConfig
,如果不加这个注解,getPart
方法是无法正常工作的,会在页面上抛出异常。这个注解是用来开启对上传文件的支持的。
效果
点击”提交“按钮后:
查看后端控制台:
分别为文件名,文件大小,文件类型。
查看服务器磁盘:
注意:
我们通过代码将图片写入了 E 盘,在本地保存了文件。由于当下这里的服务器和浏览器是在同一个主机上的,此时文件上传看起来没什么效果。如果服务器和浏览器在不同的主机上,上传效果就非常明显了。
fiddler 抓包
请求报文:
响应报文:
转载自:https://juejin.cn/post/7170267553187495944