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 项目
2、引入依赖
需要在代码中引入 Servlet api,这个 api 不是 JDK 内置的,而是第三方 (Tomcat 提供的) Tomcat 相对于 Java 官方来说,仍然属于第三方
借助 maven 直接就能引入,搜索 servlet 第一个就是,
使用此版本:
JDK,Tomcat,Servlet 版本要配套,如果版本不太配套,可能就存在一些问题~~ (不是完全用不了,大概率是大部分功能都正常,但是少数功能有 bug )
- JDK 8
- Tomcat 8.5
- Servlet 3.1
maven 默认仓库的位置:
3、创建目录结构
- 虽然当下 maven 帮我们创建了一些目录,但是还不够,还不足以支撑咱们写一个 Servlet 项目,
- 因此需要手动的再创建一些目录和文件出来~~
也有更简单的办法,但是此处先不介绍,先用最朴素的方式,手动搞好
——在 main 下创建一个目录 webapp 。Tomcat 可以同时加载多个 webapp 的,因此目录是 webapps 的 (每个 webapp 就相当于一个网站了),因为此处咱们写的是一个 webapp 就没有 s
——第二个目录 WEB-INF
——创建文件 web.xml
接下来,需要给 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 代码
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
do:处理 Get 对应到 HTTP 的 GET 方法 这个方法就是在 Tomcat 收到了一个 HTTP GET 请求 的时候,会被 Tomcat 调用到 (回调函数)
HttpServletRequest :代表一个 HTTP 请求 (Tomcat 已经收到了,已经解析成对象了)
HttpServletResponse :代表着 HTTP 响应 (当前这里的 resp 是一个空的响应对象,需要在代码中给这个对象设置一些属性的)
doGet 方法要做的工作,就是根据请求,计算生成响应~~
一个服务器的工作流程,就可以分成三个典型的步骤:
- 接收请求并解析
- 根据请求计算响应
- 构造响应数据,并返回给客户端
其中,1 3 这两步,Tomcat 已经帮我们做好了,这个就是咱们程序猿自己要实现的逻辑,也就是 doGet 要实现的内容~~
服务器想象成一个餐馆~~ 服务器上运行的代码,就要处理一个一个的请求~~
当老板收到 “来份炸酱面” 这个请求之后,后厨就行动起来了,根据客户提出的要求,来把这个炸酱面给做出来~
后厨这里,有个师傅负责切菜,有个师傅负责拉面,再有个师傅负责炸酱,再来个师傅把这里的结果给进行集成~~
服务员就可以把这份面端给我了~~
这三个师傅并发的工作,相当于三个线程!!
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("hello world!");
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 ~~
jar 和 war 都是 java 发布程序用的压缩包格式 war 算是给 Tomcat 专门搞的,这里不光会包含一些 .class ,还可以包含配置文件,以及依赖的第三方的 jar,还有 html css …
双击这个 package 触发打包
提示 BUILD SUCCESS 说明打包成功!!
6、部署
把 war 包 拷贝到 tomcat 的 webapps 目录下,启动 Tomcat
7、验证程序
咱们访问 Tomcat 的时候,是需要指定两级目录的
可以这样认为: 一个 Tomcat 上可以同时部署多个网站,一个网站上又有多个页面~~ 一个请求中的第一级路径,就告诉 Tomcat,要访问的网站是哪个,第二级路径,就告诉 Tomcat 要访问的页面是这个网站中的哪个页面
访问:http://127.0.0.1:8080/hello102/hello
没有成功,此时在 tomcat 控制台这里,敲了一下回车就出来了,显示的 hello world 就是返回的 HTTP 响应的 body 部分
这个情况是属于 cmd 的一个大坑!!!
CMD 有一个 “选择文本” 这样的模式
鼠标选中其中的一段文本,就会触发 CMD 的选中文本模式~~ 当 CMD 被选中文本的时候,CMD 就会把里面运行的程序给挂起 (暂停了,不往下走了)
- 现在这个页面内容是通过 Java 代码生成的了,但是这和你直接创建一个 HTML,里面写个 hello world 有啥区别呢??
- 页面里的内容是固定死的,是静态的,是不变的~~
- 这里的内容是可变的!!
- 根据用户不同的输入,可以得到不同的结果!!!
修改代码:获取时间戳
resp.getWriter().write("hello word!" + System.currentTimeMillis());
上述这七个步骤,这是针对一个新的项目~~ 项目创建好之后,后续修改代码,前三个步骤就不必重复了,直接从 4 - 7 进行操作即可~~ 重新部署的时候,不一定要重启,tomcat (理论上来说,是不需要重启的,但是 Windows 系统多少有点不顺畅的地方~~ ,一般在 Linux上,这个都是能顺利自动重新加载的~~)
如果看到形如这样的提示~~ 就已经重新部署了,deploy:部署
显示结果:hello word!1653073391039
smart tomcat
1、介绍
上面介绍的步骤,其实都是最朴素的操作了~~ 因此也有一些办法,来提高上述流程的效率,就可以通过一些第三方工具来简化 5 和 6 的操作,毕竟每次修改代码,都需要重新打包,重新部署~~
咱们是通过 IDEA 上的插件,直接把 Tomcat 给集成进来了~~ 做到 “一键式” 完成打包部署了~~
注意: Tomcat 和 IDEA 是两个独立的程序!!! Tomcat 不是 IDEA 功能的一部分!!
后面开发,主要还是通过 IDEA 调用 Tomcat 的方式来进行 用的时间长了之后,同学们就对于 Tomcat 的印象,就开始模糊~~
smart tomcat 是 idea 的一个插件 idea 本身虽然已经有很多功能了,但是也不能做到,面面俱到!! 因此,idea 还提供了一系列的扩展能力,允许第三方来开发一些程序,交给 idea 运行,相当于对 idea 进行了扩展~~
-
smart tomcat 并不是真的打包了~~ -
其实相当于把 webapp 目录作为 tomcat 启动的一个参数,给设定进去了,让 tomcat 直接从这个指定目录中加载 webapp -
Tomcat 默认是从 webapps 中加载 war 包,也不是只有这一条路~~ -
因此当你去看 Tomcat 的 webapps 目录,就会发现其实没有你的 war~~(不像手动拷贝)
2、安装
3、使用 smart tomcat
点击这里的三角号,就会自动完成打包部署重启 tomcat 这一系列操作
报错信息: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:
少写了第二级路径 Servlet Path:
错误 3: Servlet Path 写的和 URL 不匹配
请求的路径和服务器这边的配置不匹配:
错误 4: web.xml 写错了
当 web.xml 有问题的时候,tomcat 是不能正确加载到 webapp的~~ tomcat 发现这个目录中存在了 web.xml 且内容正确,tomcat 才能加载这个 webapp
2、405 —— Method Not Allowed
指 HTTP 中的方法
错误 1: 将 doGet 改成 doPost:
这样的用户操作,浏览器是构造了一个 GET 请求,而服务器这里写的是 doPost,不能处理 GET 请求~~
什么时候浏览器发的是 GET 请求?
-
直接在地址栏里,输入 URL -
通过 a 标签跳转 -
通过 img/link/script… -
通过 form 表单,method 指定为 GET -
通过 ajax,type 指定为 GET
什么时候浏览器发的是POST请求?
- 通过form表单, method指定为POST
- 通过ajax, type指定为POST
错误 2: 如果是代码中忘记注释掉 super.doGet(req, resp),这样的情况也会出现 405 !!!
进入到 HttpServlet 源码中,就能看到,此处父类的 doGet,干的事就是直接返回一个 405 的响应!!!
3、500
500 也是一个非常高频的错误,5 开头,是服务器出了问题 一般 500 就意味着服务器代码里抛出异常了,并且咱们的代码没处理,异常抛到 Tomcat 这里了~~
比如:如果代码中出现异常,可以通过 catch 来捕获到 如果 catch 没捕获到(类型不匹配,压根没 catch),异常就会沿着调用栈,向上传递
如果代码出现 500,就偷着乐吧,这种情况是最好解决的了!! 因为它直接告诉你,哪里有问题了!!!
异常的调用栈 (平时写代码的时候,IDEA 里出现的异常调用栈),告诉了我们出现异常的准确位置
4、空白页面
如果不给响应对象中设置任何内容,就会出现 空白页面
5、无法访问此网站
- 如果是这个情况,证明,网络就不通 (TCP 的层次上就是不通)
- 其中一个很大的可能性,就是 Tomcat 没有正确启动 (比如 Tomcat 端口被占用、启动失败……)
Servlet 运行原理
Servlet 是属于上层建筑,下面的传输层,网络层,数据链路层… 属于经济基础
- 当浏览器给服务器发送请求的时候,Tomcat 作为 HTTP 服务器,就可以接收到这个请求
HTTP 协议作为一个应用层协议,需要底层协议栈来支持工作,如下图所示:
Tomcat 其实是一个应用程序,运行在用户态的普通进程 (Tomcat 其实也是一个 Java进程) 用户写的代码 (根据请求计算相应),通过 Servlet 和 Tomcat 进行交互~~ Tomcat 进一步的和浏览器之间的网络传输,仍然是走的网络原理中的那一套:封装和分用
更详细的交互过程可以参考下图:
-
接收请求:
- 用户在浏览器输入一个 URL, 此时浏览器就会构造一个 HTTP 请求.
- 这个 HTTP 请求会经过网络协议栈逐层进行 封装 成二进制的 bit 流, 最终通过物理层的硬件设备转换成光信号/电信号传输出去.
- 这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达目标主机(这个过程也需要网络层和数据链路层参与).
- 服务器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成HTTP 请求. 并交给 Tomcat 进程进行处理(根据端口号确定进程)
- Tomcat 通过 Socket 读取到这个请求(一个字符串), 并按照 HTTP 请求的格式来解析这个请求, 根据
请求中的 Context Path 确定一个 webapp, 再通过 Servlet Path 确定一个具体的 类. 再根据当前请 求的方法 (GET/POST/…), 决定调用这个类的 doGet 或者 doPost 等方法. 此时我们的代码中的 doGet / doPost 方法的第一个参数 HttpServletRequest 就包含了这个 HTTP 请求的详细信息 -
根据请求计算响应:
- 在我们的 doGet / doPost 方法中, 就执行到了我们自己的代码. 我们自己的代码会根据请求中的一
些信息, 来给 HttpServletResponse 对象设置一些属性. 例如状态码, header, body 等. -
返回响应:
- 我们的 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 一启动就立即执行,也可能是 “懒加载” 但是此处伪代码中就假设立即加载了~~)
2). 根据刚才类加载的结果,给这些类创建 Servlet 实例~~
for (Class<Servlet> cls : allServletClasses) {
Servlet ins = cls.newInstance();
instanceList.add(ins);
}
3). 实例创建好之后,就可以调用当前 Servlet 实例的 init 方法了
Servlet 自带的方法,默认情况下 init 啥都不干 咱们在继承一个 HttpServlet 的时候,也可以自己重写 init,就可以在这个阶段,帮我们做一些初始化工作了
for (Servlet ins : instanceList) {
ins.init();
}
4). 创建 TCP socket,监听 8080 端口,等待有客户端来连接
Tomcat 为了能同时相应多个 HTTP 请求, 采取了多线程的方式实现, 因此 Servlet 是运行在 多线程环境 下的, Tomcat 内部也是通过 Socket API 进行网络通信,
每次有连接过来了,accept 就会返回,然后就给当前的线程池里,加个任务 doHttpRequest(socket); 这个任务里,就要负责处理这个请求了
此处这个主循环,和咱们之前讲过的 TCP 回显服务器,本质上没啥区别 Tomcat 工作的大部分时间都是在这个循环里完成的~~
ServerSocket serverSocket = new ServerSocket(8080);
ExecuteService pool = Executors.newFixedThreadPool(100);
while (true) {
Socket socket = ServerSocket.accept();
pool.execute(new Runnable() {
doHttpRequest(socket);
});
}
5). 如果循环退出了,Tomcat 也要结束了,就会依次循环调用每个 Servlet 的 destroy 方法
这个是属于收尾工作 (Tomcat 退出之前的事情)
for (Servlet ins : instanceList) {
ins.destroy();
}
没有 break 为啥还能走到下面的代码? 那是因为咱们当下写的是一个伪代码,只包含了核心逻辑,没有太多的实现细节~~ 实际上 Tomcat 里面会有一些条件来退出这个主循环 和 init 类似,这里的 destroy 默认也是啥都不干的,可以在用户代码中重写这个 destroy ~~
虽然这里有 5) 这个调用 destroy 这个环节,但是这个环节不一定可靠~~ 如果 Tomcat 是通过 “正常流程" 退出的,才能主动结束循环,调用这里的 destroy。如果是通过 “非正常流程" 退出的,此时就来不及调用 destroy
比如,Tomcat,8005 端口 (管理端口,通过这个端口可以对这个 Tomcat 发号施令),通过这个方式来关闭 Tomcat,就属于 “正常流程"
非正常流程:直接结束进程 (大部分是这种情况)
class Tomcat {
private List<Servlet> instanceList = new ArrayList<>()
public void start() {
Class<Servlet>[] allServletClasses = ...;
for (Class<Servlet> cls : allServletClasses) {
Servlet ins = cls.newInstance();
instanceList.add(ins);
}
for (Servlet ins : instanceList) {
ins.init();
}
ServerSocket serverSocket = new ServerSocket(8080);
ExecuteService pool = Executors.newFixedThreadPool(100);
while (true) {
Socket socket = ServerSocket.accept();
pool.execute(new Runnable() {
doHttpRequest(socket);
});
}
for (Servlet ins : instanceList) {
ins.destroy();
}
}
public static void main(String[] args) {
new Tomcat().start();
}
}
2、Tomcat 处理请求流程
class Tomcat {
void doHttpRequest(Socket socket) {
HttpServletRequest req = HttpServletRequest.parse(socket);
HttpServletRequest resp = HttpServletRequest.build(socket);
if (file.exists()) {
return;
}
Servlet ins = findInstance(req.getURL());
try {
ins.service(req, resp);
} catch (Exception e) {
}
}
}
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 里要有两级路径:
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);
|