目录
🌟一、Servlet运行原理
🌈1、Servlet的执行原理(重点)
🌈2、Tomcat伪代码的简单理解
2.1 Tomcat初始化流程
2.2 Tomcat处理请求流程
2.3 Servlet 的 service 方法的实现
🌟二、Servlet API 详解
1、HttpServlet
2、HttpServletRequest
3、HttpServletResponse
面试题:请求转发与请求重定向的区别?
🌟一、Servlet运行原理
🌈1、Servlet的执行原理
1、问题:
在Servlet的代码中我们并没有写main方法,那么对应的doGet方法是如何被调用的?响应又是如何返回给浏览器的?这就和Tomcat有关了。
2、Servlet的具体执行过程
我们自己的实现是在Tomcat中实现的。
客户端发送请求——>到Tomcat的webServer——>Servlet管理器(多个)——>Servlet实例。
总结起来,Servlet工作原理是:客户端发送请求到Web服务器,Web服务器把请求交给Servlet容器,Servlet容器根据请求的URI来确定应该由哪一个Servlet来处理请求,Servlet容器调用Servlet的init()、service()等方法,Servlet实例根据请求对象进行相应的业务逻辑处理,并把处理结果存储到响应对象中,最终Web服务器把响应返回给客户端。
具体过程:(1)接收请求
- 用户在浏览器输入一个URL,此时浏览器就会构造出一个HTTP请求;
- HTTP请求开始从应用层往下逐层封装数据(打包)得到一个二进制的bit流,最后通过物理层将数据传输给服务器端的物理层;
- 服务器端的物理层接收到数据之后,开始从物理层往上逐层分用,层层解析数据(解析),最终还原出HTTP请求,并交给Tomcat进程进行处理(根据端口号确定进程)
- Tomcat通过Socket读取到这个请求(一个字符串),并按照HTTP请求的格式来解析这个请求:根据请求中的Context Path确定一个webapp,再通过Servlet Path确定一个具体的类,再根据当前请求的方法(GET或者POST或其他)决定调用这个类的doGet或者doPost方法。此时我们的代码中的doGet或者doPost方法的第一个参数HttpServletRequest 就包含了这个 HTTP 请求的详细信息。
(2)根据请求计算响应
在我们的doGet或者doPost执行完毕之后,就执行到了我们自己的代码。我们的代码会根据请求中的一些信息,来给HttpServletResponse对象设置一些属性:比如状态码,header,body等。
(3)返回响应
- 等我们的doGet或者doPost执行结束之后,Tomcat就会自动将HttpServletResponse这个我们刚设置好的对象转化为一个符合HTTP协议的字符串,通过Socket将这个响应发送出去;
- 然后响应数据在服务器的主机上又通过网络协议栈层层封装,得到一个二进制的bit流,通过物理层将数据传输出去;
- 此时浏览器的物理层收到了响应数据,从下往上到应用层将数据进行分用,还原成HTTP响应,交给浏览器处理;
- 浏览器通过Socket读到这个响应(一个字符串),按照HTTP响应的格式来解析这个响应,并将body中的数据按照一定的格式显示在浏览器的界面上。
🌈2、Tomcat伪代码的简单理解
2.1 Tomcat初始化流程
class Tomcat {
//2、存储所有的Servlet对象
private List<Servlet> instanceList = new ArrayList<>();
//3、定义start方法
public void start(){
//(1)根据约定,读取WEB-INF/web.xml配置文件
//(2)解析出@webServlet注解修饰的类
//(3)假设这个数组中就包含了我们解析到的所有被@webServlet注解修饰的类
Class<Servlet>[] allServletClasses = ...;
//(4)实例化出所有的Servlet对象出来
for(Class<Servlet> cls : allServletClasses){
//注意:这里是利用java的反射做的,实际上还涉及到一个类的加载问题,因为我们的类字节码文件是按照约定的方式,全部在WEB-INF/classes文件夹下存放的,所以TOmcat内部实现了一个自定义的类加载器C(ClassLoader)来负责这部分工作
Servlet ins = cls.newInstance();
instanceList.add(ins);
}
//(5)调用每个Servlet对象的init()方法,每个init()方法在对象的生命周期中只会调用一次
for(Servlet ins : instanceList){
ins.init();
}
//(6)启动一个HTTP服务器,并用线程池的方式分别处理每一个Request
ServerSocket serverSocket = new ServerSocket(8080);
//注意:实际上tomcat并不是用的固定线程池,这里只是为了说明情况
while(true){
Socket socket = ServerSocket.accpect();
// 每个请求都是用一个线程独立支持,这里体现了我们 Servlet 是运行在多线程环境下的
pool.execute(new Runnable() {
doHttpRequest(socket);
});
}
//(7)调用每个 Servlet 对象的 destroy() 方法,这个方法在对象的生命中只会被调用这一次;
for (Servlet ins : instanceList) {
ins.destroy();
}
}
//1、main方法
public static void main(String[] args) {
new Tomcat().start();
}
Tomcat初始化流程总结:
内置main方法,启动start()——>获取@webServlet注解修饰的类——>通过反射获取@注解修饰的类的实例——>实例调用init()——>Tomcat使用多线程的方式实现多个Http请求的响应——>销毁实例:destory()
Tomcat在启动的时候,自己有一个main方法,Servlet自己里面没有main方法,Tomcat在启动的时候,会在整个工程里面找所有加了注解@webServlet(“URL”)的类, 并将它识别为Servlet,调用init()方法初始化;而且会找到注解相应的路径,根据用户URL中的地址,解析出时哪一个Servlet,再执行它的doGet和doPost方法。
(1)Tomcat的代码中内置了main方法,当我们启动Tomcat之后,就是从Tomcat的main方法开始指定的;
(2)被@webServlet注解修饰的类会在Tomcat启动的时候就被获取到,并集中管理;
(3)Tomcat通过反射这种语法机制来创建被@webServlet注解修饰的类的实例;
(4)这些实例被创建完成之后,会调用其中的init方法进行初始化;
注意:这个init()方法在整个生命周期中只会执行一次。并且这个方法是HttpServlet自带的,我们自己写的类可以重写init;
(5)这些实例被销毁之前,会调用destory()方法;(同样这个类也是HttpServlet自带的,可以自己重写。)
(6)Tomcat内部也是通过Socket API进行网络通信的;
(7)Tomcat为了同时响应多个Http请求,采取了多线程的方式实现。因此Servlet是运行在多线程环境下的。
2.2 Tomcat处理请求流程
在2.1的初始化代码中,有一段用来处理请求的,我们来具体看一下这段代码的含义。
class Tomcat{
//处理请求
void doHttpRequest(Socket socket){
//(1)进行HTTP协议的请求解析和响应构建
HTtpServletRequest req = HttpServletRequest.parse(socket);
HTtpServletRequest resp = HttpServletRequest.build(socket);
//(2)判断URL对应的文件是否可以在我们的根路径上找到对应的文件,如果可以找到,就是静态内容,直接使用IO进行内容输出
if(file.exists()){
//返回静态内容
return;
}
//(3)否则就是动态内容
//按照URL——servlet-name —— Servlet独享的链条,最终找到要处理本次请求的Servlet对象
Servlet ins = findInstance(req.getURL());
//(4)调用Servlet对象的service方法,这里就会最终调用到我们最终写的HttpServlet的子类里面的方法了
try {
ins.service(req, resp);
} catch (Exception e) {
// 返回 500 页面,表示服务器内部错误
}
}
}
Tomcat 处理请求流程小结:
(1)Tomcat从Socket中读取到的HTTP请求是一个字符串,因此会按照HTTP协议的格式解析成一个HttpServletRequest 对象;(2)Tomcat会根据URL的path判定这个请求是请求一个静态资源还是动态资源:如果是静态资源,直接找到对应的文件将文件中的内容通过Socket返回;如果 是动态资源,才会执行到Servlet的相关逻辑;
(3)Tomcat会根据URL中的Context Path和Servlet Path确定要调用哪个Servlet实例的service方法;
(4)通过Service方法,就会进一步调用到我们之前写的doGet或者doPost方法。
2.3 Servlet 的 service 方法的实现
在2.2节doHttpRequest方法下:
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 方法的实现小结:
(1)Servlet的service方法内部会根据当前请求的方法来决定调用其中的某个doXXX方法;
(2)在调用doXXX方法的时候,就会触发多态机制,从而执行到我们自己写的子类中的doXXX方法。
问题:理解此处的多态
(1)我们前面【Servlet学习一】代码中的Demo1_HelloServlet类,继承自HttpServlet,HttpServlet又继承于Servlet。因此相当于Demo1_HelloServlet是Servlet的子类;
(2)接下来在Tomcat启动阶段,Tomcat就根据注解的描述,创建了Demo1_HelloServlet的实例,然后将实例放在了Servlet数组中;
(3)后面我们根据请求的URL从数组中获取到了Demo1_HelloServlet实例,但是我们是通过Servlet ins这样的父类引用来获取到Demo1_HelloServlet实例的;
(4)最后, 我们通过 ins.doGet() 这样的代码调用 doGet 的时候, 正是 "父类引用指向子类对象",此时就会触发多态机制,从而调用到我们之前在 Demo1_HelloServlet中所实现的 doGet 方法。
等价代码:
Servlet ins = new Demo1_HelloServlet(); ins.doGet(req, resp);
🌟二、Servlet API 详解
我们之前介绍过,实现的一个简单的Servlet的部分代码如下:
写 Servlet 代码的时候, 首先第一步就是先创建类, 继承自 HttpServlet, 并重写其中的某些方法。
有了HttpServlet才有后续的HttpServletRequest和HttpServletResponse,因此主要介绍这三部分。
1、HttpServlet
我们写 Servlet 代码的时候,首先第一步就是先创建类,继承自 HttpServlet,并重写其中的某些方法。核心方法:
方法名称 | 调用时机 |
init | 在 HttpServlet 实例化之后被调用一次 |
destory | 在 HttpServlet 实例不再使用的时候调用一次 |
service | 收到 HTTP 请求的时候调用 |
doGet | 收到 GET 请求的时候调用(由 service 方法调用) |
doPost | 收到 POST 请求的时候调用(由 service 方法调用) |
doPut/doDelete/doOptions/... | 收到其他请求的时候调用(由 service 方法调用) |
实际开发过程中主要重写doGet与doPost方法,其他方法都很少使用。
2、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 对象 |
注意:请求对象是服务器收到的内容, 不应该修改。因此上面的方法也都只是 "读" 方法, 而不是 "写"方法。
3、HttpServletResponse
Servlet 中的 doXXX 方法的目的就是根据请求计算得到响应, 然后把响应的数据设置到
HttpServletResponse 对象中。然后 Tomcat 就会把这个 HttpServletResponse 对象按照 HTTP 协议的格式,转成一个字符串, 并通过Socket 写回给浏览器。
方法 | 描述 |
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 中写入二进制格式数据 |
注意: 响应对象是服务器要返回给浏览器的内容,这里的重要信息都是程序猿设置的。因此上面的方法都是 "写" 方法。注意: 对于状态码/响应头的设置要放到 getWriter / getOutputStream 之前,否则可能设置失效。
面试题:请求转发与请求重定向的区别?
区别:
(1)定义不同
- 请求转发(Forward):发生在服务器程序内部:当服务器端接收到一个客户端的请求之后,会先将请求转发给目标地址,然后再将目标地址返回的结果给客户端,而客户端对这是毫无感知的。这就好比,张三(客户端)找李四(服务器端)借钱,而李四没钱,于是李四又去王五那借钱,并把钱借给了张三,整个过程中张三只借了一次款,剩下的事情都是李四完成的,这就是请求转发。
- 请求重定向(Redirect):请求重定向指的是服务器端在接收到客户端的请求之后,会给客户端返回一个临时响应头,这个临时响应头中记录了,客户端需要再次发送请求(重定向)的URL地址,客户端接收到地址之后,会将请求发送到新的地址上,这就是请求重定向。这就好像张三(客户端)找李四(服务器端)借钱,李四没钱,于是李四就告诉张三,“我没钱,你去王五那借“,于是张三又去王五家借到了钱,这就是请求重定向。
(2)请求方不同:请求转发是服务器端的行为,服务器端代替客户端发送请求,并将结果返回给客户端;而请求重定向是客户端的行为。
(3)数据共享不同:请求转发是服务器端实现的,因此整个交互过程中使用的都是同一个 Request 请求对象和一个 Response 响应对象,请求和返回的数据是共享的;而请求重定向客户端发送两次完全不同的请求,所以两次请求中的数据是不同的。
(4)最终 URL 地址不同:请求转发过程中 URL 地址是不变的,请求重定向浏览器会重新再发送一次请求,而非刚开始请求的地址,所以 URL 地址发生了改变。
(5)实现代码不同。
演示请求重定向方法:
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("/redirectServlet") public class Demo1_RedirectServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.sendRedirect("http://www.baidu.com"); } }
运行后获取网址在浏览器打开: