板块一 Servlet编程:第二节 Servlet的实现与生命周期
在上一节的内容中,我们已经系统的学习了HTTP的相关概念、知道了GET和POST请求在服务器上的运行原理、请求响应在服务器中究竟是怎样运行的,从这一节开始我们将系统的学习实现Servlet的完整过程
一、Servlet相关概念
Serlvet的本质
- 当编写Java程序想要在网上实现聊天、发帖、这样一些的交互功能,普通Java技术是非常复杂的,试想要从底层搭建出一整个服务层的代码有多复杂?并且每个人搭建的底层还不一样,为了解决这个问题,sun公司就提供了Serlvet这种技术供我们使用。Servlet是Server与Applet的缩写,是服务端小程序的意思,本质上就是一个遵循Servlet开发的Java类,由服务器调用,在服务器端运行,它的创建、使用、销毁都由Servlet容器进行管理,其常见容器有很多,如Tomcat,Jetty,WebLogic Server,WebSphere,JBoss等等(在Tomcat的集成这一节中我们已经详尽的学习了Tomcat),更奇怪的是Servlet没有main()方法,本质上我们可以想象Servlet通过某种注入和回调方法与Servlet容器取得联系,从而代替我们在Servlet中书写main()函数
- Servlet与HTTP紧密联系,可以处理与HTTP协议相关的所有内容,这是Servlet应用广泛的原因之一
- 实际上在运行JSP时,服务器底层将JSP编译成一个Java类,这个类就是Servlet,因此可以说JSP就是Servlet(详见JSP追根溯源小节)
- Servlet在 JAVA WEB项目中的位置,它就是我们常说的后端
- 编程学习越往后越是如此,我们能做的其实很有限。大部分工作框架都已经帮我们做了。只
要我们实现xx接口,它会帮我们创建实例,然后搬运(接口注入)到它合适的位置,然后一套既定
的流程下来执行到创建我们需要的实例
二、中Web项目中实现Servlet规范
我们已经在板块零的第二节中创建了Java Web项目;又在板块零的第三节中成功把Tomcat集成到IDEA中,现在我们就在这个Web项目中实现Servlet规范
(1)在普通的Java类中继承HttpServlet类
在www.caijiyuan包中创建一个普通的Java类
helloServlet.java
此类继承HttpServlet类,还记得继承怎么写吗:extends
(2)重写service方法
在上一节中我们详细学习了请求响应在服务器中究竟是怎样运行的,而在Servlet中请求响应就是通过HttpServlet类中的service方法实现的,service方法有两个形参(Request,Rrsponse)
分别对应请求、响应
在HttpServlet类中,IDEA重写方法的快捷键是Ctrl+O
但我们发现有两个service,它们的区别是什么?
先来看第一个,HttpServlet的service()方法
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取http request的method参数,其实就是html的form标签
//中method属性对应的字符串
String method = req.getMethod();
long errMsg;
//判断请求方式
if(method.equals("GET")) {
//获取最后被修改时间
errMsg = this.getLastModified(req);
if(errMsg == -1L) {
/**如果servlet不支持http request header的if-modified-since属性
* 则继续处理
**/
this.doGet(req, resp);
} else {
//如果支持这个属性
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader("If-Modified-Since");
} catch (IllegalArgumentException var9) {
ifModifiedSince = -1L;
}
/**
* 如果客户端的文件最后修改时间和服务器端的文件最后修改时间一致则返回304不需要修改状态
* 这样服务器就不返回html,浏览器读取本地缓存文件,否则重新获取服务器端的对应html文件
**/
if(ifModifiedSince < errMsg / 1000L * 1000L) {
this.maybeSetLastModified(resp, errMsg);
this.doGet(req, resp);
} else {
resp.setStatus(304);
}
}
} else if(method.equals("HEAD")) {
errMsg = this.getLastModified(req);
this.maybeSetLastModified(resp, errMsg);
this.doHead(req, resp);
} else if(method.equals("POST")) {
this.doPost(req, resp);
} else if(method.equals("PUT")) {
this.doPut(req, resp);
} else if(method.equals("DELETE")) {
this.doDelete(req, resp);
} else if(method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if(method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
//如果请求不是以上的所有请求方式,该方法就会响应501错误,也就是不支持这种请求
String errMsg1 = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[]{method};
errMsg1 = MessageFormat.format(errMsg1, errArgs);
resp.sendError(501, errMsg1);
}
}
这是第二个ServletRequest
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest)req;
response = (HttpServletResponse)res;
} catch (ClassCastException var6) {
throw new ServletException("non-HTTP request or response");
}
this.service(request, response);
}
原来只有第二个 ServletRequest service
方法是由Tomcat自动调用,它将接收的客户端请求转交给HttpServlet
中的第一个HttpServletRequest protected service
方法,此保护类型的service方法再把将请求分发给doPost()
、doGet()
方法进行下一步处理
因此这里就Request/Response
俩个形参而言,重写调用第一个或第二个service方法的效果应该是一样的,此处我们直接重写第一个,也就是 HttpServletRequest protected service
编辑项目对外访问路径
我们还应将项目对外访问路径(就是在服务器中此项目的站点名)更改为自己想要的样子
此处我更改为与包名一致
/www.caijiyuan
注意:我们还应在类前设置注解@WebServlet("/helloServlet")
,告诉服务器当前资源在站点下的真实路径
helloServlet.java中写入测试内容
package www.caijiyuan;
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("/helloServlet")
public class helloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// super.service(req, resp);
//打印内容在控制台
System.out.println("Hello Servlet with terminal");
//通过流输出数据到浏览器
resp.getWriter().write( "Hello Servlet with brower");
}
}
启动服务器在浏览器中访问得
在控制台得
二、Servlet工作流程
那么HttpServletRequest request
请求是在服务器中是怎样接受到的呢?
即是上一节中详解过的请求头的功劳
当请求进入服务器时
- 服务器会通过请求头键值对中的
Host
中的localhost
找到主机是本机 - 通过
8080
端口确定本机中占用端口的程序是Tomcat - 通过请求行中的
"/www.caijiyuan"
确定是Tomcat下的哪个站点 - 通过
"/helloServlet"
确定是当前站点下的那个资源路径
找到资源路径后,如果服务器是第一次被访问就会创建一个Servlet,否则就会调用service方法生成request对象来处理会话中的请求;接受到请求后经过设定的代码生成response对象保存响应内容返回给客户端(浏览器),这就是Servlet工作的流程
三、其它实现Servlet规范的方式
(1)继承自GenericServlet 类
对于一个 Servlet 类,我们日常最常用的方法是继承自 HttpServlet 类,实际上,HttpServlet是扩展了GenericServlet 类。GenericServlet 类实现了Servlet,ServletConfig和Serializable接口。它主要完成了这些任务:
- 将 init() 中的 ServletConfig 赋给一个类级变量,可以由 getServletConfig 获得;
- 为 Servlet 所有方法提供默认实现的接口(除了service方法);
- 可以直接调用 ServletConfig 中的方法;
它的基本结构如下:
abstract class GenericServlet implements Servlet,ServletConfig{
//GenericServlet通过将ServletConfig赋给类级变量
private trServletConfig servletConfig;
public void init(ServletConfig servletConfig) throws ServletException {
this.servletConfig=servletConfig;
/*自定义init()的原因是:如果子类要初始化必须覆盖父类的init() 而使它无效 这样
this.servletConfig=servletConfig不起作用 这样就会导致空指针异常 这样如果子类要初始化,
可以直接覆盖不带参数的init()方法 */
this.init();
}
//自定义的init()方法,可以由子类覆盖
//init()不是生命周期方法
public void init(){
}
//实现service()空方法,并且声明为抽象方法,强制子类必须实现service()方法
public abstract void service(ServletRequest request,ServletResponse response)
throws ServletException,java.io.IOException{
}
//实现空的destroy方法
public void destroy(){ }
}
可以看到如果继承这个类的话,我们必须重写 service() 方法来对处理请求
继承自GenericServlet 类实现Servlet
package www.caijiyuan;
import javax.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
@WebServlet("/Servlet_Gener")
public class Servlet_Gener extends GenericServlet {
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
//打印内容在控制台
System.out.println("Hello Servlet with terminal");
//通过流输出数据到浏览器
servletResponse.getWriter().write( "Hello Servlet with brower");
}
}
启动服务器后在浏览器中访问得
(2)实现Servlet接口
除了两个继承抽象类实现Servlet的接口,还有一个通过实现interface Servlet
来实现Servlet规范的方法
实例
创建类Servlet_imp.java ,使其实现Servlet接口
IDEA中使用快捷键Alt+Shift+Enter
实现方法,在service()方法中添加测试代码,并且在类前添上@WebServlet()
注释
package www.caijiyuan;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
@WebServlet("/Servlet_imp")
public class Servlet_imp implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
//通过流输出数据到浏览器
servletResponse.getWriter().write( "Hello Servlet with brower");
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
访问浏览器同样可以得到
(3)在HttpServlet中直接重写doGet()和doPost()方法
我们在调用HttpServlet中service方法时,底层实际上是判断GET请求还是POST请求从而分别调用doGet()和doPost()方法
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
long lastModified;
if (method.equals("GET")) {
lastModified = this.getLastModified(req);
if (lastModified == -1L) {
this.doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader("If-Modified-Since");
} catch (IllegalArgumentException var9) {
ifModifiedSince = -1L;
}
if (ifModifiedSince < lastModified / 1000L * 1000L) {
this.maybeSetLastModified(resp, lastModified);
this.doGet(req, resp);
} else {
resp.setStatus(304);
}
}
} else if (method.equals("HEAD")) {
lastModified = this.getLastModified(req);
this.maybeSetLastModified(resp, lastModified);
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[]{method};
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
实例
创建类Servlet_do.java使其继承自HttpServlet,然后重写doGet()和doPost()方法
通过上一节我们已经知道了浏览器访问地址是使用GET方法,所以我们在doGet()中添加测试代码,并且在类前添上@WebServlet()
注释
package www.caijiyuan;
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("/Servlet_do")
public class Servlet_do extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// super.doGet(req, resp);
//通过流输出数据到浏览器
resp.getWriter().write( "Hello Servlet with brower");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// super.doPost(req, resp);
}
}
访问浏览器同样可以得到
以上三种方式都可以实现Servlet规范,使得普通Java类升级成Servlet类,但最简便的方式还是第一种,也就是继承自HttpServlet类
四、Servlet生命周期
有了以上的知识储备,我们就可以梳理一下Servlet的整个生命周期了:由于Servlet没有main()方法,不能独立运行,它的运行完全由Servlet引擎来控制和调度,所谓生命周期,指的就是Servlet容器何时创建 Servlet实例、何时调用其方法进行请求的处理、何时并销毁其实例的整个过程。
- 实例和初始化时机:当请求到达容器时,容器查找该 Servlet对象是否存在,如果不存在,则会创建实例并进行初始化,如果存在则会直接调用service()方法
- 就绪/调用/服务阶段:当有请求到达容器,容器调用Servlet对象的 service()方法,此方法在整个生命周期中可以被多次调用;HttpServlet的 service()方法则会依据请求方式来调用doGet()或者doPost()方法。但是,这两个do方法默认情况下,会抛出异常,需要子类去override
- 销毁时机:当容器关闭时(应用程序停止时),会将程序中的Servlet实例进行销毁。上述的生命周期可以通过Servlet 中的生命周期方法来观察。在Servlet 中有三个生命周期方法,不由用户手动调用,而是在特定的时机由容器自动调用
观察这三个生命周期方法即可观察到Servlet的生命周期
init
方法,在Servlet 实例创建之后执行(证明该Servlet有实例创建了)
public void init() throws ServletException {
System.out.println("创建实例调用");
}
service
方法,每次有请求到达某个Servlet 方法时执行,用来处理请求(证明该Servlet 进行服务了)
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("service方法调用了");
}
destroy
方法,Servlet实例销毁时执行(证明该Servlet的实例被销毁了)
public void destroy() {
System.out.println("实例被销毁了");
}
实例
启动服务器在浏览器中访问项目得
在控制台输出,即是在浏览器访问项目时调用了init()
方法(只会被调用一次init()
),并且紧接着接受请求调用了service
方法
接着关闭Tomcat服务器,在控制台输出
Servlet的生命周期,可以更详细的分为四步:Servlet类加载 – >实例化 – >服务 – >销毁
下面我们描述一下Tomcat与Servlet是如何工作的,看看下面的时序图:
- Web Client 向Servlet容器(Tomcat)发出Http请求
- Servlet 容器接收Web Client的请求
- Servlet 容器创建一个HttpServletRequest对象,将Web Client请求的信息封装到这个对象中
- Servlet 容器创建一个HttpServletResponse 对象
- Servlet 容器调HttpServlet对象service 方法,把Request与Response作为参数,传给HttpServlet
- HttpServlet 调用HttpServletRequest对象的有关方法,获取Http请求信息
- HttpServlet 调用HttpServletResponse对象的有关方法,生成响应数据
- Servlet 容器把HttpServlet的响应结果传给Web Client
以上就是此小节的所有内容,我们系统的学习了Servlet的概念、实现Servlet规范的多个方法、Servlet的工作流程、生命周期等,为之后Servlet具体对象学习打下了坚实的基础。从下一节开始我们将学习service方法两个形参之一:HttpServletRequest实例的具体操作