??前面的话??
本篇文章将介绍Servlet的基本原理已经Servlet API中常用的一些方法,我们将利用这些方法实现一些Servlet的小案例。
📒博客主页:未见花闻的博客主页 🎉欢迎关注🔎点赞👍收藏??留言📝 📌本文由未见花闻原创,CSDN首发! 📆首发时间:🌴2022年7月2日🌴 ??坚持和努力一定能换来诗与远方! 💭参考书籍:📚《暂无》 💬参考在线编程网站:🌐牛客网🌐力扣 博主的码云gitee,平常博主写的程序代码都在里面。 博主的github,平常博主写的程序代码都在里面。 🍭作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!
🍎1.Tomcat如何调用Servlet
🍏1.1Servlet原理
Servlet是属于上层建筑,它处在应用层,它的下层有传输层,网络层,数据链路层,硬件,属于“经济基础”,毕竟下层经济基础决定上层建筑。前面说过,Servlet是一组操作HTTP的API,Tomcat可作为HTTP服务器来处理请求,这个处理请求的关键就是调用Servlet来操作HTTP给客户端做出响应。
我们所写的Servlet代码没有main方法,那他是如何运行的呢?其实是Tomcat在调用Servlet,Tomcat其实就是一个应用程序,是运行在用户态上的一个普通的Java进程。 当浏览器发送请求给服务器的时候,Tomcat作为HTTP Server会调用Serlvet API,然后执行我们所写的Servlet程序来处理请求。 处理请求的过程中牵涉的不仅仅只有HTTP,还有其他层的协议,但是我们并没有感知到其他层协议的细节,只关注了应用层HTTP协议的细节,这就是协议分层好处,程序员在实现处理请求时,不必去关心应用层下面的细节。
🍏1.2Tomcat的执行逻辑
为了方便描述Tomcat的执行逻辑,我们使用伪代码的形式来分析:
初始化与收尾工作,又细分为以下部分: 1)从指定的目录中找到Servlet类,并加载。 2根据加载的结果,给这些类创建实例。 3)创建好实例后,调用Servlet对象中的init 方法。 4)创建TCP socket对象,监听8080端口,等待客户端来连接。 5)如果请求处理完毕,也就是处理请求的循环退出了,那Tomcat也结束了,调用destroy 方法结束进程,但是这个环节不一定可靠,正常退出的情况下,需要在管理端口(8005)去调用destroy ,将Tomcat关闭,但很多时候都是直接杀死进程来达到关闭的目的,此时根本来不及调用dsetroy 方法。
class Tomcat {
private List<Servlet> instanceList = new ArrayList<>();
public void start() {
Class<Servlet>[] allServletClasses = ...;
for (Class<Servlet> cls : allServletClasses) {
Servlet ins = cls.newInstance();
instanceList.add(ins);
}
for (Servlet ins : instanceList) {
ins.init();
}
ServerSocket serverSocket = new ServerSocket(8080);
ExecuteService pool = Executors.newFixedThreadPool(100);
while (true) {
Socket socket = ServerSocket.accept();
pool.execute(new Runnable() {
doHttpRequest(socket);
});
}
for (Servlet ins : instanceList) {
ins.destroy();
}
}
public static void main(String[] args) {
new Tomcat().start();
}
}
Tomcat处理请求工作: 1)读取socket中的数据,并按照HTTP协议的格式来进行解析,获取请求。 2)判断请求是需要静态内容还是动态内容,如果是静态内容,可以在根路径上找到目的文件,返回请求 3)如果是动态文件,则需要通过URL上的一级路径与二级路径来确定通过哪一个Servlet类来进行处理,没有的话就会返回404 4)找到对应Servlet对象,调用对象里面的service 方法,根据请求的方法来调用对应的do... 方法
class Tomcat {
void doHttpRequest(Socket socket) {
HttpServletRequest req = HttpServletRequest.parse(socket);
HttpServletRequest resp = HttpServletRequest.build(socket);
if (file.exists()) {
return;
}
Servlet ins = findInstance(req.getURL());
try {
ins.service(req, resp);
} catch (Exception e) {
}
}
}
service方法执行逻辑:
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);
}
......
}
}
在整个流程中,有三个关键的方法:
- init方法,在初始化阶段执行,用来初始化每一个Servlet对象,对象创建好之后,就会执行,用户可重写该方法,来执行一些初始化程序的逻辑,没有重写,
init 方法一般是空的,也就是什么也不执行。 - destroy方法,退出请求处理的主循环之后,tomcat结束之前就会执行到该方法,用来释放资源。
- service方法,在处理请求的阶段调用,针对每个动态资源的请求都需要调用。
🍎2.Servlet中关键的几个API
🍏2.1常用方法列举
HttpServlet关键方法:
方法名称 | 调用时机 |
---|
init | 在 HttpServlet 实例化之后被调用一次 | destory | 在 HttpServlet 实例不再使用的时候调用一次 | service | 收到 HTTP 请求的时候调用 | doGet | 收到 GET 请求的时候调用(由 service 方法调用) | doPost | 收到 POST 请求的时候调用(由 service 方法调用) | doPut/doDelete/doOptions/… | 收到其他请求的时候调用(由 service 方法调用) |
这些方法的调用时机,就构成了“Servlet”的生命周期。
HttpServletRequest关键方法:
方法 | 描述 |
---|
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 对象. |
HttpServletResponse关键方法:
方法 | 描述 |
---|
void setStatus(int sc) | 为该响应设置状态码。 | void setHeader(String name, String value) | 设置一个带有给定的名称和值的 header. 如果 name 已经存在,则覆盖旧的值,可以实现页面的刷新 | void addHeader(String name, String value) | 添加一个带有给定的名称和值的 header. 如果name 已经存在,不覆盖旧的值, 并列添加新的键值对 | void setContentType(String type) | 设置被发送到客户端的响应的内容类型。 | void setCharacterEncoding(String charset) | 设置被发送到客户端的响应的字符编码(MIME 字符集)例如,UTF-8。 | void sendRedirect(String location) | 使用指定的重定向位置 URL 发送临时重定向响应到客户端。 | PrintWriter getWriter() | 用于往 body 中写入文本格式数据. | OutputStream getOutputStream() | 用于往 body 中写入二进制格式数据. |
🍏2.2Post请求的构造
在同一webapp里面,关联路径不能够相同,不然Tomcat跑不起来,对于GET请求,可以使用URL的查询字符串进行构造,但是POST请求不行,需要使用form或者ajax。
构造Post请求(使用ajax构造): 在webapp目录下创建一个HTML文件,用来构造POST请求,首先我们先的引入jquery 依赖(博主使用的是本地导入,你可以如果网络地址:https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js 导入依赖),然后调用ajax构造请求。
<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>
注意上面的URL属性不能加/ ,加上表示的就是绝对路径了,当然你也可以使用./ 来表示相对路径,但是在Servlet注解关联路径必须得加上/ 。
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;
@WebServlet("/method")
public class MethodServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("POST请求");
}
}
我们访问http://127.0.0.1:8080/hello_servlet/test.html 来看控制台输出的返回结果。 我们发现与我们的预期不一致,我们处理请求的时候返回了POST请求 ,而这里显示了POST?? ,原因是发生了乱码,idea默认编码格式为utf-8 ,Windows默认的编码格式是gbk ,那浏览器解析body的时候也是以gbk 格式去进行解析,要想统一格式,就得先告诉浏览器响应数据的编码格式是什么,我们需要在Servlet程序里面设置字符格式,设置方法为调用HttpServletResponse对象的setContentType 方法,传入参数text/html; charset=utf8 。
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;
@WebServlet("/method")
public class MethodServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("POST请求");
}
}
重新打包部署,刷新页面:
🍏2.3获取请求信息
对于请求的信息,我们运用HttpServletRequest类的方法来进行请求信息的获取: 比如我们访问的url为http://127.0.0.1:8080/hello_servlet/showreq?key=10&a=100&b=200 ,很明显这是使用查询字符串构造的一个GET请求,通过HttpServletRequest类一系列对应的方法,我们可以获取到这个请求的方法类型,协议版本,URL,查询字符串,头部的一些信息等。其中查询字符串与头部信息的获取先要使用getParameterNames方法或者getHeaderNames方法获取所有的查询字符串或头部信息的所有key 值,这个一个枚举对象,然后在根据getParameter或者getHeader方法通过key 值遍历枚举对象获取value 。
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("/showreq")
public class ShowRequestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
StringBuilder stringBuilder = new StringBuilder();
resp.setContentType("text/html; charset=utf-8");
stringBuilder.append("协议版本:");
stringBuilder.append(req.getProtocol());
stringBuilder.append("<br>");
stringBuilder.append("方法:");
stringBuilder.append(req.getMethod());
stringBuilder.append("<br>");
stringBuilder.append("URL路径:");
stringBuilder.append(req.getRequestURI());
stringBuilder.append("<br>");
stringBuilder.append("URL(不包括查询字符串后面的部分):");
stringBuilder.append(req.getRequestURL());
stringBuilder.append("<br>");
stringBuilder.append("一级路径:");
stringBuilder.append(req.getContextPath());
stringBuilder.append("<br>");
stringBuilder.append("查询字符串:");
stringBuilder.append(req.getQueryString());
stringBuilder.append("<br>");
stringBuilder.append("正文编码格式:");
stringBuilder.append(req.getCharacterEncoding());
stringBuilder.append("<br>");
stringBuilder.append("mine:");
stringBuilder.append(req.getContentType());
stringBuilder.append("<br>");
stringBuilder.append("正文长度:");
stringBuilder.append(req.getContentLength());
stringBuilder.append("<br>");
stringBuilder.append("<h3>获得每一个查询字符串的键值:</h3>");
Enumeration query = req.getParameterNames();
while(query.hasMoreElements()) {
String key = (String)query.nextElement();
stringBuilder.append(key);
stringBuilder.append(":");
stringBuilder.append(req.getParameter(key));
stringBuilder.append("<br>");
}
stringBuilder.append("<h3>获得头部的键值:</h3>");
Enumeration header = req.getHeaderNames();
while(header.hasMoreElements()) {
String key = (String)header.nextElement();
stringBuilder.append(key);
stringBuilder.append(":");
stringBuilder.append(req.getHeader(key));
stringBuilder.append("<br>");
}
resp.getWriter().write(stringBuilder.toString());
}
}
结果:
🍏2.4Post请求信息的获取
我们知道post请求的请求信息在http格式中的body 部分当中,而body 中的请求内容的格式是有很多种的,比如最常见的有:
- x-www-form-urlencode格式,通过form表单或者postman构造。
- json格式
- form-data格式
x-www-form-urlencode格式:
k
e
y
=
v
a
l
u
e
&
k
e
y
=
v
a
l
u
e
&
.
.
.
key=value\&key=value\&...
key=value&key=value&...
form表单创建x-www-form-urlencode格式 请求:
<!DOCTYPE html>
<html lang="ch">
<head>
<meta charset="UTF-8">
<title>post</title>
</head>
<body>
<form action="./postParameter" method="post" accept-charset="utf-8">
<span>userId</span>
<input type="text" name="userId">
<span>classId</span>
<input type="text" name="classId">
<input type="submit" value="提交">
</form>
</body>
</html>
Servlet程序接收和处理请求: 对于x-www-form-urlencode格式 请求可以直接使用HttpServletRequest 中的getParameter 方法依据key 来获取value ,然后再将获取到的数据返回,form表单构造的请求会自动跳转页面。
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;
@WebServlet("/postParameter")
public class GetPostParameterServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html; charset=utf8");
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
resp.getWriter().write("userId=" + userId + ", " + "classId=" + classId);
}
}
运行结果:
json格式:
{
\{
{
k
e
y
:
v
a
l
u
e
,
key:value,
key:value,
k
e
y
:
v
a
l
u
e
,
key:value,
key:value,
k
e
y
:
v
a
l
u
e
,
key:value,
key:value,
.
.
.
...
...
}
\}
}
对于json格式,手动解析不容易,因为json里面的字段是可以嵌套的,但我们可以借助第三方库来解析处理json,比如Jackson,Jackson依赖导入过程如下:
处理json请求步骤: 第一步,在前端js代码中构造出格式为json 格式的请求。 其中ajax 构造post 请求,使用contentType 来说明请求的类型,data 属性来设置body 的内容。
<!DOCTYPE html>
<html lang="cn">
<head>
<meta charset="UTF-8">
<title>json</title>
</head>
<body>
<input type="text" id="userId">
<input type="text" id="classId">
<input type="button" id="submit" value="提交">
<script src="./jquery3.6.0.js"></script>
<script>
let userIdInput = document.querySelector("#userId");
let classIdInput = document.querySelector("#classId");
let button = document.querySelector("#submit");
button.onclick = function() {
$.ajax({
type : "post",
url: "getJsonPost",
contentType: "appliaction/json",
data:JSON.stringify({
userId: userIdInput.value,
classId:classIdInput.value
}),
success: function(body){
console.log(body);
}
})
}
</script>
</body>
</html>
第二步,在java后端代码中使用Jackson处理。
- 1)创建Jackson核心对象ObjectMapper对象。
- 2)读取请求中的body信息,该过程通过ObjectMapper对象的
readValue 方法实现。 - 3)创建用来接受
json 数据的类。 - 4)
readValue 方法的参数有两个,第一个参数用来表示请求的来源,可以是路径字符串,与可以是InputSream 对象,也可以是File 对象,第二个参数表示接收json数据的类对象。 - 5)处理并响应请求。
import com.fasterxml.jackson.databind.ObjectMapper;
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;
class User {
public String userId;
public String classId;
}
@WebServlet("/getJsonPost")
public class GetJsonPostServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf8");
User user = objectMapper.readValue(req.getInputStream(), User.class);
System.out.println(user.userId);
System.out.println(user.classId);
resp.getWriter().write("userId=" + user.userId + " ,classId=" + user.classId);
}
}
运行结果:
readValue 方法基本原理:
- 读取json格式的数据,并解析成键值对。
- 便利这些键值对,获得
key ,并与所需传入的对象中的属性(反射)相比,如果key 与属性的名字相同,则把key 对应的value 赋值给这个属性,否则就跳过,所有的键值对便利完后,这个对象差不多就被构造的差不多了。
🍏2.5响应的构造
案例1: 设置响应状态码
设置方法很简单,只需要调用httpServletResponse 对象中的setStatus 方法就可以了
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;
@WebServlet("/status")
public class StatusServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf8");
int status = 200;
resp.setStatus(status);
resp.getWriter().write("hello,这是" + status + "状态码的响应内容!");
}
}
启动程序,网页显示如下:
我把状态码修改为404 ,网页显示如下:
那为什么不是之前我们所遇到的那种404页面呢?这是因为我们设置的页面响应内容就是hello,这是404状态码的响应内容! ,可以理解为自定义的404 状态响应页面,就像其他的网站,如果访问不到页面,显示的提醒页面也是不一样的,比如b站的页面是这个样子的: 案例2:自动页面刷新
自动页面刷新只要在响应中设置一个header: Refresh就能实现页面的定时刷新了,对于响应header 的设置,我们可以通过HttpServletResponse 对象中的setHeader 方法来设置Refresh属性和刷新频率。
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;
@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");
resp.getWriter().write("时间戳:" + System.currentTimeMillis());
}
}
效果:
案例3:重定向案例 第一步,设置状态码为302 。 第二步,设置header:Location,调用setHeader 方法时,第一个参数填Location ,表示设置header 字段为Location ,第二个参数为重定向的目的地址,你要重定向到哪一个网址就传入哪一个地址的字符串。
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;
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset = utf8");
resp.setStatus(302);
resp.setHeader("Location", "https://leetcode.cn/");
}
}
效果:
当然,servlet提供了更为简便的重定向方法,就是使用HttpServletResponse 类中的sendRedirect 方法。
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset = utf8");
resp.sendRedirect("https://leetcode.cn/");
}
}
效果与上面第一种方法是一样的。
下期预告:表白墙小案例
觉得文章写得不错的老铁们,点赞评论关注走一波!谢谢啦!
|