简介Servlet1:https://developer.aliyun.com/article/1521653
四、Servlet的运行原理
Servlet是属于上层建筑,下面的传输层、网络层、数据链路层和物理层属于经济基础。
Servlet是Tomcat提供的API,但是Tomcar其实也只是一个应用程序,是运行在用户态的普通进程,然后用户写代码(根据请求计算响应),通过Servlet和Tomcat进行交互,然后Tomcat进一步和浏览器之间进行网络传输。
具体的过程也可以参考下图:
接收请求:用户会在浏览器输入一个URL,然后浏览器就出构造出相应的HTTP请求,这个HTTP请求会经过网络协议栈逐层封装成二进制的比特流,最后经过物理层的硬件设备转换成光信号或者电信号传输出去,然后这些信号再经过互联网的一系列网络设备到达服务器主机,服务器主机经过逐层分用还原得到HTTP请求交给Tomcat进行处理,然后利用HTTP请求的格式进行解析。根据 请求中的 Context Path 确定一个 webapp, 再通过 Servlet Path 确定一个具体的 类. 再根据当前请 求的方法 (GET/POST/...), 决定调用这个类的 doGet 或者 doPost 等方法. 此时我们的代码中的 doGet / doPost 方法的第一个参数 HttpServletRequest 就包含了这个 HTTP 请求的详细信息。
返回响应:doGet / doPost 执行完毕后, Tomcat 就会自动把 HttpServletResponse 这个我们刚设置 好的对象转换成一个符合 HTTP 协议的字符串, 通过 Socket 把这个响应发送出去.
此时响应数据在服务器的主机上通过网络协议栈层层 封装 , 最终又得到一个二进制的 bit 流 , 通过 物理层硬件设备转换成光信号/ 电信号传输出去 . 这些承载信息的光信号/ 电信号通过互联网上的一系列网络设备 , 最终到达浏览器主机,收到这些光信号/ 电信号 , 又会通过网络协议栈逐层进行 分用 , 层层解析 , 最终还原成 HTTP 响应 , 并交给浏览器处理 . 浏览器也通过 Socket 读到这个响应 ( 一个字符串 ), 按照 HTTP 响应的格式来解析这个响应 . 并且把 body 中的数据按照一定的格式显示在浏览器的界面上 .
五、Tomcat伪代码
通过 " 伪代码 " 的形式描述了 Tomcat 初始化 / 处理请求 两部分核心逻辑 .
1、Tomcat初始化
a、让Tomcat先从指定的目录中找到要加载的Servlet类
在前面部署的时候将Servlet代码编译成.class文件,然后打包成war包,并且拷贝到webapps文件夹里面,Tomcat就会从webapps里找到.class文件对应的Servlet类,然后根据需要加载
b、 根据类加载结果,给这些类创建Servlet实例
// 这里要做的的是实例化出所有的 Servlet 对象出来; for (Class<Servlet> cls : allServletClasses) { // 这里是利用 java 中的反射特性做的 // 实际上还得涉及一个类的加载问题,因为我们的类字节码文件,是按照约定的 // 方式(全部在 WEB-INF/classes 文件夹下)存放的,所以 tomcat 内部是 // 实现了一个自定义的类加载器(ClassLoader)用来负责这部分工作。 Servlet ins = cls.newInstance(); instanceList.add(ins); }
c、实例创建完成之后,调用当前Servlet实例的init方法。
// 调用每个 Servlet 对象的 init() 方法,这个方法在对象的生命中只会被调用这一次; for (Servlet ins : instanceList) { ins.init(); }
d、创建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); }); }
e、退出循环,依次调用调用Servlet的destroy的方法
// 调用每个 Servlet 对象的 destroy() 方法,这个方法在对象的生命中只会被调用这一次; for (Servlet ins : instanceList) { ins.destroy(); }
2、Tomcat处理请求
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 页面,表示服务器内部错误 } } }
Tomcat 从 Socket 中读到的 HTTP 请求是一个字符串, 然后会按照 HTTP 协议的格式解析成一个HttpServletRequest 对象.
Tomcat 会根据 URL 中的 path 判定这个请求是请求一个静态资源还是动态资源. 如果是静态资源, 直接找到对应的文件把文件的内容通过 Socket 返回. 如果是动态资源, 才会执行到 Servlet 的相关 逻辑.
Tomcat 会根据 URL 中的 Context Path 和 Servlet Path 确定要调用哪个 Servlet 实例的 service 方法.
通过 service 方法, 就会进一步调用到我们之前写的 doGet 或者 doPost。
3、Servlet 的 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); } ...... } }
根据Servlet对象来调用service方法,在service方法的内部又会进一步地调用doGet等方法。
通过上述整套流程中,可以看出Servlet的生命周期主要有三个阶段:
init:初始化阶段,对象创建好之后就会执行init方法,用户可以重写这个方法来初始化逻辑。
service:在处理请求阶段来调用,每次请求都会调用service。
destroy:退出主循环,tomcat结束之前都会调用来释放资源。
六、Servlet关键API
当前主要使用HttpServlet类、HttpServletRequest类和HttpServletResponse类。
1、HttpServlet类
在写代码创建的类都继承自HttpServlet类,这个类有以下常用方法:
创建一个MethodServlet类继承Servlet类,并且重写doPost方法。
@WebServlet("/method") public class MethodServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf8");//将字符集指定为utf-8,避免乱码 resp.getWriter().write("POST响应"); } }
但是对于POST请求在浏览器中通过URL无法直接访问,就还需要通过form表单或者ajax来进行实现。
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script> <script> $.ajax({ type: 'post', url: 'method', success: function(body){ console.log(body); } }); </script> </body> </html>
这个html在目录文件的位置:
通过127.0.0.1:8080/test/test.html在浏览器访问,通过控制台可以看到如下结果:
2、HttpServletRequest 类
HttpServletRequest类对应的是一个Http请求,当 Tomcat 通过 Socket API 读取 HTTP 请求, 并且按照 HTTP 协议的格式把字符串解析成 HttpServletRequest 对象。其常用方法:
上述的方法都只是进行读操作。
打印HTTP请求信息:
@WebServlet("/show") public class ShowRequestServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf-8"); StringBuilder respondBody = new StringBuilder(); respondBody.append(req.getProtocol()); respondBody.append("<br>"); respondBody.append(req.getMethod()); respondBody.append("<br>"); respondBody.append(req.getRequestURI()); respondBody.append("<br>"); respondBody.append(req.getContextPath()); respondBody.append("<br>"); respondBody.append(req.getQueryString()); respondBody.append("<br>"); Enumeration<String> headerNames = req.getHeaderNames(); while(headerNames.hasMoreElements()){ String headerName = headerNames.nextElement(); respondBody.append(headerName+" "); respondBody.append(req.getHeaders(headerName)); respondBody.append("<br>"); } resp.getWriter().write(respondBody.toString()); } }
在浏览器通过url访问:
获取POST请求的参数 :
首先POST请求body的格式有:x-www-form-urlencoded,这种格式需要利用form表单来进行构造。
@WebServlet("/postParameter") public class PostGetParameterServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String userName = req.getParameter("userName"); String pwd = req.getParameter("pwd"); resp.getWriter().write("userName:"+userName+" pwd:"+pwd); } }
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <form action="postParameter" method="post"> <input type="text" name="userName"> <input type="password" name="pwd"> <input type="submit" value="提交"> </form> <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script> </body> </html>
运行效果:
点击提交按钮之后:
POST请求body格式还有json格式,但是这种格式需要引入第三方库Jackson,需要在maven中心库中找到然后引入到pom.xml之中,然后前端代码中需要在JS构造出body格式为json的请求。
<body> <input type="text" id="userName"> <input type="text" id="password"> <input type="button" value="提交" id="submit"> <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"> </script> <script> let userNameInput = document.querySelector('#userName'); let passwordInput = document.querySelector('#password'); let button = document.querySelector('#submit'); button.onclick = function(){ $.ajax({ type:'post', url:'postJason', contentType:'application/json', data:JSON.stringify({ userName:userNameInput.value, password:passwordInput.value }), success:function(body){ console.log(body); } }); } </script> </body>
后端代码使用Jackson,将请求从body中读取出来,并且解析为Java对象。
class User{ public String userName; public String password; } @WebServlet("/postJason") public class PostJasonServlet extends HttpServlet { //创建一个Jason对象 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); resp.getWriter().write("userName:"+user.userName+" password:"+user.password); } }
运行效果:
提交之后可以在控制台上看到:
3、HttpServletResponse类
Servlet 中的 doXXX 方法的目的就是根据请求计算得到相应, 然后把响应的数据设置到 HttpServletResponse 对象中. 然后 Tomcat 就会把这个 HttpServletResponse 对象按照 HTTP 协议的格式, 转成一个字符串, 并通过 Socket 写回给浏览器.
其常见方法如下:
可以写一个自动刷新的页面:
@WebServlet("/refresh") public class RefreshServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setHeader("Refresh","1"); resp.getWriter().write("time"+System.currentTimeMillis()); } }
运行效果: