Servlet 是一种实现动态页面的技术,是一组 Tomcat 提供给程序猿的 API,帮助程序猿简单高效的开发一
个 web app

动态页面 vs 静态页面:

  • 静态页面也就是内容始终固定的页面,即使 用户不同/时间不同/输入的参数不同 , 页面内容也不会发生
    变化 (除非网站的开发人员修改源代码,否则页面内容始终不变)
  • 对应的,动态页面指的就是 用户不同/时间不同/输入的参数不同页面内容会发生变化

构建动态页面的技术有很多,每种语言都有一些相关的库 / 框架来做这件事
Servlet 就是 Tomcat 这个 HTTP 服务器提供给 Java 的一组 API,来完成构建动态页面这个任务

Servlet 主要做的工作:

  • 允许程序猿注册一个类,在 Tomcat 收到某个特定的 HTTP 请求的时候,执行这个类中的一些代码
  • 帮助程序猿解析 HTTP 请求,把 HTTP 请求从一个字符串解析成一个 HttpRequest 对象
  • 帮助程序猿构造 HTTP 响应,程序猿只要给指定的 HttpResponse 对象填写一些属性字段,Servlet
  • 就会自动的安装 HTTP 协议的方式构造出一个 HTTP 响应字符串,并通过 Socket 写回给客户端

简而言之,Servlet 是一组 Tomcat 提供的 API, 让程序猿自己写的代码能很好的和 Tomcat 配合起来,从而更简单的实现一个 web app
而不必关注 Socket,HTTP协议格式,多线程并发等技术细节,降低了 web app 的开发门槛,提高了开发效率


Hello World!

1、创建一个 maven 项目

java 如何把server引用到main方法里_java


2、引入依赖

需要在代码中引入 Servlet api,这个 api 不是 JDK 内置的,而是第三方 (Tomcat 提供的)
Tomcat 相对于 Java 官方来说,仍然属于第三方

借助 maven 直接就能引入,搜索 servlet 第一个就是,

java 如何把server引用到main方法里_maven_02

使用此版本:

java 如何把server引用到main方法里_servlet_03

JDK,Tomcat,Servlet 版本要配套,如果版本不太配套,可能就存在一些问题~~
(不是完全用不了,大概率是大部分功能都正常,但是少数功能有 bug )

  • JDK 8
  • Tomcat 8.5
  • Servlet 3.1

maven 默认仓库的位置:

java 如何把server引用到main方法里_java_04

java 如何把server引用到main方法里_servlet_05


3、创建目录结构

  • 虽然当下 maven 帮我们创建了一些目录,但是还不够,还不足以支撑咱们写一个 Servlet 项目,
  • 因此需要手动的再创建一些目录和文件出来~~
    也有更简单的办法,但是此处先不介绍,先用最朴素的方式,手动搞好

——在 main 下创建一个目录 webapp 。Tomcat 可以同时加载多个 webapp 的,因此目录是 webapps 的 (每个 webapp 就相当于一个网站了),因为此处咱们写的是一个 webapp 就没有 s

——第二个目录 WEB-INF

——创建文件 web.xml

java 如何把server引用到main方法里_servlet_06

接下来,需要给 web.xml 中写点内容 (不能是空着的)

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>Archetype Created Web Application</display-name>
</web-app>

4、编写 Servlet 代码

// 如果代码中找不到 HttpServlet 这个类 说明依赖没有被正确引入
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // super.doGet(req, resp);
    }
}

do:处理
Get 对应到 HTTP 的 GET 方法
这个方法就是在 Tomcat 收到了一个 HTTP GET 请求 的时候,会被 Tomcat 调用到 (回调函数)

HttpServletRequest :代表一个 HTTP 请求 (Tomcat 已经收到了,已经解析成对象了)

HttpServletResponse :代表着 HTTP 响应 (当前这里的 resp 是一个空的响应对象,需要在代码中给这个对象设置一些属性的)

doGet 方法要做的工作,就是根据请求,计算生成响应~~

一个服务器的工作流程,就可以分成三个典型的步骤:

  1. 接收请求并解析
  2. 根据请求计算响应
  3. 构造响应数据,并返回给客户端

其中,1 3 这两步,Tomcat 已经帮我们做好了,这个就是咱们程序猿自己要实现的逻辑,也就是 doGet 要实现的内容~~

服务器想象成一个餐馆~~
服务器上运行的代码,就要处理一个一个的请求~~

当老板收到 “来份炸酱面” 这个请求之后,后厨就行动起来了,根据客户提出的要求,来把这个炸酱面给做出来~

后厨这里,有个师傅负责切菜,有个师傅负责拉面,再有个师傅负责炸酱,再来个师傅把这里的结果给进行集成~~

服务员就可以把这份面端给我了~~

这三个师傅并发的工作,相当于三个线程!!

@WebServlet("/hello")
// 如果代码中找不到 HttpServlet 这个类 说明依赖没有被正确引入
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 这个代码一定要干掉! 不能调用父类的 doGet!
        // super.doGet(req, resp);

        // 这是让服务器在自己的控制台里打印
        System.out.println("hello world!");

        // 在页面上也能打印,把 hello world! 字符串放到 http 响应的 body 中,浏览器就会把 body 的内容显示到页面上
        resp.getWriter().write("hello word!" + System.currentTimeMillis());
    }
}

resp 是响应对象
getWriter 其实是返回了一个 Writer 对象 (字符流对象)
此处 Writer 对象就不是往文件里写了,而是往 HTTP 响应的 body 中写入数据
write() 是真正的干活,写数据的方法

1、 @WebServlet("/hello")

把当前的这个 HelloServlet 这个类,和 HTTP 请求中的 URL 里面路径带有 /hello 这样的请求,给关联起来了

  • Tomcat 可能会受到很多种形形色色的请求
    /123 http://123.123.123.123:8080/123
    /aaa http://123.123.123.123:8080/aaa
    /test http://123.123.123.123:8080/test
  • 这么多请求,咱们的代码只管其中的一种情况:/hello http://123.123.123.123:8080/hello
    才会让 Tomcat 来调用 HelloServlet 这个类,来进行处理

2、 保证方法统一

  • /hello 是和 HelloServlet 这个类关联起来了
  • 但是要想执行到 doGet,还得保证方法也是 GET 方法~~
  • 如果是 POST,/hello,仍然无法执行到doGet ~~

5、打包

  • 当前的代码,是不能单独运行的 (没有 main 方法)
  • 需要把当前的代码,打包,然后部署到 Tomcat 上,由 Tomcat 来进行调用

准备工作: 修改 pom.xml~~

jarwar 都是 java 发布程序用的压缩包格式
war 算是给 Tomcat 专门搞的,这里不光会包含一些 .class,还可以包含配置文件,以及依赖的第三方的 jar,还有 html css …

双击这个 package 触发打包

java 如何把server引用到main方法里_maven_07

提示 BUILD SUCCESS 说明打包成功!!

java 如何把server引用到main方法里_maven_08


6、部署

war 包 拷贝到 tomcat 的 webapps 目录下,启动 Tomcat

java 如何把server引用到main方法里_maven_09


7、验证程序

咱们访问 Tomcat 的时候,是需要指定两级目录的

  • Context Path:之所以,咱们的第一级路径叫做 hello102,是因为咱们 war 包名字就是 hello102,war 同时解压缩之后得到的路径也就是 hello102
  • Servlet Path@WebServlet("/hello") 这个注解 ,是和第二级路径相匹配的

可以这样认为:
一个 Tomcat 上可以同时部署多个网站,一个网站上又有多个页面~~
一个请求中的第一级路径,就告诉 Tomcat,要访问的网站是哪个,第二级路径,就告诉 Tomcat 要访问的页面是这个网站中的哪个页面

访问:http://127.0.0.1:8080/hello102/hello

没有成功,此时在 tomcat 控制台这里,敲了一下回车就出来了,显示的 hello world 就是返回的 HTTP 响应的 body 部分

java 如何把server引用到main方法里_tomcat_10

这个情况是属于 cmd 的一个大坑!!!

CMD 有一个 “选择文本” 这样的模式

鼠标选中其中的一段文本,就会触发 CMD 的选中文本模式~~
当 CMD 被选中文本的时候,CMD 就会把里面运行的程序给挂起 (暂停了,不往下走了)

  • 现在这个页面内容是通过 Java 代码生成的了,但是这和你直接创建一个 HTML,里面写个 hello world 有啥区别呢??
  • 页面里的内容是固定死的,是静态的,是不变的~~
  • 这里的内容是可变的!!
  • 根据用户不同的输入,可以得到不同的结果!!!

修改代码:获取时间戳

resp.getWriter().write("hello word!" + System.currentTimeMillis());

上述这七个步骤,这是针对一个新的项目~~
项目创建好之后,后续修改代码,前三个步骤就不必重复了,直接从 4 - 7 进行操作即可~~
重新部署的时候,不一定要重启,tomcat (理论上来说,是不需要重启的,但是 Windows 系统多少有点不顺畅的地方~~ ,一般在 Linux上,这个都是能顺利自动重新加载的~~)

java 如何把server引用到main方法里_开发语言_11

如果看到形如这样的提示~~ 就已经重新部署了,deploy:部署

显示结果:hello word!1653073391039


smart tomcat

1、介绍

上面介绍的步骤,其实都是最朴素的操作了~~
因此也有一些办法,来提高上述流程的效率,就可以通过一些第三方工具来简化 5 和 6 的操作,毕竟每次修改代码,都需要重新打包,重新部署~~

咱们是通过 IDEA 上的插件,直接把 Tomcat 给集成进来了~~ 做到 “一键式” 完成打包部署了~~

注意: Tomcat 和 IDEA 是两个独立的程序!!! Tomcat 不是 IDEA 功能的一部分!!

后面开发,主要还是通过 IDEA 调用 Tomcat 的方式来进行
用的时间长了之后,同学们就对于 Tomcat 的印象,就开始模糊~~

java 如何把server引用到main方法里_servlet_12

smart tomcat 是 idea 的一个插件
idea 本身虽然已经有很多功能了,但是也不能做到,面面俱到!!
因此,idea 还提供了一系列的扩展能力,允许第三方来开发一些程序,交给 idea 运行,相当于对 idea 进行了扩展~~

  • smart tomcat 并不是真的打包了~~
  • 其实相当于把 webapp 目录作为 tomcat 启动的一个参数,给设定进去了,让 tomcat 直接从这个指定目录中加载 webapp
  • Tomcat 默认是从 webapps 中加载 war 包,也不是只有这一条路~~
  • 因此当你去看 Tomcat 的 webapps 目录,就会发现其实没有你的 war~~(不像手动拷贝)

2、安装

java 如何把server引用到main方法里_开发语言_13


3、使用 smart tomcat

  • 在一个项目中,首次使用,需要先简单配置,后续就不必再配了~~
  • 当使用 Smart Tomcat 之后,Context Path 就不是 war 包名字了,而是在 Smart Tomcat 配置页面中指定的名字

java 如何把server引用到main方法里_java_14

点击这里的三角号,就会自动完成打包部署重启 tomcat 这一系列操作

java 如何把server引用到main方法里_开发语言_15

报错信息:Caused by: java.net.BindException: Address already in use: bind

如果一个端口,已经被服务器绑定了,再次启动一个程序绑定同一个端口,就会出现这个错误!!!

当下存在这个问题,是因为已经在命令行里启动了一个 Tomcat 了
如果在 idea 中再启动一个,显然端口是不能重复占用的

此时就没有问题,这些文字,在 cmd 中乱码,但是在 idea 的终端中就不再乱码了

org.apache.catalina.util.SessionIdGeneratorBase.createSecureRandom 使用[SHA1PRNG]创建会话ID生成的SecureRandom实例花费了[103]毫秒。
21-May-2022 03:36:58.905 信息 [main] org.apache.coyote.AbstractProtocol.start 开始协议处理句柄["http-nio-8080"]
21-May-2022 03:36:58.915 信息 [main] org.apache.catalina.startup.Catalina.start Server startup in 370 ms
http://localhost:8080/hello102

常见出错问题汇总

1、404

你要访问的资源在服务器上不存在

错误 1: 你请求的资源路径写的不对

错误 2: 路径虽然对,但是服务器没有正确把资源加载起来~~

少些了第一级路径 Context Path:

java 如何把server引用到main方法里_开发语言_16

少写了第二级路径 Servlet Path:

java 如何把server引用到main方法里_tomcat_17

错误 3: Servlet Path 写的和 URL 不匹配

请求的路径和服务器这边的配置不匹配:

java 如何把server引用到main方法里_开发语言_18

错误 4: web.xml 写错了

当 web.xml 有问题的时候,tomcat 是不能正确加载到 webapp的~~
tomcat 发现这个目录中存在了 web.xml 且内容正确,tomcat 才能加载这个 webapp


2、405 —— Method Not Allowed

指 HTTP 中的方法

错误 1: 将 doGet 改成 doPost:

java 如何把server引用到main方法里_tomcat_19

这样的用户操作,浏览器是构造了一个 GET 请求,而服务器这里写的是 doPost,不能处理 GET 请求~~

什么时候浏览器发的是 GET 请求?

  1. 直接在地址栏里,输入 URL
  2. 通过 a 标签跳转
  3. 通过 img/link/script…
  4. 通过 form 表单,method 指定为 GET
  5. 通过 ajax,type 指定为 GET

什么时候浏览器发的是POST请求?

  1. 通过form表单, method指定为POST
  2. 通过ajax, type指定为POST

错误 2: 如果是代码中忘记注释掉 super.doGet(req, resp),这样的情况也会出现 405 !!!

进入到 HttpServlet 源码中,就能看到,此处父类的 doGet,干的事就是直接返回一个 405 的响应!!!

java 如何把server引用到main方法里_servlet_20


3、500

500 也是一个非常高频的错误,5 开头,是服务器出了问题
一般 500 就意味着服务器代码里抛出异常了,并且咱们的代码没处理,异常抛到 Tomcat 这里了~~

比如:如果代码中出现异常,可以通过 catch 来捕获到
如果 catch 没捕获到(类型不匹配,压根没 catch),异常就会沿着调用栈,向上传递

如果代码出现 500,就偷着乐吧,这种情况是最好解决的了!! 因为它直接告诉你,哪里有问题了!!!

java 如何把server引用到main方法里_开发语言_21

异常的调用栈 (平时写代码的时候,IDEA 里出现的异常调用栈),告诉了我们出现异常的准确位置


4、空白页面

如果不给响应对象中设置任何内容,就会出现 空白页面


5、无法访问此网站

  • 如果是这个情况,证明,网络就不通 (TCP 的层次上就是不通)
  • 其中一个很大的可能性,就是 Tomcat 没有正确启动 (比如 Tomcat 端口被占用、启动失败……)

java 如何把server引用到main方法里_java_22


Servlet 运行原理

Servlet 是属于上层建筑,下面的传输层,网络层,数据链路层… 属于经济基础

java 如何把server引用到main方法里_maven_23

  • 当浏览器给服务器发送请求的时候,Tomcat 作为 HTTP 服务器,就可以接收到这个请求

HTTP 协议作为一个应用层协议,需要底层协议栈来支持工作,如下图所示:

Tomcat 其实是一个应用程序,运行在用户态的普通进程 (Tomcat 其实也是一个 Java进程)
用户写的代码 (根据请求计算相应),通过 Servlet 和 Tomcat 进行交互~~
Tomcat 进一步的和浏览器之间的网络传输,仍然是走的网络原理中的那一套:封装和分用

java 如何把server引用到main方法里_开发语言_24

更详细的交互过程可以参考下图:

java 如何把server引用到main方法里_maven_25

  1. 接收请求:
  • 用户在浏览器输入一个 URL, 此时浏览器就会构造一个 HTTP 请求.
  • 这个 HTTP 请求会经过网络协议栈逐层进行 封装 成二进制的 bit 流, 最终通过物理层的硬件设备转换成光信号/电信号传输出去.
  • 这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达目标主机(这个过程也需要网络层和数据链路层参与).
  • 服务器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成HTTP 请求. 并交给 Tomcat 进程进行处理(根据端口号确定进程)
  • Tomcat 通过 Socket 读取到这个请求(一个字符串), 并按照 HTTP 请求的格式来解析这个请求, 根据
    请求中的 Context Path 确定一个 webapp, 再通过 Servlet Path 确定一个具体的 类. 再根据当前请
    求的方法 (GET/POST/…), 决定调用这个类的 doGet 或者 doPost 等方法. 此时我们的代码中的
    doGet / doPost 方法的第一个参数 HttpServletRequest 就包含了这个 HTTP 请求的详细信息
  1. 根据请求计算响应:
  • 在我们的 doGet / doPost 方法中, 就执行到了我们自己的代码. 我们自己的代码会根据请求中的一
    些信息, 来给 HttpServletResponse 对象设置一些属性. 例如状态码, header, body 等.
  1. 返回响应:
  • 我们的 doGet / doPost 执行完毕后, Tomcat 就会自动把 HttpServletResponse 这个我们刚设置好的对象转换成一个符合 HTTP 协议的字符串, 通过 Socket 把这个响应发送出去.
  • 此时响应数据在服务器的主机上通过网络协议栈层层 封装, 最终又得到一个二进制的 bit 流, 通过物理层硬件设备转换成光信号/电信号传输出去.
  • 这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达浏览器所在的主机(这个过程也需要网络层和数据链路层参与).
  • 浏览器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成HTTP 响应, 并交给浏览器处理.
  • 浏览器也通过 Socket 读到这个响应(一个字符串), 按照 HTTP 响应的格式来解析这个响应. 并且把body 中的数据按照一定的格式显示在浏览器的界面上.

浏览器和服务器之间交互数据,这个过程中是否会涉及到 TCP 三次握手,确认应答…,是否会涉及到IP的分包组包… 是否会涉及到以太网的MTU 呢…

都是会的!!!


Tomcat 的伪代码

  • 下面的代码通过 “伪代码” 的形式描述了 Tomcat 初始化 / 处理请求 两部分核心逻辑
  • 所谓 “伪代码”,并不是一些语法严谨,功能完备的代码,只是通过这种形式来大概表达某种逻辑

1、Tomcat 初始化流程

  • Tomcat 的代码中内置了 main 方法, 当我们启动 Tomcat 的时候, 就是从 Tomcat 的 main 方法开始执行的,
  • 被 @WebServlet 注解修饰的类会在 Tomcat 启动的时候就被获取到,并集中管理,

1). 让 Tomcat 先从指定的目录中找到所有要加载的 Servlet 类~~

前面部署的时候,是把 Servlet 代码编译成了 .class,然后打了 war 包,然后拷贝到了 webapps 里面。Tomcat 就会从 webapps 里来找到哪些 .class 对应的 Servlet 类,并且需要进行加载 (当然啦,这里的加载不一定是 Tomcat 一启动就立即执行,也可能是 “懒加载”
但是此处伪代码中就假设立即加载了~~)

java 如何把server引用到main方法里_tomcat_26

2). 根据刚才类加载的结果,给这些类创建 Servlet 实例~~

// 这里要做的的是实例化出所有的 Servlet 对象出来;
for (Class<Servlet> cls : allServletClasses) {
    // 这里是利用 java 中的反射特性做的
    // 实际上还得涉及一个类的加载问题,因为我们的类字节码文件,是按照约定的
    // 方式(全部在 WEB-INF/classes 文件夹下)存放的,所以 tomcat 内部是
    // 实现了一个自定义的类加载器(ClassLoader)用来负责这部分工作。
    Servlet ins = cls.newInstance(); // 反射
    instanceList.add(ins);
}

3). 实例创建好之后,就可以调用当前 Servlet 实例的 init 方法了

Servlet 自带的方法,默认情况下 init 啥都不干
咱们在继承一个 HttpServlet 的时候,也可以自己重写 init,就可以在这个阶段,帮我们做一些初始化工作了

// 调用每个 Servlet 对象的 init() 方法,这个方法在对象的生命中只会被调用这一次;
for (Servlet ins : instanceList) {
	ins.init();
}

4). 创建 TCP socket,监听 8080 端口,等待有客户端来连接

Tomcat 为了能同时相应多个 HTTP 请求, 采取了多线程的方式实现, 因此 Servlet 是运行在 多线程环境 下的, Tomcat 内部也是通过 Socket API 进行网络通信,

每次有连接过来了,accept 就会返回,然后就给当前的线程池里,加个任务
doHttpRequest(socket); 这个任务里,就要负责处理这个请求了

此处这个主循环,和咱们之前讲过的 TCP 回显服务器,本质上没啥区别
Tomcat 工作的大部分时间都是在这个循环里完成的~~

// 利用我们之前学过的知识,启动一个 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);
    });
}

5). 如果循环退出了,Tomcat 也要结束了,就会依次循环调用每个 Servlet 的 destroy 方法

这个是属于收尾工作 (Tomcat 退出之前的事情)

// 调用每个 Servlet 对象的 destroy() 方法,这个方法在对象的生命中只会被调用这一次;
for (Servlet ins : instanceList) {
    ins.destroy();
}

没有 break 为啥还能走到下面的代码?
那是因为咱们当下写的是一个伪代码,只包含了核心逻辑,没有太多的实现细节~~
实际上 Tomcat 里面会有一些条件来退出这个主循环
和 init 类似,这里的 destroy 默认也是啥都不干的,可以在用户代码中重写这个 destroy ~~

虽然这里有 5) 这个调用 destroy 这个环节,但是这个环节不一定可靠~~
如果 Tomcat 是通过 “正常流程" 退出的,才能主动结束循环,调用这里的 destroy。如果是通过 “非正常流程" 退出的,此时就来不及调用 destroy

比如,Tomcat,8005 端口 (管理端口,通过这个端口可以对这个 Tomcat 发号施令),通过这个方式来关闭 Tomcat,就属于 “正常流程"

非正常流程:直接结束进程 (大部分是这种情况)

class Tomcat {
    // 用来存储所有的 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);
        }

        // 调用每个 Servlet 对象的 init() 方法,这个方法在对象的生命中只会被调用这一次;
        for (Servlet ins : instanceList) {
            ins.init();
        }

        // 利用我们之前学过的知识,启动一个 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);
            });
        }

        // 调用每个 Servlet 对象的 destroy() 方法,这个方法在对象的生命中只会被调用这一次;
        for (Servlet ins : instanceList) {
            ins.destroy();
        }
    }
        
    public static void main(String[] args) {
        new Tomcat().start();
    }
}

2、Tomcat 处理请求流程

class Tomcat {
    void doHttpRequest(Socket socket) {
        // 参照我们之前学习的 HTTP 服务器类似的原理,进行 HTTP 协议的请求解析,和响应构建
        HttpServletRequest req = HttpServletRequest.parse(socket); // 1) 
        HttpServletRequest resp = HttpServletRequest.build(socket);
        
        // 判断 URL 对应的文件是否可以直接在我们的根路径上找到对应的文件,如果找到,就是静态内容
        // 直接使用我们学习过的 IO 进行内容输出
        if (file.exists()) { // 2)
            // 返回静态内容
            return;
        }
        
        // 走到这里的逻辑都是动态内容了
        
        // 根据我们在配置中说的,按照 URL -> servlet-name -> Servlet 对象的链条
        // 最终找到要处理本次请求的 Servlet 对象
        Servlet ins = findInstance(req.getURL()); // 3)
        
        // 调用 Servlet 对象的 service 方法
        // 这里就会最终调用到我们自己写的 HttpServlet 的子类里的方法了
        try {
            ins.service(req, resp); // 4)
        } catch (Exception e) {
            // 返回 500 页面,表示服务器内部错误
        }
    }
}

1). req :是通过读取 socket 中的数据,然后再按照 HTTP 协议的请求格式来解析的,构造成了一个 HttpServletRequest 对象
resp :这里则是相当于 new 了一个空的对象

2). 是判定当前要请求的资源是否是静态文件~~
如果是静态文件,就读取文件内容,把文件内容构造到 resp 对象的 body 中,并且返回这个 resp 对象

例如可以再 webapp 目录中,创建一个静态文件 test.html,启动,访问:http://127.0.0.1:8080/hello102/test.html

3). 根据请求的 URL,来获取到使用哪个类来处理
URL 里要有两级路径:

  • 第一级路径:Context Path,确定一个 webapp
  • 第二级路径:Servlet Path,确定一个 Servlet 类,如果没有找到匹配的 Servlet 类,就会返回 404

4). 根据刚才找到的 Servlet 对象,来调用 service 方法~~ 在 service 方法内部,又会进一步的调用 doGet / doPost


3、Servlet 的 service 方法的实现

如果在执行 Servlet 的 service 方法过程中出现未处理的异常就会返回500

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 的关键方法,主要有三个:

  • init:初始化阶段,对象创建好了之后,就会执行到,用户可以重写这个方法,来执行一些初始化逻辑
  • service:在处理请求阶段来调用,每次来个请求都要调用一次 service
  • destroy:退出主循环,tomcat 结束之前会调用,用来释放资源

他们的调用时机就称 Servlet 的生命周期,生命周期这个词是计算机里一个挺常见的术语 “啥时候,该做啥事”

Servlet 的 service 方法内部会根据当前请求的方法,决定调用其中的某个 doXXX 方法
在调用 doXXX 方法的时候,就会触发 多态 机制,从而执行到我们自己写的子类中的 doXXX 方法

理解此处的 多态

  • 我们前面自己写的 HelloServlet 类, 继承自 HttpServlet 类. 而 HttpServlet 又继承自 Servlet. 相当于 HelloServlet 就是 Servlet 的子类.
  • 接下来, 在 Tomcat 启动阶段, Tomcat 已经根据注解的描述, 创建了 HelloServlet 的实例, 然后把这个实例放到了 Servlet 数组中.
  • 后面我们根据请求的 URL 从数组中获取到了该 HelloServlet 实例, 但是我们是通过 Servlet ins这样的父类引用来获取到 HelloServlet 实例的.
  • 最后, 我们通过 ins.doGet() 这样的代码调用 doGet 的时候, 正是 “父类引用指向子类对象”, 此时就会触发多态机制, 从而调用到我们之前在 HelloServlet 中所实现的 doGet 方法
  • 等价代码:

Servlet ins = new HelloServlet(); ins.doGet(req, resp);