Servlet是属于上层建筑,它处在应用层,它的下层有传输层,网络层,数据链路层,硬件,属于“经济基础”,毕竟下层经济基础决定上层建筑。前面说过,Servlet是一组操作HTTP的API,Tomcat可作为HTTP服务器来处理请求,这个处理请求的关键就是调用Servlet来操作HTTP给客户端做出响应。
我们所写的Servlet代码没有main方法,那他是如何运行的呢?其实是Tomcat在调用Servlet,Tomcat其实就是一个应用程序,是运行在用户态上的一个普通的Java进程。
当浏览器发送请求给服务器的时候,Tomcat作为HTTP Server会调用Serlvet API,然后执行我们所写的Servlet程序来处理请求。
处理请求的过程中牵涉的不仅仅只有HTTP,还有其他层的协议,但是我们并没有感知到其他层协议的细节,只关注了应用层HTTP协议的细节,这就是协议分层好处,程序员在实现处理请求时,不必去关心应用层下面的细节。
为了方便描述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
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 中写入二进制格式数据. |
在同一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请求"); }}
重新打包部署,刷新页面:
对于请求的信息,我们运用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("
"); //2方法类型 stringBuilder.append("方法:"); stringBuilder.append(req.getMethod()); stringBuilder.append("
"); //3获取查URL路径 stringBuilder.append("URL路径:"); stringBuilder.append(req.getRequestURI()); stringBuilder.append("
"); //4URL(不包括查询字符串后面的部分) stringBuilder.append("URL(不包括查询字符串后面的部分):"); stringBuilder.append(req.getRequestURL()); stringBuilder.append("
"); //5一级路径 stringBuilder.append("一级路径:"); stringBuilder.append(req.getContextPath()); stringBuilder.append("
"); //6查询字符串 stringBuilder.append("查询字符串:"); stringBuilder.append(req.getQueryString()); stringBuilder.append("
"); //7正文编码格式 stringBuilder.append("正文编码格式:"); stringBuilder.append(req.getCharacterEncoding()); stringBuilder.append("
"); //8mine stringBuilder.append("mine:"); stringBuilder.append(req.getContentType()); stringBuilder.append("
"); //9正文长度 stringBuilder.append("正文长度:"); stringBuilder.append(req.getContentLength()); stringBuilder.append("
"); //10获得每一个查询字符串的键值: stringBuilder.append("获得每一个查询字符串的键值:
"); Enumeration query = req.getParameterNames(); while(query.hasMoreElements()) { String key = (String)query.nextElement(); stringBuilder.append(key); stringBuilder.append(":"); stringBuilder.append(req.getParameter(key)); stringBuilder.append("
"); } //11获得头部的键值 stringBuilder.append("获得头部的键值:
"); Enumeration header = req.getHeaderNames(); while(header.hasMoreElements()) { String key = (String)header.nextElement(); stringBuilder.append(key); stringBuilder.append(":"); stringBuilder.append(req.getHeader(key)); stringBuilder.append("
"); } resp.getWriter().write(stringBuilder.toString()); }}
结果:
我们知道post请求的请求信息在http格式中的 body 部分当中,而 body 中的请求内容的格式是有很多种的,比如最常见的有:
k e y = v a l u e & k e y = v a l u e & . . . key=value\&key=value\&... k e y = v a l u e & k e y = v a l u e & . . .
form表单创建 x-www-form-urlencode格式 请求:
html><html lang="ch"><head> <meta charset="UTF-8"> <title>posttitle>head><body> <form action="./postParameter" method="post" accept-charset="utf-8"> <span>userIdspan> <input type="text" name="userId"> <span>classIdspan> <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格式:
{ \{ {
k e y : v a l u e , key:value, k e y : v a l u e ,
k e y : v a l u e , key:value, k e y : v a l u e ,
k e y : v a l u e , key:value, k e y : v a l u e ,
. . . ... . . .
} \} }
对于json格式,手动解析不容易,因为json里面的字段是可以嵌套的,但我们可以借助第三方库来解析处理json,比如Jackson,Jackson依赖导入过程如下:
处理json请求步骤:
第一步,在前端js代码中构造出格式为 json 格式的请求。
其中 ajax 构造 post 请求,使用 contentType 来说明请求的类型, data 属性来设置 body 的内容。
html><html lang="cn"><head> <meta charset="UTF-8"> <title>jsontitle>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处理。
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 方法基本原理:
案例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/"); }}
效果与上面第一种方法是一样的。