这篇博客,是基于上一篇博客对Servlet 知识的拓展。有兴趣的可以看一下。
一、安装 Smart Tomcat 插件
对于上篇博客讲到,将Tomcat和Servlet 中的代码联系起来,具体的步骤比较琐碎,但是并不复杂。需要创建目录、打包、部署程序到webapp 中 等操作,是比较麻烦的。
而引入了IDEA 中带有的 smart Tomcat 插件,对我们提高开发效率有一定的帮助。IDEA专业版自带有该插件,社区版就需要自行在IDEA中下载。
在File栏中打开Setting:搜索smart Tomcat进行安装。 下载好smart Tomcat插件后,需要进行配置。 点击上图的按钮,会弹出一个界面,点击左上角的+ 号,选中smart Tomcat选项: 点击smart Tomcat选项后,还会有下图的界面,点击OK后,就会出现右上角那样的图标了: 需要注意的是:Context Path是上下文的意思,能够确定是哪个webapp,在访问Servlet代码的的时候会用到。 注:Toomcat不是 IDEA 的一部分,它们两个是完全互不相干的进程。 IDEA中一点击就能运行并且现显示 Tomcat的日志,这个过程其实是 IDEA 这个进程,调用了Tomcat 进程(进程创建+程序替换),IDEA 把Tomcat 的输出内容重定向到自己的终端窗口中。 我们写好了相关的代码后,运行就会自动地部署程序到Tomcat中,在IDEA 终端里面有个路径,可以打开去看一下该路径下有什么。 打开该路径的文件,我们可以发现,在我们的用户目录底下,它创建了一个.SmartTomcat 目录,里面存放的有很多子目录,都是包含着跟Servlet代码有关的文件。 实际上,Smart Tomcat这个插件的运行原理,并没有打包,而是只是给这个项目,创建了一个单独的目录,把当前正在运行的Tomcat 给临时复制了一个副本,来运行当前正在编辑的代码。 因此我们在webapp 中是看不到war 包的。
二、对于浏览器中的访问出错
代码:
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("GET 请求");
}
}
1.出现 404
最大的原因,就是路径写错了:
a) 没有写上下文,即java100_Servlet ,这个在我们配置smart Tomcat的时候设置的。 b) 写了上下文没有写与代码中@WebServlet注释中写的路径,就没法将HT特定的HTTP 请求和代码相关联。 c) URL中写的最后的路径与代码中注释的路径不匹配,也会出现404 . d) 如果 web.xml 配置错误,也会出现404 ,但是这个是复制上去的,一般不会错。
2.出现405
405的主要原因:请求的方法和代码中重写的方法对不上。
a)在浏览器中访问Tomcat上的资源是GET 方法,而Servlet中的doPost方法是处理Post方法的。 b) 在重写doGet 方法时,编译器会自动地调用父类的doGet方法。父类中的doGet代码中,就是直接地返回405.
3.出现500
主要原因是:代码中抛出异常了,页面上或者Tomcat 日志中都会明确提示出异常的信息调用栈等详细信息。
将代码改为: 字符串为空去求长度是不可以的,就会抛出异常。
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String str = null;
int n = str.length();
resp.getWriter().write("GET 请求");
}
}
需要注意的是:实际开发中不要把错误信息直接显示在页面上。
4.出现空白页
出现空白页的原因是:在Servlet代码中什么都没有实现。即 如果把doGet方法内的唯一一条代码注释掉,就会出现空白页。
5.无法访问此网站
这个的主要原因是:Tomcat启动失败。 将注释变为hello,而不是/hello ,再去打包程序的时候,就会出错了。此时再去访问Tomcat的资源,就会显示无法访问。
页面错误: 部署时的错误提示,一般在终端有很多的信息时,错误的提示都在最上方。
三、Servlet运行原理
在 Servlet 的代码中我们并没有写 main 方法, 那么对应的 doGet 代码是如何被调用的呢? 响应又是如何返回给浏览器的?
这个代码是基于在Tomcat 的基础上运行的。 上面的流程图,讲的是:Web Browser 是客户端,通过HTTP 协议发给HTTP 服务器,即Tomcat。Tomcat 拿到了HTTP 请求后,就会对请求进行解析,生成一个 HTTPServletRequest 对象。我们调用Servlet 类,来执行程序员写好的逻辑(Servlet Program),此时还有可能会连接到数据库。
更详细的过程:
- 接收请求:
- 用户在浏览器输入一个 URL, 此时浏览器就会构造一个 HTTP 请求.
- 这个 HTTP 请求会经过网络协议栈逐层进行 封装 成二进制的 bit 流, 最终通过物理层的硬件设备转换成光信号/电信号传输出去.
- 这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达目标主机(这个过程也需要网络层和数据链路层参与).
- 服务器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成HTTP 请求. 并交给 Tomcat 进程进行处理(根据端口号确定进程)
- Tomcat 通过 Socket 读取到这个请求(一个字符串), 并按照 HTTP 请求的格式来解析这个请求, 根据请求中的 Context Path 确定一个 webapp, 再通过 Servlet Path 确定一个具体的 类. 再根据当前请求的方法 (GET/POST/…), 决定调用这个类的 doGet 或者 doPost 等方法. 此时我们的代码中的doGet / doPost 方法的第一个参数 HttpServletRequest 就包含了这个 HTTP 请求的详细信息。
-
根据请求计算响应 在我们的 doGet / doPost 方法中, 就执行到了我们自己的代码. 我们自己的代码会根据请求中的一些信息, 来给 HttpServletResponse 对象设置一些属性. 例如状态码, header, body 等 -
返回响应:
- 我们的 doGet / doPost 执行完毕后, Tomcat 就会自动把 HttpServletResponse 这个我们刚设置好的对象转换成一个符合 HTTP 协议的字符串, 通过 Socket 把这个响应发送出去.
- 此时响应数据在服务器的主机上通过网络协议栈层层 封装, 最终又得到一个二进制的 bit 流, 通过物理层硬件设备转换成光信号/电信号传输出去.
- 这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达浏览器所在的主机(这个过程也需要网络层和数据链路层参与).
- 浏览器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成HTTP 响应, 并交给浏览器处理.
- 浏览器也通过 Socket 读到这个响应(一个字符串), 按照 HTTP 响应的格式来解析这个响应. 并且把body 中的数据按照一定的格式显示在浏览器的界面上
Servlet 的伪代码,可以自己去查看,只是了解Servlet运行的逻辑。
四、Servlet API 详解
1. HttpServlet
我们写 Servlet 代码的时候, 首先第一步就是先创建类, 继承自 HttpServlet, 并重写其中的某些方法。
核心方法:
方法名称 | 调用时机 |
---|
init | 在 HttpServlet 实例化之后被调用一次 | destory | 在 HttpServlet 实例不再使用的时候调用一次 | service | 收到 HTTP 请求的时候调用 | doGet | 收到 GET 请求的时候调用(由 service 方法调用) | doPost | 收到 POST 请求的时候调用(由 service 方法调用) | doPut/doDelete/doOptions/… | 收到其他请求的时候调用(由 service 方法调用) |
继承HTTPServlet 是为了重写该类里面的一些方法,重写方法的目的是为了能够把程序员定义的逻辑给插入到Tomcat 这个“框架”中,好让Tomcat 进行调用。
类似于这样的操作,在前面也是见过的。 例如:Comparable、Comparable,它们是我们重写了里面的CompareTo方法和Compare方法,是根据我们自己的逻辑去执行代码,调用是该接口自己根据什么情况才去调用的。还有多线程中,类继承于Thread重写run方法,我们实际上也没有调用run。是利用多态的方式去实现的。
实际上,在其它语言中,还有更简洁的做法: 如JS中的函数,对于一个操作:如:只需要赋值一个函数过去即可。
button:onlick=function() {
}
一个常见的面试题: 说一下Servlet 的生命周期: 答: 第一句话:Servlet在实例化之后调用一次init 第二句话:Servlet 每次收到请求,调用一次service 第三句话:Servlet 在销毁之前,调用一次 destroy
乱码问题: 当我们在body 中写入中文后,如下代码: 再在浏览器的控制台中去查看body时,发现出现了乱码的现象: 这个问题的原因是:IDEA 中与浏览器的编码方式是不一样的,在IDEA 中的编码方式是UTF-8,而在浏览器中的编码方式是挺复杂的,这个在浏览器中可以看到,因此浏览器就会默认按照该编码方式来解析响应,自然地,在控制台中看到的就是乱码了。
解决方案: 1.让服务器返回的数据就是 浏览器 的编码方式,和浏览器的编码方式一致。不推荐 2.让浏览器按照UTF-8进行解析,则只要在响应中的 header 里面加上Content-Type,在Context-Type 里面注明响应的编码是UTF-8 就可以了。
先在webapp 中创建一个html文件,在html中设置Get请求的按钮和POST 请求的按钮,引入jQuery的第三方库来创建ajax表单,并且设置表单中的类型、发送路径、打印日志的方法。 前端代码:
<body>
<button onclick="sendGet()">发送 Get 请求</button>
<button onclick="sendPost()">发送 POST 请求</button>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
function sendGet() {
$.ajax({
type:"get",
url:"method",
sucess:function(data,status) {
console.log(data);
}
})
}
function sendPost() {
$.ajax({
type:"post",
url:"method",
data:"request body",
sucess:function(data,status) {
console.log(data);
}
})
}
</script>
</body>
界面如下:
在doxx方法中设置Type,如后面这段代码:resp.setContentType("text/html; charset=utf-8"); 注意:设置Content-Type 的时候,是先设置后再去输出的,否则在控制台打印的仍然是GET ??? ,就没有起到编码的作用,因此要保证SetContent-Type 和 write 的先后顺序。 单单设置这个还不够,还要设置该项目的编码方式。打开Setting,搜索encoding,有一个File encoding,将下图的两个选项选择utf-8即可。 对构建ajax表单的时候,url需要注意的点:构造请求的时候,路径的前面不要带 / ,否则 / 就是根目录了。 (java100_servlet是上下文,method是代码中@WebServlet注释中的/method)
浏览器的编码方式可以在抓包的时候看到,在没有设置浏览器的编码方式的时候,去发送一个GET 请求,此时去抓包可以看到,里面的Content-Type 的编码方式是ISO-8859-1 .
2. HttpServletRequest
这个类就表示一个Http请求,理解这个类的前提就是要理解http 协议的格式。
回顾下Http请求的报文格式: 1.首行:方法类型,URL,版本号。URL 中进一步地分出来path,query string 2.header,一堆键值对,键值对的类型也很多 3.空行 4.body
在HttpServletRequest类中,有很多的方法,能够将Http报文格式里的内容给分离出来。由Tomcat把 字符串结构 的请求解析成一个结构化的数据 即 从字符串到结构化的数据,这样的过程称为“反序列化”。
核心方法:
方法 | 描述 |
---|
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.URL和URI 的含义是类似的,都是表示网络上的一个资源,L指的是Location(资源的位置),I指的是 identify (资源的标识符), 2.String getQueryString() 方法返回的是完整的query string;如query string:a=10&b=20 ,返回的就是整个。 而下面这三个方法,都是对于query string 来操作的。第一个是返回的是所有query string 所有的key,以枚举的类型返回。第二个是返回key对应的value值。第三个是返回某个key 的所有value值,返回的是字符串数组类型,大多数用的是参数重复的时候(可能参数为a=10&a=20)。 3.下面这两个方法都是对header进行操作的。 假设报头为: 那么第一个方法返回的是header头中的key值,如:Content-Tpye、Content-Length等。第二个方法是返回header头中对应key 的value值。
4.下面这个方法是:先获取到InputStream对象,之后就可以从里面读到body的内容了。
2.1 代码示例: 打印请求信息
要求:打印GET 方法(直接去访问服务器代码)中的请求信息。
思路:用StringBuilder 进行数据的拼接,将StringBuider的拼接后的数据以html 的形式显示到页面上。需要注意的是,获取到header中所有的key之后(枚举类型),需要进行类似迭代器的方式获取到对应的value值。
public class showRequestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf-8");
StringBuilder respBody = new StringBuilder();
respBody.append(req.getProtocol());
respBody.append("<br>");
respBody.append(req.getMethod());
respBody.append("<br>");
respBody.append(req.getRequestURI());
respBody.append("<br>");
respBody.append(req.getContextPath());
respBody.append("<br>");
respBody.append(req.getQueryString());
respBody.append("<br>");
respBody.append("<h3>headers:</h3>");
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
respBody.append(headerName + ": ");
respBody.append(req.getHeader(headerName));
respBody.append("<br>");
}
resp.getWriter().write(respBody.toString());
}
}
页面显示效果:
2.2 代码示例: 获取 GET 请求中的参数
GET 请求中的参数一般都是通过 query string 传递给服务器的. 形如:
https:
此时浏览器通过 query string 给服务器传递了两个参数, userId 和 classId, 值分别是 1111 和 100,在服务器端就可以通过 getParameter 来获取到参数的值。
因此我们可以约定,在客户端中的query string 中假设只有两个参数,userId和classId,当我们从客户端访问服务器的时候,让服务器能够返回客户端中输入的userId和classId 中的参数。
创建GetParameterServlet类 因为约定的参数只有userId和classId,因此调用getParameter 的时候去查找名字相同的即可。
@WebServlet("/getParameter")
public class GetParameterServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf-8");
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
if (userId == null || userId.equals("")) {
}
resp.getWriter().write(String.format("userId: %s; classId: %s <br>",userId, classId));
}
}
假设在客户端访问服务器的时候,没有带上参数(即127.0.0.1:8080/20220415/getParameter),那么页面的显示为: 因此,如果当前的query string 中的key不存在,那么得到的value 就是null 。
假设在客户端服务器服务器的时候带上这两个参数,如:127.0.0.1:8080/20220415/getParameter?userId=10&classId=1,则效果如下图显示: 再有,如果我们在客户端访问服务器的时候,只写了两个参数的key,没有填写值,如:127.0.0.1:8080/20220415/getParameter?userId=&classId= ,则页面显示如下图:此时getParameter得到的是一个空的字符串。 因此有必要在代码中判断,在访问服务器的时候,是否带有两个参数,带有两个参数的时候是否又赋予了value 。
2.3 代码示例: 获取 POST 请求中的参数(1)
POST 请求的参数一般通过 body 传递给服务器. body 中的数据格式有很多种. 如果是采用 form 表单的形式,仍然可以通过 getParameter 获取参数的值。
我们知道Post请求的body 中的格式有三种: 1.application/x-www-form-urlencoded 如:a=10&b=20,跟query string 和类似。 2.mutipart/form-data 这种格式比较复杂,主要是用来传输文件的,生成一个分隔符。 3.application/json 如:
{
a:10,
b:20
}
假设此时是第一种格式,那么使用getParameter 方法来获取也是可以的,跟getQueryString 方法没区别。
代码:
@WebServlet("/postParameter")
public class PostParameterServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf-8");
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
resp.getWriter().write(String.format("userId:%s classId:%s",userId,classId));
}
}
为了构造POST 请求,我们需要写一个html 页面来验证服务器的程序。我们此处使用form表单的形式即可。
testPost.html: 注:input里面的name 与 代码中的getParameter里面的参数可以匹配的。
<!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>testPost</title>
</head>
<body>
<form action="getParameter" method="post">
<input type="text" name="userId">
<input type="text" name="classId">
<input type="submit" value="提交">
</form>
</body>
</html>
testPost页面如下,此时都输入22: 点击提交后: 去抓包后,可以看到一个POST请求,并且可以看到body:
2.4 代码示例: 获取 POST 请求中的参数(2)
如果 POST 请求中的 body 是按照 JSON 的格式来传递, 那么获取参数的代码就要发生调整。 假设此时我们返回的数据是一个整体的JSON,并且是将整个JSON 格式的body作为响应进行返回,在客户端的控制台中打印出来。
代码:
@WebServlet("/postParameterJson")
public class PostParameterJsonServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String body = readBody(req);
resp.getWriter().write(body);
}
private String readBody(HttpServletRequest req) throws IOException {
InputStream inputStream = req.getInputStream();
int contentLength = req.getContentLength();
byte[] buffer = new byte[contentLength];
inputStream.read(buffer);
return new String(buffer,"utf-8");
}
}
为了构造一个post请求,自己来设置一个post请求,并且传输的post请求以ajax的形式对Json进行传输。
testPost2.html:关键代码在script里面。
<button onclick="sendJson()">发送请求</button>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
function sendJson() {
let body = {
userId:100,
classId:1
};
$.ajax({
type:'POST',
url:'postParameterJson',
contentType:"application/json;charset:utf-8",
data:JSON.stringify(body),
success:function(body,status) {
console.log(body);
}
});
}
</script>
点击html 的页面发送请求按钮,在控制台中打印的是:
2.5 代码示例: 获取 POST 请求中的参数(3)
在上面,我们是把整个 body 视为一个整体进行返回了,但更多时候,是需要解析 json 格式的body,即获取到userId 和classId 里面具体的值。但是,json 的格式解析起来是比较复杂的,因为json里面可以再嵌套json,无限套娃。
那么它的格式这么复杂,我们要怎么去解析json 格式的数据呢?我们可以使用第三方库——Jackson Databind ,这个库也适用于Spring全家桶。
注意:使用 jackson 解析json 的时候,需要先明确,要把这个 字符串 转成什么样的对象。可以参考下面的例子。
代码: 需要注意去体会ObjectMapper将Json对象转换成Java对象的过程。
class JsonData {
public int userId;
public int classId;
}
@WebServlet("/postParameterJson")
public class PostParameterJsonServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String body = readBody(req);
ObjectMapper objectMapper = new ObjectMapper();
JsonData jsonData = objectMapper.readValue(body, JsonData.class);
resp.getWriter().write(String.format("userId: %d; classId: %d <br>",
jsonData.userId, jsonData.classId));
}
private String readBody(HttpServletRequest req) throws IOException {
InputStream inputStream = req.getInputStream();
int contentLength = req.getContentLength();
byte[] buffer = new byte[contentLength];
inputStream.read(buffer);
return new String(buffer, "utf-8");
}
}
此时输出的结果就不是整个body了: 对于将Json对象转换成Java对象的底层: 1.先把Json格式的字符串转换成类似于 HashMap ,如:userId对应100,classId对应10。 2.根据类对象,获取到要转换结果的类,都有哪些属性,每个属性的名字等。此处就通过JsonData获取到,里面的属性有两个,名字分别是userId和classId(通过反射机制)。 3.拿着JsonData这里的每个属性的名字,去第一步构造的哈希表里面去查。如果查到了,就把查询到的值赋值到JsonData 对应的属性里面。
在创建JsonData 的时候,就需要先知道Json里面成员的名字,得和Jso字符串里的key 是匹配的。当然,如果不匹配的话,Jackson还提供了一些机制,来描述Json 字符串 的key 和构建出的结果类的字段之间的映射关系,但不必要这么麻烦。
小结:
3. 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 之前. 否则可能设置失效
3.1 代码示例: 设置状态码
@WebServlet("/status")
public class StatusServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf-8");
String statusString = req.getParameter("status");
if (statusString == null || statusString.equals("")) {
resp.getWriter().write("当前的请求参数 status 缺失");
return;
}
resp.setStatus(Integer.parseInt(statusString));
resp.getWriter().write("status: " + statusString);
}
}
当我们在访问服务器的时候,带上参数status,则服务器那边会根据传入的参数status来设置响应的status,那么就会返回对应status 的状态码。 如:设置status为200 如:设置status为404 如:设置status为500
3.2 代码示例: 自动刷新
header中有个属性——Refresh,它的值就是隔多长时间刷新,单位是s 。
@WebServlet("/autoRefresh")
public class AutoRefreshServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf8");
resp.setHeader("Refresh", "1");
long timeStamp = System.currentTimeMillis();
resp.getWriter().write("timestamp: " + timeStamp);
}
}
访问服务器时的页面展示:每隔一秒时间戳都是在发生变化的,但是时间戳并不是经确到1s,因为网络传输之间是会有时差的。 抓某次刷新后的包可以看到:
3.3 代码示例: 重定向
重定向就是 “呼叫转移”,状态码是302 ,可以设置Location字段重定向。
代码:
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(302);
resp.setHeader("Location","https://www.baidu.com");
}
}
此时我们 客户端去访问服务器,一敲回车后,就可以看到页面是直接跳转到百度地址上的。
五、Postman 工具
下面对Servlet API的使用,会经常需要我们自己去构造请求,但是每次自己去构造请求会非常麻烦。要不就是form表单,要么就是ajax,对于可以用相同的请求,就不用每次去敲代码来实现。可以使用Postman 工具。
步骤1:点击+ 步骤2: 最后点击sent就可以发送了。并且在下面的Response 栏中会显示出服务器返回的响应结果。
六、实现Web表白墙(了解后端即可)
1. 前后端分离实现表白墙(ajax)
约定:在客户端中提交的数据是Json格式的,HTTP请求的是POST 方法的话,POST的数据就在服务器中存储好,并且刷新页面数据不会丢失。如果是刷新页面的话,是GET方法,就会从服务器中读取之前存储的数据,保证上次在页面上的数据不丢失。
前端部分:放在body里面的关键样式:
<div class="container">
<h1>表白墙</h1>
<p>输入后点击提交, 会将信息显示在墙上</p>
<div class="row">
<span>谁</span>
<input type="text" class="edit">
</div>
<div class="row">
<span>对谁</span>
<input type="text" class="edit">
</div>
<div class="row">
<span>说什么</span>
<input type="text" class="edit">
</div>
<div class="row">
<input type="button" value="提 交" id="submit">
</div>
<!-- 每次点击 "提交" 都在下面新增一个 .row , 里面就是放置用户输入的话 -->
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
function load() {
$.ajax({
type: 'GET',
url: 'message',
success: function(data, status) {
let container = document.querySelector('.container');
let messages = data;
for (let message of messages) {
let row = document.createElement('div');
row.className = 'row';
row.innerHTML = message.from + '对' + message.to + '说: '+ message.message;
container.appendChild(row);
}
}
});
}
load();
let submitButton = document.querySelector('#submit');
submitButton.onclick = function() {
let edits = document.querySelectorAll('.edit');
let from = edits[0].value;
let to = edits[1].value;
let message = edits[2].value;
console.log(from + ", " + to + ", " + message);
if (from == '' || to == '' || message == '') {
return;
}
let row = document.createElement('div');
row.className = 'row';
row.innerHTML = from + '对' + to + '说: ' + message;
let container = document.querySelector('.container');
container.appendChild(row);
for (let i = 0; i < edits.length; i++) {
edits[i].value = '';
}
$.ajax({
type:'POST',
url:"message",
data:JSON.stringify({from:from,to:to,message:message}),
contentType:"application/json;charset=utf-8",
success:function(data,status) {
if(data.ok==1) {
console.log("提交消息成功");
}else {
container.log("提交信息失败");
}
}
});
}
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.container {
width: 400px;
margin: 0 auto;
}
h1 {
text-align: center;
padding: 20px 0;
}
p {
text-align: center;
color: #666;
padding: 10px 0;
font-size: 14px;
}
.row {
height: 50px;
display: flex;
justify-content: center;
align-items: center;
}
span {
width: 90px;
font-size: 20px;
}
input {
width: 310px;
height: 40px;
}
#submit {
width: 400px;
color: white;
background-color: orange;
border: none;
border-radius: 5px;
font-size: 18px;
}
#submit:active {
background-color: black;
}
.edit {
font-size: 18px;
padding-left: 5px;
}
</style>
后端部分: 注:在doGet方法中,因为是处理的get方法,因此就需要用一个数组将Json转变后的对象进行存储。只要客户端访问服务器,服务器就传给客户端一个Message的数组,给前端去进行处理。doPost方法只需要将客户端中输入的数据转为Json对象传给服务器,服务器再利用Jackson去进行Json对象的转化,保存到数组中。
class Message {
public String from;
public String to;
public String message;
}
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
public ObjectMapper objectMapper = new ObjectMapper();
public List<Message> messageList = new ArrayList<>();
@Override
public void init() throws ServletException {
Message message = new Message();
message.from="黑猫";
message.to="白猫";
message.message="喵";
messageList.add(message);
Message message1 = new Message();
message1.from="黑猫";
message1.to="白猫";
message1.message="喵";
messageList.add(message1);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json;charset=utf-8");
objectMapper.writeValue(resp.getWriter(),messageList);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Message message = objectMapper.readValue(req.getInputStream(),Message.class);
messageList.add(message);
resp.setContentType("application/json;charset:utf-8");
resp.getWriter().write("{\"ok\": 1}");
}
}
前端中需要注意的点:需要对doGet方法中传入的数组进行判断,因为此处是以json格式进行传输的,因此该数组被jQuery自动转成object/Array 了。要学会在客户端的源代码中打断点进行调试。 当前后端交互的过程出现问题,首先,前端和后端都有可能是问题的来源,因此就要定位是前端有问题还是后端,如果是前端,打开开发者工具,那么在控制台中就会显示具体的异常信息。找出异常的代码,借助chrome 调试器,主要就是看异常之前,代码中临时的数据是咋的。 上面的代码,即使是刷新页面,数据也会从服务器中上传到客户端。 但是,当前的服务器是把数据都保存到了 messageList 变量中,变量就是内存!一旦服务器重启,内存就会消失,随之之前保存的数据也会消失,这就会造成不可预知的后果。
如何让数据做到持久化? 有两种方式: 1.写入到文件中 2.写入到数据库中
2. 利用模板引擎实现表白墙(form表单)
前端代码: 鉴于表白墙的样式跟上面的一模一样,为了减少篇幅,我把CSS 的代码就去掉了。把form表单的前端关键代码显示出来就行。 在提交处利用form表单提交数据给服务器。
<form action="message" method="POST">
<div class="container">
<h1>表白墙</h1>
<p>输入后点击提交, 会将信息显示在墙上</p>
<div class="row">
<span>谁</span>
<input type="text" class="edit" name="from">
</div>
<div class="row">
<span>对谁</span>
<input type="text" class="edit" name="to">
</div>
<div class="row">
<span>说什么</span>
<input type="text" class="edit" name="message">
</div>
<div class="row">
<input type="submit" value="提 交" id="submit">
</div>
<!-- 每次点击 "提交" 都在下面新增一个 .row , 里面就是放置用户输入的话 -->
<!-- 添加模板这里的变量, 每个 row 都是一个表白墙上的消息 -->
<div class="row" th:each="message: ${messages}">
<span th:text="${message.from}"></span>
对
<span th:text="${message.to}"></span>
说:
<span th:text="${message.message}"></span>
</div>
</div>
</form>
后端代码: 先创建ThymeleafConfig类来建立好Servlet共享的键值对
@WebListener
public class ThymeleafConfig implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
ServletContext servletContext = servletContextEvent.getServletContext();
TemplateEngine engine = new TemplateEngine();
ServletContextTemplateResolver resolver = new ServletContextTemplateResolver(servletContext);
resolver.setPrefix("/WEB-INF/template/");
resolver.setSuffix(".html");
resolver.setCharacterEncoding("utf-8");
engine.setTemplateResolver(resolver);
servletContext.setAttribute("engine",engine);
System.out.println("engine 初始化完毕");
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
}
}
再创建Message类和MessageServlet类:
class Message {
public String from;
public String to;
public String message;
public Message(String from, String to, String message) {
this.from = from;
this.to = to;
this.message = message;
}
}
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
private List<Message> messages = new ArrayList<>();
@Override
public void init() throws ServletException {
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf-8");
ServletContext context = getServletContext();
TemplateEngine engine = (TemplateEngine) context.getAttribute("engine");
WebContext webContext = new WebContext(req, resp, context);
webContext.setVariable("messages", messages);
String html = engine.process("messageWall", webContext);
resp.getWriter().write(html);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
String from = req.getParameter("from");
String to = req.getParameter("to");
String msg = req.getParameter("message");
Message message = new Message(from, to, msg);
messages.add(message);
resp.sendRedirect("message");
}
}
实现效果跟用ajax是一样的,只是代码的实现方式不同。 大致效果:
3. 将数据写入到文件中来实现
客户端中的代码不改变,主要就是改变数据的存储方式。 doPost方法: 注:将message对象的from、to、message 属性以 \t 作为分隔符分割,保存在文件中即可。需要注意的是FileWriter fileWriter = new FileWriter(filePath, true) ,要设置true参数,是为追加写文件类型,即关闭文件后再打开,原来的数据还是存在的,若只是单纯地写文件,关闭文件再打开,前面的数据会丢失。
@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");
resp.getWriter().write("{\"ok\": 1}");
}
private void save(Message message) {
System.out.println("向文件中写入数据!");
try (FileWriter fileWriter = new FileWriter(filePath, true)) {
fileWriter.write(message.from + "\t" + message.to + "\t" + message.message + "\n");
} catch (IOException e) {
e.printStackTrace();
}
}
}
doGet方法: 注:因为此处读文件的时候要以行来读,每一行代表一个数据。又因为FileReader不能每行地读,因此可以给它封装成BufferedReader 来每行地读取数据。
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json;charset=utf-8");
List<Message> messageList = load();
objectMapper.writeValue(resp.getWriter(),messageList);
}
private List<Message> load() {
List<Message> messageList = new ArrayList<>();
System.out.println("从文件加载!");
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath))) {
while (true) {
String line = bufferedReader.readLine();
if (line == null) {
break;
}
String[] tokens = line.split("\t");
Message message = new Message();
message.from = tokens[0];
message.to = tokens[1];
message.message = tokens[2];
messageList.add(message);
}
} catch (IOException e) {
e.printStackTrace();
}
return messageList;
}
演示:原本什么都没有,因为此时文件中没有数据。 输入: 此时刷新页面,也没问题。此时关闭页面,重新打开,之前的数据还会有。此时关闭页面,重新打开,并且重启服务器,此时之前的数据还会有。
总代码:
class Message {
public String from;
public String to;
public String message;
}
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
public ObjectMapper objectMapper = new ObjectMapper();
private String filePath = "C:/java-language/java-language/20220413/messages.txt";
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json;charset=utf-8");
List<Message> messageList = load();
objectMapper.writeValue(resp.getWriter(),messageList);
}
private List<Message> load() {
List<Message> messageList = new ArrayList<>();
System.out.println("从文件加载!");
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath))) {
while (true) {
String line = bufferedReader.readLine();
if (line == null) {
break;
}
String[] tokens = line.split("\t");
Message message = new Message();
message.from = tokens[0];
message.to = tokens[1];
message.message = tokens[2];
messageList.add(message);
}
} catch (IOException e) {
e.printStackTrace();
}
return messageList;
}
@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");
resp.getWriter().write("{\"ok\": 1}");
}
private void save(Message message) {
System.out.println("向文件中写入数据!");
try (FileWriter fileWriter = new FileWriter(filePath, true)) {
fileWriter.write(message.from + "\t" + message.to + "\t" + message.message + "\n");
} catch (IOException e) {
e.printStackTrace();
}
}
}
4. 将数据写入到数据库来实现
涉及到数据库的代码编程,就会使用到JDBC,因此就要先引入 mysql Connector 的第三方库(5.1.47版本)。
首先要引入JDBC,就要实例化DataSource 类,会涉及到单例模式,并且涉及到资源的关闭,因此我们可以专门去创建一个类来进行这些操作。
DBUtil 类:
public class DBUtil {
private static final String URL = "jdbc:mysql://127.0.0.1:3306/java100?useSSL=false&characterEncoding=utf8";
private static final String USERNAME = "root";
private static final String PASSWORD = "111111";
private static volatile DataSource dataSource = null;
public static DataSource getDataSource() {
if (dataSource == null) {
synchronized (DBUtil.class) {
if (dataSource == null) {
dataSource = new MysqlDataSource();
((MysqlDataSource)dataSource).setURL(URL);
((MysqlDataSource)dataSource).setUser(USERNAME);
((MysqlDataSource)dataSource).setPassword(PASSWORD);
}
}
}
return dataSource;
}
public static Connection getConnection() throws SQLException {
return getDataSource().getConnection();
}
public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {
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();
}
}
}
}
doPost方法: 改变的是save,其余都不变。
@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");
resp.getWriter().write("{\"ok\": 1}");
}
private void save(Message message) {
System.out.println("写入数据到数据库");
Connection connection = null;
PreparedStatement statement = null;
try {
connection=DBUtil.getConnection();
String sql = "insert into message values(?,?,?)";
statement=connection.prepareStatement(sql);
statement.setString(1,message.from);
statement.setString(2,message.to);
statement.setString(3,message.message);
int ret = statement.executeUpdate();
if(ret==1) {
System.out.println("插入成功");
}else {
System.out.println("插入失败");
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
DBUtil.close(connection,statement,null);
}
}
}
此时我们验证是否能够插入成功,我们在客户端中输入,然后再去查看数据库。 说明插入是没有问题的。
doGet方法:能够从数据库中加载数据到客户端中。
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json;charset=utf-8");
List<Message> messageList = load();
objectMapper.writeValue(resp.getWriter(),messageList);
}
private List<Message> load() {
List<Message> messageList = new ArrayList<>();
System.out.println("从数据库读数据");
Connection connection =null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
connection = DBUtil.getConnection();
String sql = "select * from message";
statement = connection.prepareStatement(sql);
resultSet = statement.executeQuery();
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 {
DBUtil.close(connection,statement,resultSet);
}
return messageList;
}
|