具体研究一下 Servlet 里面的关键APl~~
主要介绍这三个类:
- HttpServlet
- HttpServletRequest
- HttpServletResponse
一、HttpServlet
1、多态
咱们自己写的代码,就是通过继承这个类,重写其中的方法,来被 Tomcat 执行到的
也就是多态! 举例:
集合类:
List<String> list = new ArrayList<>();
list.add(.......
多线程:
class MyThread extends Thread {
public void run() {
}
}
Servlet 这里也是一个很好的例子~~
// 调用 Servlet 对象的 service 方法
// 这里就会最终调用到我们自己写的 HttpServlet 的子类里的方法了
try {
ins.service(req, resp); // 4)
} 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);
}
......
}
}
2、核心方法
方法名称 | 调用时机 |
---|---|
init | 在 HttpServlet 实例化之后被调用一次 |
destory | 在 HttpServlet 实例不再使用的时候调用一次 |
service | 收到 HTTP 请求的时候调用 |
doGet | 收到 GET 请求的时候调用(由 service 方法调用) |
doPost | 收到 POST 请求的时候调用(由 service 方法调用 |
doPut/doDelete/doOptions/… | 收到其他请求的时候调用(由 service 方法调用) |
我们实际开发的时候主要重写 doXXX 方法,很少会重写 init / destory / service
这些方法的调用时机,就称为 Servlet 生命周期 (也就是描述了一个 Servlet 实例从生到死的过程)
注意: HttpServlet 的实例只是在程序启动时创建一次,而不是每次收到 HTTP 请求都重新创建实例
3、代码示例:处理 POST 请求
创建 MethodServlet 类
当我们给两个类都指定了 /hello
路径的时候,Tomcat 自己就退出了~~
结果:Process finished with exit code 0
进程的退出码~~
C 语言 的 main 函数的 return值
一般退出码用 0 表示是正常退出,其他非 0 值表示异常退出 (直接杀死 tomcat,此时退出码是个 -1 )
人家明确的告诉你,这俩类,不能映射为同一个 url-pattern
@WebServlet("/method") // 在同一个 webapp 里 多个 Servlet 关联的路径 不能相同!
public class MethodServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// super.doPost(req, resp);
resp.getWriter().write("POST 响应");
}
}
如果是 GET 请求,直接浏览器中,通过 URL 就能构造
构造 POST 请求:
- form
- ajax
<!-- jquery cdn -->
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script>
$.ajax({
type:'post',
url: 'method',
success: function(body) {
console.log(body);
}
});
</script>
启动,观察响应结果:
我们预期的响应是:“POST 响应”,但是出现了乱码!!!
在当前场景中,字符串主要有两个环节:
-
生成 (IDEA 里面通过硬编码的方式把这个字符串写进去)
-
展示 (浏览器把这个字符串显示到控制台里)
如果这俩操作,不能按照统一的编码方式来进行就容易出现乱码~~
上面我们写的 post 方法:
这里的编码,就取决于,IDEA 当前是以怎样的编码来组织咱们的项目,一般默认都是 utf8~~ settings-搜索 encoding 可以看到
浏览器展示字符串:
默认情况下,一般是跟随着系统的默认编码~~ (windows 简体中文版,一般默认编码是 gbk ),显式地告诉浏览器,你不要按照 gbk 的方式来读了,要按照 utf8 来读!!
解决方法 :在 post 方法中,加上一句:
resp.setContentType("text/html; charset=utf8");
二、Postman
是否有更简单的,不用写代码,就能构造 POST 请求的方式呢?
有一系列的第三方工具,可以构造任意的 HTTP 请求,其中一个比较知名的,就是 postman
postman 最初是 chrome 的一个插件,后来不小心就火了,现在单独搞了一个 postman 程序,同时还有了个对象! (postwoman)
三、HttpServletRequest
- 当 Tomcat 通过 Socket API 读取 HTTP 请求(字符串),并且按照 HTTP 协议的格式把字符串解析成 HttpServletRequest 对象
- HttpServletRequest 对应到一个 HTTP 请求,HTTP请求中有啥,这里就有啥
HttpServletResponse 对应到一个HTTP响应,HTTP响应中有啥,这里就有啥
1、核心方法
通过这些方法可以获取到一个请求中的各个方面的信息
注意:请求对象是服务器收到的内容,不应该修改,因此上面的方法也都只是 “读” 方法,而不是 "写"方法
方法 | 描述 |
---|---|
String getProtocol() | 返回请求协议的名称和版本。 |
String getMethod() | 返回请求的 HTTP 方法的名称,例如,GET、POST 或 PUT。 |
String getRequestURI() | 从协议名称直到 HTTP 请求的第一行的查询字符串中,返回该 请求的 URL 的一部分。 |
String getContextPath() | 返回指示请求上下文的请求 URI 部分。 |
String getQueryString() | 返回包含在路径后的请求 URL 中的查询字符串。 |
Enumeration getParameterNames() | 返回一个 String 对象的枚举,包含在该请求中包含的参数的名 称。 |
String getParameterNames() | 以字符串形式返回请求参数的值,或者如果参数不存在则返回 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 对象 |
getRequestURI()
方法名字叫做 URI 不是 URL,这其实是两个概念,但是这俩概念非常相似,甚至可以混用
getQueryString
得到是完整的查询字符串,形如 ?a=10&b=20。下面两个方法,相当于是把查询字符串给解析成键值对了,
getParameterNames
是得到所有的 key,以 Enum 的方式来表示,getParameter
则是根据 key 来拿到 value
getHeaderNames
,getHeader
获取请求报头,请求报头也是键值对结构,此处 servlet 也是把请求头进行了解析,得到了键值对结构
getInputStream
这个就得到了一个输入流对象,从这个对象中读取数据,其实就读到了请求的 body
1.1、代码示例: 打印请求信息
创建 ShowRequestServlet 类
@WebServlet("/showRequest")
public class ShowRequestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// super.doGet(req, resp);
// 调用刚才涉及的几个关键 API,把结果组织到一个 html 中,并作为响应的 body
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.getContextPath());
stringBuilder.append("<br>");
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>");
}
resp.setContentType("text/html; charset=utf8 // 防止乱码
resp.getWriter().write(stringBuilder.toString());
}
}
访问 http://127.0.0.1:8080/hello102/showRequest
结果:
改成 http://127.0.0.1:8080/hello102/showRequest?a=10,queryString 就不再是 null,而是 a=10
- 每次咱们修改了代码之后,都需要重新打包部署,才能生效
是否存在,只要代码一修改,就自动重新打包部署的机制?? 不就又省事了嘛??- 有的!!! 程序猿这种生物,对于 “重复性” 的操作,是深恶痛绝的!! —— 热加载
2、获取 GET 请求中的参数
上述 API 能够让我们拿到 HTTP 请求的各个方面内容~~ 但是却没那么常用
更常用的,其实是 getParameter
这个方法 (取到query string中的详细内容)
GET 请求中的参数一般都是通过 query string 传递给服务器的,形如
https://v.bitedu.vip/personInf/student?userId=1111&classId=100
此时浏览器通过 query string 给服务器传递了两个参数, userId 和 classId, 值分别是 1111 和 100
在服务器端就可以通过 getParameter 来获取到参数的值
创建 GetParameterServlet 类
@WebServlet("/getParameter")
public class GetParameterServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// super.doGet(req, resp);
// 预期浏览器传来的请求:/getParameter?userId=123&classId=789
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
resp.getWriter().write("userId=" + userId + ", classId=" + classId);
}
}
当没有 query string 的时候,getParameter 获取的值为 null
-
访问: http://127.0.0.1:8080/hello102/getParameter
结果:userId=null, classId=null
-
访问:http://127.0.0.1:8080/hello102/getParameter?userId=10&&classId=20
结果:userId=10, classId=20
3、获取 POST 请求中的参数
POST 请求的参数一般通过 body 传递给服务器,body 中的数据格式有很多种
POST 请求 body 格式:
- x-www-form-urlencoded
- form-data
- json
3.1、POST 请求中 body 按照 form 表单的形式
如果是采用 form 表单的形式,服务器如何获取参数呢? 仍然可以和 GET 一样通过 getParameter 获取参数的值,
创建 PostGetParameterServlet 类
@WebServlet("/postGetParameter")
public class PostGetParameterServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// super.doPost(req, resp);
// 假设前端传过来的参数是:userId=10&classId=20
// 服务器也是通过 req.getParameter 来获取内容的
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
resp.getWriter().write("userId=" + userId + ", classId=" + classId);
}
}
构造 POST 请求:
注意: test.html 是在 webapp 目录下,和 WEB-INF 是在同级目录中,而不是在 WEB-INF 里面
如果你把 test.html 放错位置了,那么大概率就要 404~
<form action="postGetParameter" method="post">
<input type="text" name="userId">
<input type="text" name="classId">
<input type="submit" value="提交">
</form>
<!-- jquery cdn -->
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script>
</script>
访问:http://127.0.0.1:8080/hello102/test.html
结果: userId=111, classId=222
此时通过抓包可以看到,form 表单构造的 body 数据的格式为:
POST http://127.0.0.1:8080/hello102/postGetParameter HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Content-Length: 22
Cache-Control: max-age=0
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: http://127.0.0.1:8080
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1:8080/hello102/test.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
userId=111&classId=222
3.2、POST 请求中 body 按照 JSON 的格式
{
userld: 1234,
classld: 5678
}
对于这种 body 为 json 的格式来说,如果手动来解析,其实并不容易 ( JSON 里面的字段是能嵌套的~)
像这种情况,手动处理比较麻烦,可以使用第三方的库,来直接处理 json 格式数据~~
Java 生态中,用来处理 JSON 的第三库,种类也是很多~~
我们使用的库,叫做 Jackson (Spring官方推荐的库)
通过 maven 把 jackson 这个库,给下载到本地,并引入到项目中(还是 pom.xml 标签 dependencies 中)~~
1). 在浏览器前端代码中,通过 js 构造出 body 为 json 格式的请求
<!-- 要想构造一个 json 格式的请求, 就不再使用 form 而是使用 ajax 了 -->
<input type="text" id="userId">
<input type="text" id="classId">
<input type="button" value="提交" id="submit">
<!-- jquery cdn -->
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script>
let userIdInput = document.querySelector("#userId");
let classIdInput = document.querySelector("#classId");
let buttom = document.querySelector("#submit");
buttom.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>
对于像 post 这样的请求,ajax 就允许使用 data
属性来构造请求的 body 部分
此处要构造的内容,其实是一个 js 对象
需要把这个 js 的对象,再通过 JSON.stringify
,从 js 对象,转成一个字符串
这个字符串的格式就正是 json 的 格式
—— {“userld”:“123”,“classld”:“456”}
2). 在 java 后端代码中,通过 jackson 来进行处理
需要使用 jackson,把请求 body 中的数据读取出来,并且解析成 Java 中的对象
User user = objectMapper.readValue(req.getInputStream(), User.class);
readValue()
:把 JSON 格式的字符串,转成 Java 的对象
- 第一个参数: 表示对哪个字符串进行转换,这个参数可以填写成一个 String / 一个 InputStream 对象 / 一个 File 对象
- 第二个参数: 表示要把这个 JSON 格式的字符串,转成哪个 Java 对象
上面我们说 Json 的格式形如:—— {“userld”:“123”,“classld”:“456”}
通过 getInputStream()
得到到就是这个字符串
那么 readValue 是如何转换的?
-
先把
getInputStream
对应的流对象里面的数据都读取出来 -
针对这个 json 字符串进行解析,从 字符串 => 键值对
-
key: userld; value: 123
-
key: classld; value: 456
-
此处就要求,User 类 的属性名,得和键值对中的 key 的名字匹配~~
(要求名字匹配,这个只是 jackson 默认行为),如果你就非得想搞个不匹配的名字,也不是不可以
-
-
遍历这个键值对,依次获取到每一个 key
根据这个 key 的名字,和 User 类里面的属性名字,对比一下~~ 看有没有匹配的名字!!- 如果发现匹配的属性,则把当前 key 对应的 value 赋值到该 User 类的属性中~~ (赋值的过程中同时会进行类型转换)
- 如果没有匹配的属性,就跳过,取下一个 key ~~
-
当把所有的键值对都遍历过之后,此时 User 对象就被构造的差不多了~~
class User {
// 当前都设为 public,如果是 private,但是同时提供了 getter setter,效果等同
public int userId;
public int classId;
}
@WebServlet("/postJson")
public class PostJsonServlet extends HttpServlet {
// 1、创建一个 Jackson 的核心对象
ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
// 2、读取 body 中的请求,然后使用 ObjectMapper 来解析成需要的对象
User user = objectMapper.readValue(req.getInputStream(), User.class);
// 源代码是.java => 二进制字节码.class => 被加载到内存中就成为了 class 对象
// getInputStream():读取请求的 body 内容. 返回一个 InputStream 对象
resp.getWriter().write("userId: " + user.userId + ", classId: " + user.classId);
}
}
点击提交之后,可以看到,在浏览器控制台中,就打印出来了服务器的响应数据~~
当前使用的是 ajax 的方式来提交数据,这个操作默认不会产生页面跳转,就和咱们使用 form 风格差别很大~~
抓包可以观察 body 数据的格式:
POST http://127.0.0.1:8080/hello102/postJson HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Content-Length: 32
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"
Accept: */*
Content-Type: application/json
X-Requested-With: XMLHttpRequest
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://127.0.0.1:8080
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:8080/hello102/test.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
{"userId":"111","classId":"222"}
Postman 构造 JSON 请求
四、HttpServletResponse
- Servlet 中的 doXXX 方法的目的就是根据请求计算得到响应,然后把响应的数据设置到 HttpServletResponse 对象中
- 然后 Tomcat 就会把这个 HttpServletResponse 对象按照 HTTP 协议的格式,转成一个字符串,并通过 Socket 写回给浏览器
1、核心方法
方法 | 描述 |
---|---|
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 中写入二进制格式数据 |
addHeader
例如 Set-Cookie,一个响应中,是可以有多个 Set-Cookie 这样的 key 的
sendRedirect
构造一个 302 重定向响应
getWriter
getOutputStream
往 body 中写数据,更多的是使用文本的方式
- 注意: 响应对象是服务器要返回给浏览器的内容,这里的重要信息都是程序猿设置的,因此上面的方法都是 “写” 方法.
- 注意: 对于状态码 / 响应头的设置要放到 getWriter / getOutputStream 之前,否则可能设置失效
2、代码示例:设置状态码
创建 StatusServlet 类
——200:
@WebServlet("/status")
public class StatusServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// super.doGet(req, resp);
resp.setStatus(200);
resp.getWriter().write("hello");
}
}
访问:http://127.0.0.1:8080/hello102/status
显示:hello
抓包结果:
HTTP/1.1 200
Content-Length: 5
Date: Sat, 21 May 2022 13:30:30 GMT
Keep-Alive: timeout=20
Connection: keep-alive
hello
——404:
resp.setStatus(200);
重新部署,访问
显示:hello
开发者工具显示:GET http://127.0.0.1:8080/hello102/status 404
抓包结果:
HTTP/1.1 404
Content-Length: 5
Date: Sat, 21 May 2022 13:33:09 GMT
Keep-Alive: timeout=20
Connection: keep-alive
hello
注意: 服务器返回的状态码,只是在告诉浏览器,当前的响应是个啥状态,并不影响浏览器照常去显示 body 中的内容~~
3、代码示例:自动刷新
实现一个程序,让浏览器每秒钟自动刷新一次,并显示当前的时间戳
例如 “文字直播”:
这是在十年前左右的时候 4G 和 5G 还没有出来,流量还是很贵的时候,使用手机看视频 / 看直播,都是不太现实的,当时一种流行的方式就是文字直播
各种比赛,当时就是有一些专门文字直播平台,有工作人员,比赛现场的情况,用文字的方式来描述出来
经常要涉及到页面刷新~~
创建 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://127.0.0.1:8080/hello102/autoRefresh
显示结果:timeStamp: 1653140698925
这段数字是每隔一秒左右在不断变化的
抓包结果:
HTTP/1.1 200
Refresh: 1
Content-Length: 24
Date: Sat, 21 May 2022 13:48:02 GMT
Keep-Alive: timeout=20
Connection: keep-alive
timeStamp: 1653140882691、
4、代码示例:重定向
实现一个程序,返回一个重定向 HTTP 响应,自动跳转到另外一个页面
创建 RedirectServlet 类
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 返回一个 302 重定向,让浏览器自动跳转到 sogou 主页
resp.setStatus(302);
resp.setHeader("Location", "https://www.sogou.com");
}
}
抓包结果:
HTTP/1.1 302
Location: http://www.sogou.com
Content-Length: 0
Date: Mon, 21 Jun 2021 08:17:26 GMT
Keep-Alive: timeout=20
Connection: keep-alive
Servlet 提供了一个更简便的实现重定向的写法:
resp.sendRedirect("https://www.sogou.com");
- 光理解了 Servlet API 还不足以支撑我们写出一个功能完整的网站
- 还需要理解一个网站的开发过程大概是怎样的,理解这里的一些基本的编程思维和设计思路~~ (通过更多的案例来进行强化的)