一:Servlet
1.1 Servlet 是什么
Servlet 是一种实现动态页面的技术. 是一组 Tomcat 提供给程序猿的 API, 帮助程序猿简单高效的开发一个 web app.
构建动态页面的技术有很多, 每种语言都有一些相关的库/框架来做这件事,Servlet 就是 Tomcat 这个 HTTP 服务器提供给 Java 的一组 API, 来完成构建动态页面这个任务.
Servlet 主要做的工作:
- 允许程序猿注册一个类, 在 Tomcat 收到某个特定的 HTTP 请求的时候, 执行这个类中的一些代码.
- 帮助程序猿解析 HTTP 请求, 把 HTTP 请求从一个字符串解析成一个 HttpRequest 对象.
- 帮助程序猿构造 HTTP 响应. 程序猿只要给指定的 HttpResponse 对象填写一些属性字段,
- Servlet 就会自动的安装 HTTP 协议的方式构造出一个 HTTP 响应字符串, 并通过 Socket 写回给客户端.
简而言之, Servlet 是一组 Tomcat 提供的 API, 让程序猿自己写的代码能很好的和 Tomcat 配合起来, 从而更简单的实现一个 web app.
1.2 第一个 Servlet 程序
1.2.1 创建项目
- 使用 IDEA 创建一个 Maven 项目
菜单 -> 文件 -> 新建项目 -> Maven
2) 选择项目要存放的目录
3) 项目创建完毕后, 一般右下角会弹出以下对话框. 选择 Enable Auto-Import
1.2.2 引入依赖
Maven 项目创建完毕后, 会自动生成一个 pom.xml 文件,我们需要在 pom.xml 中引入 Servlet API 依赖的 jar 包.
- 在中央仓库 https://mvnrepository.com/ 中搜索 “servlet”, 一般第一个结果就是.
- 选择版本. 一般我们使用 3.1.0 版本
Servlet 的版本要和 Tomcat 匹配,如果我们使用 Tomcat 8.5, 那么就需要使用 Servlet 3.1.0
可以在 http://tomcat.apache.org/whichversion.html 查询版本对应关系.
3) 把中央仓库中提供的 xml 复制到项目的 pom.xml 中
修改后的 pom.xml 形如
<?xml version="1.0" encoding="UTF-8"?> <!-- XML声明,指定文档的版本和编码方式 -->
<project xmlns="http://maven.apache.org/POM/4.0.0" <!-- 定义一个名为project的XML元素,指定了XML命名空间 -->
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <!-- 引入了XML Schema实例命名空间 -->
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd"> <!-- 指定了XML Schema的位置 -->
<modelVersion>4.0.0</modelVersion> <!-- 定义POM模型的版本号 -->
<groupId>org.example</groupId> <!-- 定义项目的组织标识符,通常采用域名反转的形式 -->
<artifactId>ServletHelloWorld</artifactId> <!-- 定义项目的唯一标识符,通常是项目的名称 -->
<version>1.0-SNAPSHOT</version> <!-- 定义项目的版本号,可以采用语义化版本号格式 -->
<dependencies> <!-- 定义项目的依赖列表 -->
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api --> <!-- 注释:提供了依赖项的信息,指向Maven仓库的URL -->
<dependency> <!-- 定义一个依赖项 -->
<groupId>javax.servlet</groupId> <!-- 定义依赖项的组ID,通常指定依赖项所属的组织或库 -->
<artifactId>javax.servlet-api</artifactId> <!-- 定义依赖项的Artifact ID,通常指定依赖项的名称 -->
<version>3.1.0</version> <!-- 定义依赖项的版本号 -->
<scope>provided</scope> <!-- 定义依赖项的作用范围,provided表示该依赖项由运行时环境提供,不需要打包到项目中 -->
</dependency>
</dependencies>
</project>
< dependencies > 标签内部放置项目依赖的 jar 包. maven 会自动下载依赖到本地.
关于 groupId, artifactId, version 如果我们要把这个写的代码发布到中央仓库上,那么就需要设定好这几个 ID 了.
- groupId: 表示组织名称
- artifactId: 表示项目名称
- version: 表示版本号
中央仓库就是按照这三个字段来确定唯一一个包的.
红色方框圈出来的部分, 就是这个 jar 包的 groupId, artifactId, version
1.2.3 创建目录
当项目创建好了之后, IDEA 会帮我们自动创建出一些目录. 形如
这些目录中:
- src 表示源代码所在的目录
- main/java 表示源代码的根目录. 后续创建 .java 文件就放到这个目录中.
- main/resources 表示项目的一些资源文件所在的目录. 此处暂时不关注.
- test/java 表示测试代码的根目录. 此处暂时不关注.
这些目录还不够, 我们还需要创建一些新的目录/文件.
- 创建 webapp 目录
在 main 目录下, 和 java 目录并列, 创建一个 webapp 目录 (注意, 不是 webapps).
- 创建 web.xml
然后在 webapp 目录内部创建一个 WEB-INF 目录, 并创建一个 web.xml 文件(注意单词拼写.)
3) 编写 web.xml
往 web.xml 中拷贝以下代码
<!DOCTYPE web-app PUBLIC <!-- 声明文档类型,指定了web-app元素的DTD(文档类型定义) -->
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" <!-- DTD的标识符,指定了DTD的版本和语言 -->
"http://java.sun.com/dtd/web-app_2_3.dtd" > <!-- 指定了DTD的位置,用于验证XML文档的结构 -->
<web-app> <!-- 定义了一个名为web-app的XML元素,表示一个web应用程序 -->
<display-name>Archetype Created Web Application</display-name> <!-- 定义了web应用程序的显示名称 -->
</web-app> <!-- 结束web-app元素的定义 -->
webapp 目录就是未来部署到 Tomcat 中的一个重要的目录. 当前我们可以往 webapp 中放一些静态资源, 比如 html , css 等.
在这个目录中还有一个重要的文件 web.xml. Tomcat 找到这个文件才能正确处理 webapp 中的动态资源.
1.2.4 编写代码
在 java 目录中创建一个类 HelloServlet, 代码如下:
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
System.out.println("hello");
resp.getWriter().write("hello");
}
}
- 创建一个类 HelloServlet , 继承自 HttpServlet
- 在这个类上方加上 @WebServlet(“/hello”) 注解, 表示 Tomcat 收到的请求中, 路径为/hello的请求才会调用 HelloServlet 这个类的代码. (这个路径未包含 Context Path)
- 重写 doGet 方法. doGet 的参数有两个, 分别表示收到的 HTTP 请求 和要构造的 HTTP 响应. 这个方法会在Tomcat 收到 GET 请求时触发
- HttpServletRequest 表示 HTTP 请求. Tomcat 按照 HTTP 请求的格式把 字符串 格式的请求转成了一个 HttpServletRequest 对象. 后续想获取请求中的信息(方法, url, header, body 等)都是通过这个对象来获取.
- HttpServletResponse 表示 HTTP 响应. 代码中把响应对象构造好(构造响应的状态码, header,body 等)
- resp.getWriter() 会获取到一个流对象, 通过这个流对象就可以写入一些数据, 写入的数据会被构造成一个 HTTP 响应的body 部分, Tomcat 会把整个响应转成字符串, 通过 socket 写回给浏览器.
这个代码虽然只有寥寥几行, 但是包含的信息量是巨大的.
- 我们的代码不是通过 main 方法作为入口了. main 方法已经被包含在 Tomcat 里, 我们写的代码会被Tomcat 在合适的时机调用起来.
此时我们写的代码并不是一个完整的程序, 而是 Tomcat 这个程序的一小部分逻辑.
我们随便写个类都能被 Tomcat 调用嘛? 满足啥样条件才能被调用呢?得满足三个条件:
- 创建的类需要继承自 HttpServlet
- 这个类需要使用 @WebServlet 注解关联上一个 HTTP 的路径
- 这个类需要实现 doXXX 方法.
当这三个条件都满足之后, Tomcat 就可以找到这个类, 并且在合适的时机进行调用.
1.2.5 打包程序
使用 maven 进行打包. 打开 maven 窗口 (一般在 IDEA 右侧就可以看到 Maven 窗口, 如果看不到的话,可以通过 菜单 -> View -> Tool Window -> Maven 打开)
然后展开 Lifecycle , 双击 package 即可进行打包.
如果比较顺利的话, 能够看到 SUCCESS 这样的字样.
如果代码/配置/环境存在问题, 可能会提示 BUILD FAILED, 可以根据具体提示的错误信息具体解决.
打包成功后, 可以看到在 target 目录下, 生成了一个 jar 包.
这样的 jar 包并不是我们需要的, Tomcat 需要识别的是另外一种 war 包格式,另外这个 jar 包的名字太复杂了, 我们也希望这个名字能更简单一点.
war 包和 jar 包的区别
- jar 包是普通的 java 程序打包的结果. 里面会包含一些 .class 文件.
- war 包是 java web 的程序, 里面除了会包含 .class 文件之外, 还会包含 HTML, CSS, JavaScript,图片, 以及其他的 jar 包. 打成 war 包格式才能被 Tomcat 识别.
ServletHelloWorld-1.0-SNAPSHOT.jar 的由来
相当于把 artifactId 和 version 拼接起来了.
在 pom.xml 中新增一个 packing 标签, 表示打包的方式是打一个 war 包.
<packaging>war</packaging>
在 pom.xml 中再新增一个 build 标签, 内置一个 finalName 标签, 表示打出的 war 包的名字是HelloServlet
<build>
<finalName>ServletHelloWorld</finalName>
</build>
完整的 pom.xml 形如
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>ServletHelloWorld</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api
-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<packaging>war</packaging>
<build>
<finalName>ServletHelloWorld</finalName>
</build>
</project>
重新使用 maven 打包, 可以看到生成的新的 war 包的结果.
1.2.6 部署程序
把 war 包拷贝到 Tomcat 的 webapps 目录下,启动 Tomcat , Tomcat 就会自动把 war 包解压缩.
看到这个日志说明 Tomcat 已经正确识别了 ServletHelloWorld 这个 webapp.
1.2.7 验证程序
此时通过浏览器访问 http://127.0.0.1:8080/ServletHelloWorld/hello 就可以看到结果了.
注意: URL 中的 PATH 分成两个部分, 其中 HelloServlet 为 Context Path, hello 为 Servlet Path
1.3 更方便的部署方式
手动拷贝 war 包到 Tomcat 的过程比较麻烦. 我们还有更方便的办法,此处我们使用 IDEA 中的 Smart Tomcat 插件完成这个工作.
1.3.1 安装 Smart Tomcat 插件
- 菜单 -> 文件 -> Settings
2) 选择 Plugins, 选择 Marketplace, 搜索 “tomcat”, 点击 “Install”.
注意: 安装过程必须要联网.
- 安装完毕之后, 会提示 “重启 IDEA”
1.3.2 配置 Smart Tomcat 插件
- 点击右上角的 “Add Configuration”
- 选择左侧的 “Smart Tomcat”
3) 在 Name 这一栏填写一个名字(可以随便写)
在 Tomcat Server 这一栏选择 Tomcat 所在的目录. 其他的选项不必做出修改.
其中 Context Path 默认填写的值是项目名称.
这会影响到后面咱们的访问页面.
- 点击 OK 之后, 右上角变成了
点击绿色的三角号, IDEA 就会自动进行编译, 部署, 启动 Tomcat 的过程.
此时 Tomcat 日志就会输出在 IDEA 的控制台中, 可以看到现在就不再乱码了.
- 访问页面.
在浏览器中使用 http://127.0.0.1:8080/ServletHelloWorld/hello 访问页面.
注意路径的对应关系.
使用 Smart Tomcat 部署的时候, 我们发现 Tomcat 的 webapps 内部并没有被拷贝一个 war 包,也没有看到解压缩的内容.
Smart Tomcat 相当于是在 Tomcat 启动的时候直接引用了项目中的 webapp 和 target 目录.
1.4 访问出错怎么办?
1.4.1 出现 404
404 表示用户访问的资源不存在. 大概率是 URL 的路径写的不正确.
- 少写了 Context Path
通过 /hello 访问服务器
- 少写了 Servlet Path
通过 /ServletHelloWorld 访问服务器
- Servlet Path 写的和 URL 不匹配
修改 @WebServlet 注解的路径,重启 Tomcat 服务器.
此时URL 中的路径写作 “/hello” , 而代码中写作的 Servlet Path 为 “/helloServlet”, 两者不匹配.
- web.xml 写错了
清除 web.xml 中的内容重启 Tomcat 服务器,
通过浏览器访问 URL, 可以看到:
在 Tomcat 启动的时候也有相关的错误提示
1.4.2 出现 405
405 表示对应的 HTTP 请求方法没有实现.
没有实现 doGet 方法.
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
}
重启 Tomcat 服务器,在浏览器中访问, 可以看到:
在浏览器地址栏直接输入 URL , 会发送一个 HTTP GET 请求,此时就会根据 /ServletHelloWorld/hello 这个路径找到 HelloServlet 这个类. 并且尝试调用HelloServlet 的 doGet 方法.
但是如果没有实现 doGet 方法, 就会出现上述现象.
1.4.3 出现 500
往往是 Servlet 代码中抛出异常导致的.
修改代码:
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String s = null;
resp.getWriter().write(s.length());
}
}
重启 Tomcat 服务器,重新访问页面, 可以看到:
在页面上已经有具体的异常调用栈,异常信息里已经提示了出现异常的代码是 HelloServlet.java 的第 13 行.
resp.getWriter().write(s.length());
仔细检查这里的代码就可以看到空指针异常.
1.4.4 出现 “空白页面”
修改代码, 去掉 resp.getWritter().write() 操作.
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
System.out.println("hello");
}
}
重启服务器,访问服务器, 可以看到一个空白页面:
抓包可以看到, 响应 body 中的内容就是 “空数据”
1.4.5 出现 “无法访问此网站”
一般是 Tomcat 启动就失败了.
Servlet Path 写错了.
应该写作 “/hello”, Tomcat 在启动的时候已经提示了相关的错误,Tomcat 启动的日志里面报错信息可能比较多, 需要耐心观察, 找到关键的提示.
看到的现象:
1.4.6 总结
熟悉 HTTP 协议能够让我们调试问题事半功倍.
- 4xx 的状态码表示路径不存在, 往往需要检查 URL 是否正确, 和代码中设定的 Context Path 以及Servlet Path是否一致.
- 5xx 的状态码表示服务器出现错误, 往往需要观察页面提示的内容和 Tomcat 自身的日志, 观察是否存在报错.
- 出现连接失败往往意味着 Tomcat 没有正确启动, 也需要观察 Tomcat 的自身日志是否有错误提示.
- 空白页面这种情况则需要我们使用抓包工具来分析 HTTP 请求响应的具体交互过程.
1.5 Servlet 运行原理
在 Servlet 的代码中我们并没有写 main 方法, 那么对应的 doGet 代码是如何被调用的呢? 响应又是如何返回给浏览器的?
1.5.1 Tomcat 的定位
我们自己的实现是在 Tomcat 基础上运行的。
当浏览器给服务器发送请求的时候, Tomcat 作为 HTTP 服务器, 就可以接收到这个请求.
HTTP 协议作为一个应用层协议, 需要底层协议栈来支持工作. 如下图所示:
更详细的交互过程可以参考下图:
- 接收请求:
- 用户在浏览器输入一个 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中的数据按照一定的格式显示在浏览器的界面上.
1.5.2 Tomcat 的伪代码
下面的代码通过 “伪代码” 的形式描述了 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();
}
}
小结:
- Tomcat 的代码中内置了 main 方法. 当我们启动 Tomcat 的时候, 就是从 Tomcat 的 main 方法开始执行的.
- 被 @WebServlet 注解修饰的类会在 Tomcat 启动的时候就被获取到, 并集中管理.
- Tomcat 通过 反射 这样的语法机制来创建被 @WebServlet 注解修饰的类的实例.
- 这些实例被创建完了之后, 会点调用其中的 init 方法进行初始化. (这个方法是 HttpServlet 自带的,我们自己写的类可以重写 init)
- 这些实例被销毁之前, 会调用其中的 destory 方法进行收尾工作. (这个方法是 HttpServlet自带的,我们自己写的类可以重写 destory)
- Tomcat 内部也是通过 Socket API 进行网络通信.
- Tomcat 为了能同时相应多个 HTTP 请求, 采取了多线程的方式实现. 因此 Servlet 是运行在 多线程环境 下的.
- Tomcat 处理请求流程
class Tomcat {
void doHttpRequest(Socket socket) {
// 参照我们之前学习的 HTTP 服务器类似的原理,进行 HTTP 协议的请求解析,和响应构建
HttpServletRequest req = HttpServletRequest.parse(socket);
HttpServletRequest resp = HttpServletRequest.build(socket);
// 判断 URL 对应的文件是否可以直接在我们的根路径上找到对应的文件,如果找到,就是静态
内容
// 直接使用我们学习过的 IO 进行内容输出
if (file.exists()) {
// 返回静态内容
return;
}
// 走到这里的逻辑都是动态内容了
// 根据我们在配置中说的,按照 URL -> servlet-name -> Servlet 对象的链条
// 最终找到要处理本次请求的 Servlet 对象
Servlet ins = findInstance(req.getURL());
// 调用 Servlet 对象的 service 方法
// 这里就会最终调用到我们自己写的 HttpServlet 的子类里的方法了
try {
ins.service(req, resp);
} catch (Exception e) {
// 返回 500 页面,表示服务器内部错误
}
}
}
小结:
- Tomcat 从 Socket 中读到的 HTTP 请求是一个字符串, 然后会按照 HTTP
协议的格式解析成一个HttpServletRequest 对象. - Tomcat 会根据 URL 中的 path 判定这个请求是请求一个静态资源还是动态资源.
如果是静态资源,直接找到对应的文件把文件的内容通过 Socket 返回. 如果是动态资源, 才会执行到 Servlet 的相关逻辑. - Tomcat 会根据 URL 中的 Context Path 和 Servlet Path 确定要调用哪个 Servlet 实例的service方法.
- 通过 service 方法, 就会进一步调用到我们之前写的 doGet 或者 doPost
- Servlet 的 service 方法的实现
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 方法内部会根据当前请求的方法, 决定调用其中的某个 doXXX 方法.
- 在调用 doXXX 方法的时候, 就会触发 多态 机制, 从而执行到我们自己写的子类中的 doXXX 方法.
二: Servlet API 详解
2.1 HttpServlet
我们写 Servlet 代码的时候, 首先第一步就是先创建类, 继承自 HttpServlet, 并重写其中的某些方法.
核心方法:
方法名称 | 调用时机 |
---|---|
init | 在 HttpServlet 实例化之后被调用一次 |
destroy | 在 HttpServlet 实例不再使用的时候调用一次 |
service | 收到 HTTP 请求的时候调用 |
doGet | 收到 GET 请求的时候调用(由 service 方法调用) |
doPost | 收到 POST 请求的时候调用(由 service 方法调用) |
doPut/doDelete/doOptions/… | 收到其他请求的时候调用(由 service 方法调用) |
我们实际开发的时候主要重写 doXXX 方法, 很少会重写 init / destory / service .
这些方法的调用时机, 就称为 “Servlet 生命周期”. (也就是描述了一个 Servlet 实例从生到死的过程).
注意: HttpServlet 的实例只是在程序启动时创建一次. 而不是每次收到 HTTP 请求都重新创建实例.
2.1.1 代码示例: 处理 GET 请求
创建 MethodServlet.java, 创建 doGet 方法
@WebServlet("/method")
public class MethodServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.getWriter().write("GET response");
}
}
创建 testMethod.html, 放到 webapp 目录中, 形如
一个 Servlet 程序中可以同时部署静态文件. 静态文件就放到 webapp 目录中即可.
<button onclick="sendGet()">发送 GET 请求</button>
<script>
function sendGet() {
ajax({
method: 'GET',
url: 'method',
callback: function (body, status) {
console.log(body);
}
});
}
// 把之前封装的 ajax 函数拷贝过来
function ajax(args) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
// 0: 请求未初始化
// 1: 服务器连接已建立
// 2: 请求已接收
// 3: 请求处理中
// 4: 请求已完成,且响应已就绪
if (xhr.readyState == 4) {
args.callback(xhr.responseText, xhr.status)
}
}
xhr.open(args.method, args.url);
if (args.contentType) {
xhr.setRequestHeader('Content-type', args.contentType);
}
if (args.body) {
xhr.send(args.body);
} else {
xhr.send();
}
}
</script>
重新部署程序, 使用 URL http://127.0.0.1:8080/ServletHelloWorld/testMethod.html 访问页面.
点击 “发送 GET 请求” 按钮, 即可在控制台看到响应内容.
通过 Fiddler 抓包, 可以看到,
- 当浏览器中输入 URL 之后, 浏览器先给服务器发送了一个 HTTP GET 请求
- 当点击 “发送 GET 请求” 按钮, 浏览器又通过 ajax 给服务器发送了一个 HTTP GET 请求
注意这个 ajax 请求的 URL 路径. 代码中写的 URL url: ‘method’, 为一个相对路径, 最终真实发送的请求的 URL 路径为 /ServletHelloWorld/method
2.1.1.1 关于乱码问题
如果我们在响应代码中写入中文, 例如
resp.getWriter().write("GET 响应");
- 此时在浏览器访问的时候, 会看到 “乱码” 的情况.
- 我们可以在代码中, 通过 resp.setContentType(“text/html; charset=utf-8”); 显式的指定编码方式.
- 此时通过抓包可以看到, 当加上了resp.setContentType(“text/html; charset=utf-8”); 代码之后, 响应中多了 Content-Type 字段, 内部指定了编码方式. 浏览器看到这个字段就能够正确解析中文了.
2.1.2 代码示例: 处理 POST 请求
- 在 MethodServlet.java 中, 新增 doPost 方法.:
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws
ServletException, IOException {
resp.setContentType("text/html; charset=utf-8");
resp.getWriter().write("POST 响应");
}
- 在 testMethod.html 中, 新增一个按钮, 和对应的点击事件处理函数
<button onclick="sendPost()">发送 POST 请求</button>
<script>
function sendPost() {
ajax({
method: 'POST',
url: 'method',
callback: function (body, status) {
console.log(body);
}
})
}
</script>
- 重新部署程序, 使用 URLhttp://127.0.0.1:8080/ServletHelloWorld/testMethod.html 访问页面.
4. 点击 “发送 POST 请求” 按钮, 可以在控制台中看到结果
通过类似的方式还可以验证 doPut, doDelete 等方法. 此处不再一一演示.
2.2 HttpServletRequest
当 Tomcat 通过 Socket API 读取 HTTP 请求(字符串), 并且按照 HTTP 协议的格式把字符串解析成HttpServletRequest 对象.
核心方法:
方法 | 描述 |
---|---|
getProtocol() | 返回请求协议的名称和版本。 |
getMethod() | 返回请求的 HTTP 方法的名称,例如,GET、POST 或 PUT。 |
getRequestURI() | 返回该请求的 URL 的一部分,从协议名称直到 HTTP 请求的第一行的查询字符串中。 |
getContextPath() | 返回指示请求上下文的请求 URI 部分。 |
getQueryString() | 返回包含在路径后的请求 URL 中的查询字符串。 |
getParameterNames() | 返回一个包含请求中所有参数名称的 String 对象的枚举。 |
getParameter(String name) | 返回请求参数的值,以字符串形式表示,如果参数不存在则返回 null。 |
getParameterValues(String name) | 返回一个包含所有给定请求参数值的字符串对象数组,如果参数不存在则返回 null。 |
getHeaderNames() | 返回一个枚举,包含请求中所有头名。 |
getHeader(String name) | 返回指定请求头的值,以字符串形式表示。 |
getCharacterEncoding() | 返回请求主体中使用的字符编码的名称。 |
getContentType() | 返回请求主体的 MIME 类型,如果类型未知则返回 null。 |
getContentLength() | 返回请求主体的长度(以字节为单位),如果长度未知则返回 -1。 |
getInputStream() | 返回用于读取请求 body 内容的 InputStream 对象。 |
通过这些方法可以获取到一个请求中的各个方面的信息.
注意: 请求对象是服务器收到的内容, 不应该修改. 因此上面的方法也都只是 “读” 方法, 而不是 "写"方法.
2.2.1 代码示例: 打印请求信息
- 创建 ShowRequest 类
@WebServlet("/showRequest")
public class ShowRequest extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/html; charset=utf-8");
StringBuilder respBody = new StringBuilder();
respBody.append(req.getProtocol());
respBody.append("<br>");
respBody.append(req.getMethod());
respBody.append("<br>");
respBody.append(req.getRequestURI());
respBody.append("<br>");
respBody.append(req.getContextPath());
respBody.append("<br>");
respBody.append(req.getQueryString());
respBody.append("<br>");
respBody.append("<h3>headers:</h3>");
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
respBody.append(headerName + " ");
respBody.append(req.getHeader(headerName));
respBody.append("<br>");
}
resp.getWriter().write(respBody.toString());
}
}
- 部署程序.
在浏览器通过 URL http://127.0.0.1:8080/ServletHelloWorld/showRequest 访问, 可以看到
2.2.2 代码示例: 获取 GET 请求中的参数
GET 请求中的参数一般都是通过 query string 传递给服务器的. 形如https://v.bitedu.vip/personInf/student?userId=1111&classId=100
此时浏览器通过 query string 给服务器传递了两个参数, userId 和 classId, 值分别是 1111 和 100 我们在服务器端可以通过 getParameter 来获取到参数的值.
- 创建 GetParameter 类
@WebServlet("/getParameter")
public class GetParameter extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/html; charset=utf-8");
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
resp.getWriter().write("userId: " + userId + ", " + "classId: " +
classId);
}
}
- 重新部署程序, 在浏览器中通过 http://127.0.0.1:8080/ServletHelloWorld/getParameter 访问,可以看到
当没有 query string的时候, getParameter 获取的值为 null.
- 如果通过 http://127.0.0.1:8080/ServletHelloWorld/getParameter?userId=123&classId=456 访问, 可以看到
此时说明服务器已经获取到客户端传递过来的参数.
getParameter 的返回值类型为 String. 必要的时候需要手动把 String 转成 int.
2.2.3 代码示例: 获取 POST 请求中的参数(1)
POST 请求的参数一般通过 body 传递给服务器. body 中的数据格式有很多种. 如果是采用 form 表单的形式, 仍然可以通过 getParameter 获取参数的值.
- 创建类 PostParameter
@WebServlet("/postParameter")
public class PostParameter extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/html; charset=utf-8");
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
resp.getWriter().write("userId: " + userId + ", " + "classId: " +
classId);
}
}
- 创建 testPost.html, 放到 webapp 目录中
<form action="postParameter" method="POST">
<input type="text" name="userId">
<input type="text" name="classId">
<input type="submit" value="提交">
</form>
- 重新部署程序, 通过URLhttp://127.0.0.1:8080/ServletHelloWorld/testPost.html 访问, 可以看到 HTML
-
在输入框中输入内容, 点击提交
-
可以看到跳转到了新的页面, 并显示出了刚刚传入的数据.
-
此时通过抓包可以看到, form 表单构造的 body 数据的格式为:
POST http://127.0.0.1:8080/ServletHelloWorld/postParameter HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Content-Length: 22
Cache-Control: max-age=0
sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"
sec-ch-ua-mobile: ?0
Upgrade-Insecure-Requests: 1
Origin: http://127.0.0.1:8080
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/91.0.4472.114 Safari/537.36
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,imag
e/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1:8080/ServletHelloWorld/testPost.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
userId=123&classId=456
Content-Type: application/x-www-form-urlencoded, 对应的 body 数据格式就形如userId=123&classId=456
2.2.4 代码示例: 获取 POST 请求中的参数(2)
如果 POST 请求中的 body 是按照 JSON 的格式来传递, 那么获取参数的代码就要发生调整.
- 创建 PostParameterJson 类
@WebServlet("/postParameterJson")
public class PostParameterJson extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("application/json;charset=utf-8");
String body = readBody(req);
resp.getWriter().write(body);
}
private String readBody(HttpServletRequest req) throws IOException {
int contentLength = req.getContentLength();
byte[] buffer = new byte[contentLength];
InputStream inputStream = req.getInputStream();
inputStream.read(buffer);
return new String(buffer, "utf-8");
}
}
- 创建 testPostJson.html
<button onclick="sendJson()">发送 JSON 格式 POST 请求</button>
<script>
function sendJson() {
ajax({
url: 'postParameterJson',
method: 'POST',
contentType: 'application/json; charset=utf-8',
body: JSON.stringify({ userId: 123, classId: 456 }),
callback: function (body, status) {
console.log(body);
}
});
}
function ajax(args) {
// 函数体略.... 参考之前封装的版本.
}
</script>
- 在浏览器中通过 http://127.0.0.1:8080/ServletHelloWorld/testPostJson.html 访问, 可以看到
- 点击按钮, 则浏览器就会给服务器发送一个 POST 请求, body 中带有 JSON 格式.
POST http://127.0.0.1:8080/ServletHelloWorld/postParameterJson HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Content-Length: 28
sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/91.0.4472.114 Safari/537.36
Content-Type: application/json; charset=utf-8
Accept: */*
Origin: http://127.0.0.1:8080
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:8080/ServletHelloWorld/testPostJson.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
{"userId":123,"classId":456}
- 服务器收到这个结果之后, 又把数据返回了回去, 浏览器中看到了响应结果.
注意: 到目前为止, 服务器拿到的 JSON 数据仍然是一个整体的 String 类型, 如果要想获取到 userId 和classId 的具体值, 还需要搭配 JSON 库进一步解析.
2.2.5 代码示例: 获取 POST 请求中的参数(3)
引入 Jackson 这个库, 进行 JSON 解析.
- 在中央仓库中搜索 Jackson, 选择 JackSon Databind
- 把中央仓库中的依赖配置添加到 pom.xml 中, 形如:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
- 在 PostParameterJson 类中修改代码
// 创建一个新的类表示 JSON 数据, 属性的名字需要和 JSON 字符串中的 key 一致.
class JsonData {
public String userId;
public String classId;
}
@WebServlet("/postParameterJson")
public class PostParameterJson extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
String body = readBody(req);
// 创建 ObjectMapper 对象. 这个是 Jackson 中的核心类.
ObjectMapper objectMapper = new ObjectMapper();
// 通过 readValue 方法把 body 这个字符串转成 JsonData 对象
JsonData jsonData = objectMapper.readValue(body, JsonData.class);
resp.getWriter().write("userId: " + jsonData.userId + ", " + "classId: "
+ jsonData.classId);
}
private String readBody(HttpServletRequest req) throws IOException {
int contentLength = req.getContentLength();
byte[] buffer = new byte[contentLength];
InputStream inputStream = req.getInputStream();
inputStream.read(buffer);
return new String(buffer, "utf-8");
}
}
注意:
- JsonData 这个类用来表示解析之后生成的 JSON 对象. 这个类的属性的名字和类型要和 JSON 字符串的 key 相对应.
- Jackson 库的核心类为 ObjectMapper. 其中的 readValue 方法把一个 JSON 字符串转成 Java 对象.其中的 writeValueAsString 方法把一个 Java 对象转成 JSON 格式字符串.
- readValue 的第二个参数为 JsonData 的 类对象. 通过这个类对象, 在 readValue
的内部就可以借助反射机制来构造出 JsonData 对象, 并且根据 JSON 中key 的名字, 把对应的 value赋值给JsonData 的对应字段.