由之前所学,Servlet类的实例化以及方法的调用是完全不需要我们自己操作的,我们只需要按照标准开发好Servlet类,然后放到Tomcat里跑就行了。那也就是说Servlet对象的创建到销毁整个过程都是由Tomcat自动来给我们管理的。
一、概念
什么是Servlet的生命周期?
- 应用程序中的对象不仅在空间上有层次结构的关系,在时间上也会因为处于程序运行过程中的不同阶段而表现出不同状态和不同行为——这就是对象的生命周期。
- 简单的叙述生命周期,就是对象在容器中从开始创建到销毁的过程。
Servlet容器
- Servlet对象是Servlet容器创建的,生命周期方法都是由容器(目前我们使用的是Tomcat)调用的。这一点和我们之前所编写的代码有很大不同。在今后的学习中我们会看到,越来越多的对象交给容器或框架来创建,越来越多的方法由容器或框架来调用,开发人员要尽可能多的将精力放在业务逻辑的实现上。
Servlet主要的生命周期执行特点
生命周期 | 对应方法 | 执行时机 | 执行次数 |
---|---|---|---|
实例化 | 构造器 | 第一次请求或者容器启动 | 1 |
初始化 | init() | 构造完毕后 | 1 |
处理服务 | service(HttpServletRequest req,HttpServletResponse resp) | 每次请求 | 多次 |
销毁 | destory() | 容器关闭 | 1 |
二、Servlet在Tomcat中是单例的
由此看见,Servlet在Tomcat中是单例的,如果是单例的,就会出现问题,因为我们的程序是允许有很多客户端同时向同一个Tomcat的同一个URL相同(即同一个servlet),此时Tomcat会发生什么事情呢?
假设线程A和线程B来请求,每一个线程有自己独立的栈,Servlet对象是在堆中存放的,service方法是在每个线程栈进行压栈执行的,因此两个线程都会执行service方法,但是如果这两个线程想要操作一些成员变量,它们都会往堆内存(即同一个Servlet实例对象)中找,此时就会引发线程安全问题。
例如下图,线程1在读取 num=1
的时候,进入了service,刚刚准备执行if这个判断,但这个瞬间,线程2执行不同路径的代码,将num的值修改为了5,这个if就不成立了,就把线程1执行的路径修改掉了。
因为Servlet是单例的,所以所有的线程都是单例的,就会出现线程不安全。
我们已经知道了servlet是线程不安全的,给我们的启发是:尽量的不要在servlet中定义成员变量。如果不得不定义成员变量,那么:
① 不要去修改成员变量的值
② 不要根据成员变量的值做一些逻辑判断。
二、代码示例
package com.atguigu.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/servletLiftCycle")
public class ServletLifeCycle extends HttpServlet {
public ServletLifeCycle(){
System.out.println("构造器");
}
// PS:重写的是没有参数的init方法
@Override
public void init() throws ServletException {
System.out.println("初始化方法");
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("service方法");
}
@Override
public void destroy() {
System.out.println("销毁方法");
}
}
三、构造方法不能是私有的
Demo02Servlet.java
// 注意这里改成了private
private Demo02Servlet () {
System.out.println("正在实例化...");
}
四、修改Servlet的初始化时机
默认情况下,第一次请求时,tomcat才会去实例化,初始化,然后再服务。这样的好处是提高系统的启动速度。缺点是:第一次请求时,耗时较长。
结论:如果需要提高系统的启动速度,当前默认情况就是这样。如果需要提高响应速度,我们应该设置Servlet的初始化时机。
认是第一次请求时实例化,初始化。我们可以通过 <load-on-startup>
(startup:启动,load:加载)
可以配置指定Servlet启动的先后顺序,在 <servlet>
标签下配置
<load-on-startup>
的值为0或正整数(一般是0-10之间,最小值是0),数字越小,启动的时间越靠前
<servlet>
<servlet-name>Demo02Servlet</servlet-name>
<servlet-class>com.atguigu.servlets.Demo02Servlet</servlet-class>
<!--
默认值是-1,含义是:Tomcat启动时不会实例化该Servlet
其他正整数 15,含义是:Tomcat在启动时,实例化该Servlet的顺序,如果是15,那就是第15个被实例化的。此时我们就可以控制多个Servlet之间实例化的顺序。
如果序号冲突了,Tomcat会自动协调顺序,而且序号不一定要连续
PS:Tomcat容器中,已经定义了一些随系统启动实例化的servlet,我们自定义的servlet的load-on-startup尽量不要占用数字1-5
-->
<load-on-startup>6</load-on-startup>
</servlet>
使用注解的方式修改:
@WebServlet(value = "/servletLiftCycle", loadOnStartup = -1)
Tomcat里的 web.xml
是所有项目公共的 web.xml
。正常情况下,每一个项目都有自己独立的 web.xml
,但是这两个文件中有一些配置是一样的,如果我们不处理这些一样的配置的话,web.xml
中就会有很多很多代码,我们一旦创建后,这个 web.xml
文件就会变得特别大。
此时Tomcat就会这样做:将我们自己项目的 web.xml
抽取出来,放到conf中,然后形成 web.xml
。意思就是说,如果我们自己项目中没写的配置,就会按照conf中的 web.xml
配置来运行,但如果在我们自己当前项目里又写了一遍,那就会使用自己当前项目里 web.xml
中的配置。
我们可以来看看conf中的 web.xml
有什么配置,ctrl + F 搜索 load-on
可以发现数字 1 ~ 5
几乎都被占用了,虽然说序号重复Tomcat会自动协调,但是个人推荐我们自己写序号的时候,尽量不要跟Tomcat里已经配置好的这些序号冲突,防止因为冲突导致某些功能不好使。因此我们在配置的时候建议从6开始写会好一些。
四、default-servlet
我们刚才在 conf/web.xml
里发现有一个 <load-on-startup>
,这好像是Tomcat默认给我们提供的一个Servlet,并且要求这个Servlet在项目启动时立刻对这个Servlet初始化。
往下找,可以发现 default
对应的 url-pattern
是 /
,即除JSP以外的所有资源都要走它。
default-servlet
跟后面要学的SpringMVC框架有关,我们根据下图来解释一下。
当我们请求某个Servlet时,如请求servlet1,那么Tomcat就会找到servlet1来执行;如果你请求的是servlet2,那么Tomcat就会找到servlet2来执行。但是,当我们在请求静态资源的时候,假设我们请求的是 aaa.html、a.png、a.css、a.js
,此时Tomcat会先拿这个路径跟我们所有的servlet对比一遍,看看这个路径跟不跟某一个servlet匹配上,如果没匹配上,此时 default-servlet
就上场了,此时Tomcat会交给 default-servlet
进行处理,此时 default-servlet
就会根据你请求的资源路径去找对应的静态资源文件,找到后通过IO流读取这个文件进入我们的程序,并且通过IO流的形式把这个文件放到response身上,同时再根据这个文件的拓展名选择合适的MIME类型,设置response的 Content-Type
响应头,最后再由Tomcat将response对象转为一个报文,返回给客户端。
在之后我们会学习SpringMVC框架,这个框架会自己提供一个Servlet,他自己提供的Servlet会造成Tomcat所提供的 default-servlet
丢失,但如果你依旧想请求静态资源,就会报404。因此如果你的项目不是前后端分离,想要请求项目里面的静态资源,就需要重新配置 default-servlet
,让它再次生效,你的项目才能够正常访问一些静态资源。
其他的Tomcat还默认提供了JspServlet,但是JSP现在已经被淘汰了,我们就不提了。
三、总结
- 通过生命周期测试我们发现Servlet对象在容器中是单例的
- 容器是可以处理并发的用户请求的,每个请求在容器中都会开启一个线程
- 多个线程可能会使用相同的Servlet对象,所以在Servlet中,我们不要轻易定义一些容易经常发生修改的成员变量
- load-on-startup中定义的正整数表示实例化顺序,如果数字重复了,容器会自行解决实例化顺序问题,但是应该避免重复
- Tomcat容器中,已经定义了一些随系统启动实例化的servlet,我们自定义的servlet的load-on-startup尽量不要占用数字1-5