海上生明月,天涯共此时,祝大家中秋快乐呀~
目录
1.Tomcat伪代码
Tomcat伪代码包括了两部分的核心逻辑,一是Tomcat的初始化,二是处理请求。
①Tomcat的初始化:
(1)Tomcat先从指定目录中找到所有要加载的servlet类。
前面我们部署的时候,就是把servlet代码编译成了.class,然后打了war包,然后拷贝到了webapps目录下,而Tomcat就会从这个目录下来找到哪些.class对应的是servlet类,并且需要进行加载。
(2)根据刚才类加载的结果,给这些类创建servlet实例
// 用来存储所有的 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); }
(3)实例创建好之后,就可以调用当前servlet实例的init方法了。
// 调用每个 Servlet 对象的 init() 方法,这个方法在对象的生命中只会被调用这一次; for (Servlet ins : instanceList) { ins.init(); }
init()方法是servlet自带的方法,默认情况下什么也不干,我们在继承一个HTTPServlet的时候,也可以选择自己重写init,从而在这个阶段写一些能够帮助我们完成一些初始化工作的内容。
(4)创建TCP socket,监听8080端口,等待客户端来连接。
// 利用我们之前学过的知识,启动一个 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); }); }
每次当客户端有连接的时候,accpet就会返回,然后给当前线程池里加上一个任务,然后doHttpRequest(socket)就在这个任务里负责这个请求。Tomcat工作的大部分时间都是在这个循环里完成的。
(5)如果循环退出来,Tomcat也要结束了的时候,就会依次调用每个servlet的destroy的方法。
// 调用每个 Servlet 对象的 destroy() 方法,这个方法在对象的生命中只会被调用这一次; for (Servlet ins : instanceList) { ins.destroy(); }
这是属于Tomcat的收尾工作,和init一样,默认也是什么也不干,但是也是可以被我们重写的。除此之外,虽然有destroy这个流程,但是这个环节并不一定可靠,只有当Tomcat是正常流程结束时,才可以这么调用,而大部分情况都是采用直接结束进程这种非正常流程来退出的,这个时候就来不及来调用它。
②Tomcat处理请求
(1)Tomcat 从 Socket 中读到的 HTTP 请求是一个字符串, 然后会按照 HTTP 协议的格式解析成一个HttpServletRequest 对象.(req,而此处resp相当于new了一个空对象)
(2)Tomcat 会根据 URL 中的 path 判定这个请求是请求一个静态资源还是动态资源. 如果是静态资源, 直接找到对应的文件把文件的内容构造到resp对象的body中,以 Socket(即返回这个resp对象)返回。如果是动态资源, 才会执行到 Servlet 的相关逻辑。
(3)Tomcat 会根据 URL 中的 Context Path(确定是哪一个webapp) 和 Servlet Path(确定是哪个servlet类) 确定要调用哪个 Servlet 实例的 service。如果没有找到匹配的,就会返回404。
(4)找到刚刚servlet的对象来调用service 方法,在service内部就会进一步调用到我们之前写的 doGet 或者 doPost 。
方法 .代码如下:
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 页面,表示服务器内部错误 } } }
其中第(4)步代码:
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); } ...... } }
在整套流程中涉及到的servlet方法主要有三个:(也被称为一个servlet的生命周期)
(1)初始化阶段,对象创建好了之后,就会执行到.用户可以重写这个方法,来执行一些初始化逻辑。
(2)在处理请求阶段来调用.每次来个请求都要调用一次
service
(3)退出主循环,
tomcat
结束之前会调用,用来释放资源.
2.Servlet API
2.1HTTPServlet
①常见API
在实际中我们主要重写do...的方法,很少重写init,destory,service这些方法。
注意: HttpServlet 的实例只是在程序启动时创建一次. 而不是每次收到 HTTP 请求都重新创建实例.
②处理GET请求:
(1)直接在浏览器中,通过URL就能构造(GET请求最常用用法)
(2)通过ajax构造GET请求:
创建 MethodServlet.java, 创建 doGet 方法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 doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //super.doGet(req, resp); resp.setContentType("text/html; charset=utf-8"); resp.getWriter().write("GET response获取响应"); } }
创建 TestMethod.html, 放到 webapp 目录中,与WEB-INF处于同级关系<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script> $.ajax({ type: 'get', url: 'method', success: function (body) { console.log(body); } }); </script> </body> </html>
启动smart Tomcat一键打包部署后结果如下:
注意!!!加上resp.setContentType("text/html; charset=utf-8");
不然的话遇到中文字符就会出现如图所示乱码情况
③处理POST请求:
我们这里是直接通过ajax来构造请求的
(1)java代码:
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 { //super.doGet(req, resp); resp.setContentType("text/html; charset=utf-8"); resp.getWriter().write("post response响应"); } }
(2)html代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script> $.ajax({ type: 'post', url: 'method', success: function (body) { console.log(body); } }); </script> </body> </html>
注意!!!
a.创建 TestMethod.html, 放到 webapp 目录中,与WEB-INF处于同级关系。
b.还需要注意这里的服务资源路径
c.打包部署后:
post是不能通过地址栏访问,现在是js里边写代码,通过ajax来访问的。所以我们可以通过ajax页面来进行访问。
d.我们还需要是否加/的问题。
2.2HTTPServletRequest
当 Tomcat 通过 Socket API 读取 HTTP 请求 ( 字符串 ), 并且按照 HTTP 协议的格式把字符串解析成 HttpServletRequest 对象。HttpServletRequest对应到一个HTTP请求,HTTP请求中有啥,这里就有啥。①一些常见API:
方法 描述 String getProtocol() 返回请求协议的名称和版本 String getMethod() 返回请求的 HTTP 方法的名称,例如,GET、POST 或 PUT String getRequestURI() 从协议名称直到 HTTP 请求的第一行的查询字符串中,返回该请求的 URL 的一部分 String getContextPath() 返回指示请求上下文的请求 URI 部分。 String getQueryString() 返回包含在路径后的请求 URL 中的查询字符串,得到完整的查询字符串 Enumeration getParameterNames() 返回一个 String 对象的枚举,包含在该请求中包含的参数的名称,得到所有的key,以 Enum的方式来表示. String getParameter(String name) 以字符串形式返回请求参数的值,或者如果参数不存在则返回null ,根据key来拿到value 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)![]()
(2)
(3)
(4)
③我们用代码来演示一些部分方法:
代码如下:
public class showGetAPI extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("<h3>首行部分 </h3>"); //返回请求协议的名称和版本,并换行 stringBuilder.append(req.getProtocol()); stringBuilder.append("<br>"); //返回请求的 HTTP 方法的名称 stringBuilder.append(req.getMethod()); stringBuilder.append("<br>"); //从协议名称直到 HTTP 请求的第一行的查询字符串中,返回该请求的 URL 的一部分 stringBuilder.append(req.getRequestURI()); stringBuilder.append("<br>"); //返回指示请求上下文的请求 URI 部分。 stringBuilder.append(req.getContextPath()); stringBuilder.append("<br>"); //返回包含在路径后的请求 URL 中的查询字符串,得到完整的查询字符串 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=utf-8"); resp.getWriter().write(stringBuilder.toString());//将整个字符串拼接内容进行打印 } }
结果如下:
④最常用的其实是getParameter这个方法,用来获取querystring中的详细内容
(GET 请求中的参数一般都是通过 query string 传递给服务器的. 形如
https://v.bitedu.vip/personInf/student?userId=1111&classId=100此时浏览器通过 query string 给服务器传递了两个参数, userId 和 classId, 值分别是 1111 和 100在服务器端就可以通过 getParameter 来获取到参数的值. )我们用代码来进行演示: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("/GetParameter") public class GetParameter extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html; charset=utf-8"); //为这个GET设置两个String类型的参数 String userName=req.getParameter("userName"); String password=req.getParameter("password"); resp.getWriter().write("userName="+userName+","+"passwprd="+password); } }
当没有 query string的时候, getParameter 获取的值为 null.
这个时候如果我们这样访问127.0.0.1:8080/untitled20/GetParameter?userName=张三&password=123456就可以得到下面的。
此时说明服务器已经获取到客户端传递过来的参数.
⑤POST请求body格式
POST 请求的参数一般通过 body 传递给服务器 . body 中的数据格式有很多种。这里主要说三种(1)x-www-form-urlencoded如果请求是这种格式,服务器获取参数的方式和GET一样,也是getParameter。那么如何在前端构造一个这样的格式请求呢?
1.form表单java代码: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 PostParameter extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf-8"); String userName=req.getParameter("userName"); String password=req.getParameter("password"); resp.getWriter().write("userName="+userName+","+"password="+password); } }
html代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>PostParameter</title> </head> <body> <form action="postParameter" method="post"> <input type="text" name="userName"> <input type="text" name="password"> <input type="submit" name="提交"> </form> </body> </html>
如何建立连接?
结果展示:当你在网页输入PostParameter
http://127.0.0.1:8080/untitled20/TestPost.html
时,会出现
输入后提交会出现:
2.postman比较简单,这里就不再统一说了
(2)json
所以,我们可以使用第三方的库来直接处理json格式数据.这里主要使用的库叫做Jackson
(Spring
官方推荐的库),通过maven
把jackson
这个库,给下载到本地并引入到项目中。a.在中央仓库中找到Jackon相关API,并且在pom.xml中引入jackson依赖:
在pom.xml中引入依赖:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12.7</version> </dependency>
b.在浏览器前端代码中,通过js构造出body为json格式的请求
其中用
ajax来
构造post
请求,使用contentType
来说明请求的类型,data
属性来设置body
的内容。<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <!-- 构造json格式的请求--> <input type="text" id="userId"> <input type="text" id="classId"> <input type="button" id="submit" value="提交" > <script src="https://code.jquery.com/jquery-3.6.0.min.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: 'postJson', contentType: 'application/json', data: JSON.stringify({ userId: userIdInput.value, classId: classIdInput.value }), success: function(body) { console.log(body); } }); } </script> </body> </html>
其中值得注意的进一步说明:
c.在Java后端代码中,通过Jackon来进行处理,需要使用Jackon,把请求body中的数据读取出来,并且解析成Java中的对象
代码:
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;//这个类的定义是都行. 但是你前端传的数据类型要匹配,虽然在这个地方可以用int,但是理论上我们不确定 jackson 里是否是帮咱们做类型转换了 } @WebServlet("/postJson")//注意此注解只能够在servlet类中使用,要挨着servlet这个类 public class PostJsonSelvlet extends HttpServlet { //1.创建一个json核心对象 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方法来解析 //readValue的作用就是把json格式的字符串转换成java对象 //readValue中有两个参数,第一个参数的含义是对哪个字符串进行转换 //第二个参数,表示需要将请求的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); } }
结果显示如下:
d.readValue是怎么完成转换的?
1.先把getInputStream对应的流对象里面的数据都读取出来;
2.针对这个json字符串进行解析,从字符串=>键值对;
3遍历这个键值对,依次获取到每一个key 。根据这个key 的名字,和User类里面的属性名字对比一下,看有没有匹配的名字!!如果发现匹配的属性,则把当前key对应的value赋值到该User类的属性中(赋值的过程中同时会进行类型转换),如果没有匹配的属性,就跳过,取下一个key.
4.当把所有的键值对都遍历过之后,此时User对象就被构造的差不多了.(3)form-data这里也不详细讲了。
2.3HTTPServletResponse
Servlet 中的 doXXX 方法的目的就是根据请求计算得到相应, 然后把响应的数据设置到HttpServletResponse 对象中.然后 Tomcat 就会把这个 HttpServletResponse 对象按照 HTTP 协议的格式, 转成一个字符串, 并通过 Socket 写回给浏览器.①核心API:
注意: 响应对象是服务器要返回给浏览器的内容, 这里的重要信息都是程序猿设置的. 因此上面的方法都是 "写" 方法.注意: 对于状态码/响应头的设置要放到 getWriter / getOutputStream 之前. 否则可能设置失效.②实例一:设置状态码:
实现一个程序 , 用户在浏览器通过参数指定要返回响应的状态码 .(使用的是GET,因为更简单)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 setStatus extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf8"); int status=500; resp.setStatus(status); resp.getWriter().write("响应状态码是"+status); } }
展示结果:
用post,只需要把java代码中的doGet改为doPost,然后再添加一下下面的代码即可。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>status</title> </head> <body> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script> $.ajax({ type:'post', url:'status', success: function (body) { console.log(body); } }); </script> </body> </html>
展示结果:
③实例二:自动刷新
这是用GET的代码:
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("/refresh") public class autoRefresh 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()); } }
展示结果:
每秒会自动刷新
如果用post的话只需要把方法改doPost,然后加上下面这段
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>status</title> </head> <body> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script> $.ajax({ type:'post', url:'refresh', success: function (body) { console.log(body); } }); </script> </body> </html>
输出结果在控制台上依旧可以看见:
同样每次刷新,时间戳就会发生变化。
④实例三:重定向
实现一个程序, 返回一个重定向 HTTP 响应, 自动跳转到另外一个页面.
GET代码:
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 RediractServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //实现重定向,让浏览器自动跳转到搜狗浏览器 resp.setStatus(302); resp.setHeader("Location","https://www.baidu.com"); //另一种更简单的重定向写法 //resp.sendRedirect("https://www.baidu.com"); } }
结果如下:会自动跳转到百度