Servlet基本原理与常见API方法的应用
⭐️前面的话⭐️
本篇文章将介绍Servlet的基本原理已经Servlet API中常用的一些方法,我们将利用这些方法实现一些Servlet的小案例。
📒博客主页:未见花闻的博客主页
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文由未见花闻原创!
📆华为云首发时间:🌴2022年7月31日🌴
✉️坚持和努力一定能换来诗与远方!
💭参考书籍:📚《无》
💬参考在线编程网站:🌐牛客网🌐力扣
博主的码云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 {
// 用来存储所有的 Servlet 对象
private List<Servlet> instanceList = new ArrayList<>();
public void start() {
// 根据约定,读取 WEB-INF/web.xml 配置文件;
// 并解析被 @WebServlet 注解修饰的类
// 假定这个数组里就包含了我们解析到的所有被 @WebServlet 注解修饰的类.
Class<Servlet>[] allServletClasses = ...;
// 这里要做的的是实例化出所有的 Servlet 对象出来;
for (Class<Servlet> cls : allServletClasses) {
// 这里是利用 java 中的反射特性做的
// 实际上还得涉及一个类的加载问题,因为我们的类字节码文件,是按照约定的
// 方式(全部在 WEB-INF/classes 文件夹下)存放的,所以 tomcat 内部是
// 实现了一个自定义的类加载器(ClassLoader)用来负责这部分工作。
Servlet ins = cls.newInstance();
instanceList.add(ins);
}
// 调用每个 Servlet 对象的 init() 方法,这个方法在对象的生命中只会被调用这一次;
for (Servlet ins : instanceList) {
ins.init();
}
// 利用我们之前学过的知识,启动一个 HTTP 服务器
// 并用线程池的方式分别处理每一个 Request
ServerSocket serverSocket = new ServerSocket(8080);
// 实际上 tomcat 不是用的固定线程池,这里只是为了说明情况
ExecuteService pool = Executors.newFixedThreadPool(100);
while (true) {
Socket socket = ServerSocket.accept();
// 每个请求都是用一个线程独立支持,这里体现了我们 Servlet 是运行在多线程环境下的
pool.execute(new Runnable() {
doHttpRequest(socket);//处理请求
});
}
// 调用每个 Servlet 对象的 destroy() 方法,这个方法在对象的生命中只会被调用这一次;
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) {
// 参照我们之前学习的 HTTP 服务器类似的原理,进行 HTTP 协议的请求解析,和响应构建
HttpServletRequest req = HttpServletRequest.parse(socket);
HttpServletRequest resp = HttpServletRequest.build(socket);
// 判断 URL 对应的文件是否可以直接在我们的根路径上找到对应的文件,如果找到,就是静态
// 直接使用我们学习过的 IO 进行内容输出
if (file.exists()) {
// 返回静态内容
return;
}
// 走到这里的逻辑都是动态内容了
// 根据我们在配置中说的,按照 URL -> servlet-name -> Servlet 对象的链条
// 最终找到要处理本次请求的 Servlet 对象
Servlet ins = findInstance(req.getURL());
// 调用 Servlet 对象的 service 方法
// 这里就会最终调用到我们自己写的 HttpServlet 的子类里的方法了
try {
ins.service(req, resp);
} catch (Exception e) {
// 返回 500 页面,表示服务器内部错误
}
}
}
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 {
//访问链接:http://127.0.0.1:8080/hello_servlet/showreq?key=10&a=100&b=200
StringBuilder stringBuilder = new StringBuilder();
resp.setContentType("text/html; charset=utf-8");
//1协议名称与版本
stringBuilder.append("协议版本:");
stringBuilder.append(req.getProtocol());
stringBuilder.append("<br>");
//2方法类型
stringBuilder.append("方法:");
stringBuilder.append(req.getMethod());
stringBuilder.append("<br>");
//3获取查URL路径
stringBuilder.append("URL路径:");
stringBuilder.append(req.getRequestURI());
stringBuilder.append("<br>");
//4URL(不包括查询字符串后面的部分)
stringBuilder.append("URL(不包括查询字符串后面的部分):");
stringBuilder.append(req.getRequestURL());
stringBuilder.append("<br>");
//5一级路径
stringBuilder.append("一级路径:");
stringBuilder.append(req.getContextPath());
stringBuilder.append("<br>");
//6查询字符串
stringBuilder.append("查询字符串:");
stringBuilder.append(req.getQueryString());
stringBuilder.append("<br>");
//7正文编码格式
stringBuilder.append("正文编码格式:");
stringBuilder.append(req.getCharacterEncoding());
stringBuilder.append("<br>");
//8mine
stringBuilder.append("mine:");
stringBuilder.append(req.getContentType());
stringBuilder.append("<br>");
//9正文长度
stringBuilder.append("正文长度:");
stringBuilder.append(req.getContentLength());
stringBuilder.append("<br>");
//10获得每一个查询字符串的键值:
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>");
}
//11获得头部的键值
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格式:
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 {
//获取post请求body请求中的参数
//设置请求与响应编码格式
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html; charset=utf8");
//比如useId = classId=
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
//写会数据
resp.getWriter().write("userId=" + userId + ", " + "classId=" + classId);
}
}
运行结果:
json格式:
对于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>
<!-- 前端HTML部分 -->
<input type="text" id="userId">
<input type="text" id="classId">
<input type="button" id="submit" value="提交">
<!-- 如果已经准备好本地的jQuery就导入本地的,否则可以找网上的jQuery cdn网络路径 -->
<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 {
//1.创建一个Jackson的核心对象
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//格式
resp.setContentType("text/html; charset=utf8");
//处理
//2.读取body请求的内容,使用ObjectMapper对象的readValue方法来解析
//就是将字符串转换成java的对象,readValue方法的第一个参数可以是路径字符串可以是输入流对象,引入可以是File对象
//第二个参数,表示需要将请求的json格式数据转换成哪一个java对象
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");
//设置Refresh,第二个参数表示刷新频率,单位是秒
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.setStatus(302);
// //设置重定向字段与地址,如跳转到力扣官网
// resp.setHeader("Location", "https://leetcode.cn/");
resp.sendRedirect("https://leetcode.cn/");
}
}
效果与上面第一种方法是一样的。
下期预告:表白墙小案例
- 点赞
- 收藏
- 关注作者
评论(0)