大家好,我是晓星航。今天为大家带来的是 Servlet 相关的讲解!
Servlet 是一种实现动态页面的技术. 是一组 Tomcat 提供给程序猿的 API, 帮助程序猿简单高效的开发一 个 web app.
回顾 动态页面 vs 静态页面
静态页面也就是内容始终固定的页面. 即使 用户不同/时间不同/输入的参数不同 , 页面内容也不会发生变化. (除非网站的开发人员修改源代码, 否则页面内容始终不变).
对应的, 动态页面指的就是 用户不同/时间不同/输入的参数不同, 页面内容会发生变化.
举个栗子:
Tomcat 的主页
https://tomcat.apache.org/
就是一个静态页面.而 B 站的主页
https://www.bilibili.com/
则是一个动态页面.
构建动态页面的技术有很多, 每种语言都有一些相关的库/框架来做这件事.
Servlet 就是 Tomcat 这个 HTTP 服务器提供给 Java 的一组 API, 来完成构建动态页面这个任务.
Servlet 主要做的工作
当然, Servlet 还支持一些其他的辅助功能, 此处暂时先不介绍.
简而言之, Servlet 是一组 Tomcat 提供的 API, 让程序猿自己写的代码能很好的和 Tomcat 配合起来, 从 而更简单的实现一个 web app.
而不必关注 Socket, HTTP协议格式, 多线程并发等技术细节, 降低了 web app 的开发门槛, 提高了开发效 率.
一共有7个步骤:
使用 IDEA 创建一个 Maven 项目.
Maven 是个"工程管理" 工具。
Enable Auto-Import
此时这个项目就创建出来了
Maven 项目创建完毕后, 会自动生成一个 pom.xml 文件.
我们需要在 pom.xml 中引入 Servlet API 依赖的 jar 包.
Servlet 的版本要和 Tomcat 匹配.
如果我们使用 Tomcat 8.5, 那么就需要使用 Servlet 3.1.0
可以在 http://tomcat.apache.org/whichversion.html 查询版本对应关系.
修改后的 pom.xml 形如
<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.0modelVersion>
<groupId>org.examplegroupId>
<artifactId>ServletHelloWorldartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>3.1.0version>
<scope>providedscope>
dependency>
dependencies>
project>
标签内部放置项目依赖的 jar 包. maven 会自动下载依赖到本地.
关于 groupId, artifactId, version
这几个东西暂时我们不关注. 啥时候需要关注呢? 如果我们要把这个写的代码发布到中央仓库上, 那 么就需要设定好这几个 ID 了.
groupId: 表示组织名称
artifactId: 表示项目名称
version: 表示版本号
中央仓库就是按照这三个字段来确定唯一一个包的.
红色方框圈出来的部分, 就是这个 jar 包的 groupId, artifactId, version
当项目创建好了之后, IDEA 会帮我们自动创建出一些目录. 形如
这些目录中:
这些目录还不够, 我们还需要创建一些新的目录/文件.
在 main 目录下, 和 java 目录并列, 创建一个 webapp 目录 (注意, 不是 webapps).
然后在 webapp 目录内部创建一个 WEB-INF 目录, 并创建一个 web.xml 文件
这里的顺序一定一定是webapp,其里面创建WEB-INF,再里面创建web.xml,顺序一定一定不能出错,这里博主一开始就因为顺序和文件夹位置创建错误从而找了半个小时的bug
一开始因为提示缺少xml文件,但是博主这里实际上是创建了web.xml
文件的,因此博主找了很久,最后发现可能是位置不对,从而删除了之前的结构,重新创建。
结果梅开二度,第一次是webapp创建到了src下没有和main文件并列,这次是web.xml没有创建到WEB-INF下,导致我们的idea还是没有识别到.xml文件导致报错,再博主又搜寻了10min之后,终于发现了这个罪魁祸首!!!
我们在错误过后,更改文件夹位置,为了重新创建war包,我们需要把原先的包给他清除掉,不然还是会报错,导致我们继续看着计算机陷入沉思…
这里我们先通过idea找到原先的war包文件夹位置:
将原先的这两个war压缩包和war解压之后的文件夹都删掉,然后在重新使用maven的package功能重新打包
那么此时我嫩拿到的这个4KB的war包文件就是正确的啦!!!
注意单词拼写,目录的名字和结构,都不能错.
往 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 Applicationdisplay-name>
web-app>
webapp 目录就是未来部署到 Tomcat 中的一个重要的目录. 当前我们可以往 webapp 中放一些静 态资源, 比如 html , css 等.
在这个目录中还有一个重要的文件 web.xml. Tomcat 找到这个文件才能正确处理 webapp 中的动态资源.
这个文件的作用是让 tomcat 能够识别当前的代码为 webapp,并进行加载。
在 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");
}
}
注解是 Java 中的特殊的类。Java 专门定义了一种"语法糖"来实现注解。注解的作用:针对一个类/方法,进行额外的"解释说明",赋予这个类/方法额外的功能/含义。此处的 @WebServlet 注解,作用是把当前的类,和一个 HTTP 请求的路径关联起来。
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 写回给浏览器.这个代码虽然只有寥寥几行, 但是包含的信息量是巨大的.
当这三个条件都满足之后, Tomcat 就可以找到这个类, 并且在合适的时机进行调用.
使用 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>warpackaging>
在 pom.xml 中再新增一个 build 标签, 内置一个 finalName 标签, 表示打出的 war 包的名字是 HelloServlet
<build>
<finalName>ServletHelloWorldfinalName>
build>
完整的 pom.xml 形如
<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.0modelVersion>
<groupId>org.examplegroupId>
<artifactId>ServletHelloWorldartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>3.1.0version>
<scope>providedscope>
dependency>
dependencies>
<packaging>warpackaging>
<build>
<finalName>ServletHelloWorldfinalName>
build>
project>
重新使用 maven 打包, 可以看到生成的新的 war 包的结果.
把 war 包拷贝到 Tomcat 的 webapps 目录下.
启动 Tomcat , Tomcat 就会自动把 war 包解压缩.
看到这个日志说明 Tomcat 已经正确识别了 ServletHelloWorld 这个 webapp.
此时通过浏览器访问 http://127.0.0.1:8080/hello_servlet/hello
就可以看到结果了.
注意: URL 中的 PATH 分成两个部分, 其中 HelloServlet 为 Context Path, hello 为 Servlet Path
手动拷贝 war 包到 Tomcat 的过程比较麻烦. 我们还有更方便的办法.
此处我们使用 IDEA 中的 Smart Tomcat 插件完成这个工作.
理解 “插件” (plugin)
天火 + 擎天柱 => 会飞的擎天柱.
天火在牺牲之前把自己变成了擎天柱的 “飞行插件”. 在擎天柱需要起飞的时候就变成翅膀装在擎天柱身上. 不需要起飞的时候就卸下来放到擎天柱的集装箱里.
程序开发的时候也经常如此.
像 IDEA 这样的程序虽然功能强大, 但是也无法面面俱到. 对于一些特殊场景的功能, 开发者就可以 开发一些 “插件”. 如果需要这个插件, 就单独安装.
插件就是对程序的一些特定场景, 做出一些特定的功能的扩展.
注意: 安装过程必须要联网.
如果右上角没有这个
也可以通过找到上面的Run点击
然后选择Edit Configurations
点击这里的 + 号
Name
这一栏填写一个名字(可以随便写)在 Tomcat Server
这一栏选择 Tomcat 所在的目录. 其他的选项不必做出修改.
这会影响到后面咱们的访问页面.
点击绿色的三角号, IDEA 就会自动进行编译, 部署, 启动 Tomcat 的过程.
此时 Tomcat 日志就会输出在 IDEA 的控制台中, 可以看到现在就不再乱码了.
在浏览器中使用 http://127.0.0.1:8080/ServletHelloWorld/hello
访问页面.
注意路径的对应关系.
使用 Smart Tomcat 部署的时候, 我们发现 Tomcat 的 webapps 内部并没有被拷贝一个 war 包, 也没有看到解压缩的内容.
Smart Tomcat 相当于是在 Tomcat 启动的时候直接引用了项目中的 webapp 和 target 目录.
404 表示用户访问的资源不存在. 大概率是 URL 的路径写的不正确.
错误实例1: 少写了 hello_servlet
通过 /hello
访问服务器
错误实例2: 少写了 hello
通过 /hello_servlet
访问服务器
错误实例3: Servlet Path 写的和 URL 不匹配
修改 @WebServlet 注解的路径
重启 Tomcat 服务器.
URL 中的路径写作 “/hello” , 而代码中写作的 Servlet Path 为 “/helloServlet”, 两者不匹配.
错误实例4: web.xml 写错了
清除 web.xml 中的内容
重启 Tomcat 服务器.
通过浏览器访问 URL, 可以看到:
在 Tomcat 启动的时候也有相关的错误提示
405 表示对应的 HTTP 请求方法没有实现.
错误实例: 没有实现 doGet 方法.
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
}
重启 Tomcat 服务器.
在浏览器中访问, 可以看到:
在浏览器地址栏直接输入 URL , 会发送一个 HTTP GET 请求.
此时就会根据 /ServletHelloWorld/hello
这个路径找到 HelloServlet
这个类. 并且尝试调用 HelloServlet
的 doGet 方法.
但是如果没有实现 doGet 方法, 就会出现上述现象.
往往是 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());
仔细检查这里的代码就可以看到空指针异常.
错误实例:
修改代码, 去掉 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 中的内容就是 “空数据”
一般是 Tomcat 启动就失败了.
错误实例: Servlet Path 写错了.
应该写作 “/hello”, Tomcat 在启动的时候已经提示了相关的错误.
Tomcat 启动的日志里面报错信息可能比较多, 需要耐心观察, 找到关键的提示.
看到的现象:
初学 Servlet, 遇到的这类问题会非常多. 我们不光要学习 Servlet 代码的基本写法, 也要学习排查错误的思路.
程序猿调试 BUG 如同医生诊病.
一个有经验的程序猿和一个新手程序猿相比, 最大的优势往往不是代码写的多好, 而是调试效率有多高. 同一个问题可能新手花了几天都无法解决的, 但是有经验的程序猿可能几分钟就搞定了.
你还觉得 “程序猿是吃青春饭” 嘛 ?
熟悉 HTTP 协议能够让我们调试问题事半功倍.
观察日志是调试程序的重要途径. Tomcat 的日志往往很多, 需要同学们耐心阅读, 经常阅读, 熟练了 就能更快速的找到问题了.
在 Servlet 的代码中我们并没有写 main 方法, 那么对应的 doGet 代码是如何被调用的呢? 响应又是如何 返回给浏览器的?
我们自己的实现是在 Tomcat 基础上运行的。
当浏览器给服务器发送请求的时候, Tomcat 作为 HTTP 服务器, 就可以接收到这个请求.
HTTP 协议作为一个应用层协议, 需要底层协议栈来支持工作. 如下图所示:
更详细的交互过程可以参考下图:
下面的代码通过 “伪代码” 的形式描述了 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();
}
}
小结
@WebServlet
注解修饰的类会在 Tomcat 启动的时候就被获取到, 并集中管理.@WebServlet
注解修饰的类的实例.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 页面,表示服务器内部错误
}
}
}
小结
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);
}
......
}
}
小结
理解此处的 多态
我们前面自己写的 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);
我们写 Servlet 代码的时候, 首先第一步就是先创建类, 继承自 HttpServlet, 并重写其中的某些方法.
我们实际开发的时候主要重写 doXXX 方法, 很少会重写 init / destory / service .
这些方法的调用时机, 就称为 “Servlet 生命周期”. (也就是描述了一个 Servlet 实例从生到死的过程).
注意: HttpServlet 的实例只是在程序启动时创建一次. 而不是每次收到 HTTP 请求都重新创建实例.
创建 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 目录中即可.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 使用这个页面来构造 ajax 请求 -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>
$.ajax({
type:'get',
url:'method',
success:function(data,status) {
console.log(data);
}
})
</script>
</body>
</html>
重新部署程序, 使用 URL http://http://127.0.0.1:8080/hello_servlet/testMethod.html
访问页面.
虽然这里啥都没有, 但是我们可以在控制台看到响应内容.
通过 Fiddler 抓包, 可以看到,
注意这个 ajax 请求的 URL 路径. 代码中写的 URL
url: 'method'
, 为一个相对路径, 最终真实发 送的请求的 URL 路径为/ServletHelloWorld/method
关于乱码问题
如果我们在响应代码中写入中文, 例如
resp.getWriter().write("GET 响应");
此时在浏览器访问的时候, 会看到 “乱码” 的情况.
关于 "乱码 ":
中文的编码方式有很多种. 其中最常见的就是 utf-8 .
如果没有显式的指定编码方式, 则浏览器不能正确识别编码, 就会出现乱码的情况.
可以在代码中, 通过 resp.setContentType("text/html; charset=utf-8");
显式的指定编码方式.
此时通过抓包可以看到, 当加上了 resp.setContentType("text/html; charset=utf-8");
代码之后, 响应中多了 Content-Type 字段, 内部指定了编码方式. 浏览器看到这个字段就能够正确解析中文了.
在 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>
重新部署程序, 使用 URL http://127.0.0.1:8080/ServletHelloWorld/testMethod.html
访问页面.
点击 “发送 POST 请求” 按钮, 可以在控制台中看到结果
通过类似的方式还可以验证 doPut, doDelete 等方法. 此处不再一一演示.
当 Tomcat 通过 Socket API 读取 HTTP 请求(字符串), 并且按照 HTTP 协议的格式把字符串解析成 HttpServletRequest 对象.
通过这些方法可以获取到一个请求中的各个方面的信息.
注意: 请求对象是服务器收到的内容, 不应该修改. 因此上面的方法也都只是 “读” 方法, 而不是 "写 " 方法.
创建 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("
");
respBody.append(req.getMethod());
respBody.append("
");
respBody.append(req.getRequestURI());
respBody.append("
");
respBody.append(req.getContextPath());
respBody.append("
");
respBody.append(req.getQueryString());
respBody.append("
");
respBody.append("headers:
");
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
respBody.append(headerName + " ");
respBody.append(req.getHeader(headerName));
respBody.append("
");
}
resp.getWriter().write(respBody.toString());
}
}
部署程序.
在浏览器通过 URL http://127.0.0.1:8080/hello_servlet/showRequest
访问, 可以看到
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/hello_servlet/getParameter
访问, 可以看到
当没有 query string的时候, getParameter 获取的值为 null.
如果通过 http://127.0.0.1:8080/hello_servlet/getParameter?userId=123&classId=456
访问, 可以看到
此时说明服务器已经获取到客户端传递过来的参数.
getParameter 的返回值类型为 String. 必要的时候需要手动把 String 转成 int.
POST 请求的参数一般通过 body 传递给服务器. body 中的数据格式有很多种. 如果是采用 form 表单的 形式, 仍然可以通过 getParameter 获取参数的值.
创建类 PostParameter
@WebServlet("/postParameter")
public class PostParameterServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String studentId = req.getParameter("studentId");
String classId = req.getParameter("classId");
resp.setContentType("text/html");
resp.getWriter().write("studentId = " + studentId + " classId = " + classId);
}
}
创建 testPost.html, 放到 webapp 目录中
<form action="postParameter" method="post">
<input type="text" name="studentId">
<input type="text" name="classId">
<input type="submit" value="提交">
form>
重新部署程序, 通过 URL http://127.0.0.1:8080/ServletHelloWorld/test.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
如果 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/hello_servlet/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 库进一步解析.
引入 Jackson 这个库, 进行 JSON 解析.
com.fasterxml.jackson.core
jackson-databind
2.12.3
// 创建一个新的类表示 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");
}
}
注意
Servlet 中的 doXXX 方法的目的就是根据请求计算得到相应, 然后把响应的数据设置到 HttpServletResponse 对象中.
然后 Tomcat 就会把这个 HttpServletResponse 对象按照 HTTP 协议的格式, 转成一个字符串, 并通过 Socket 写回给浏览器.
注意: 响应对象是服务器要返回给浏览器的内容, 这里的重要信息都是程序猿设置的. 因此上面的方 法都是 "写 " 方法.
注意: 对于状态码/响应头的设置要放到 getWriter / getOutputStream 之前. 否则可能设置失效.
实现一个程序, 用户在浏览器通过参数指定要返回响应的状态码.
创建 StatusServlet 类
@WebServlet("/statusServlet")
public class StatusServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String statusString = req.getParameter("status");
if (statusString != null) {
resp.setStatus(Integer.parseInt(statusString));
}
resp.getWriter().write("status: " + statusString);
}
}
部署程序, 在浏览器中通过 URL http://127.0.0.1:8080/ServletHelloWorld/statusServlet?status=200
访问, 可以看到
抓包结果:
HTTP/1.1 200
Content-Length: 11
Date: Mon, 21 Jun 2021 08:05:37 GMT
Keep-Alive: timeout=20
Connection: keep-alive
status: 200
变换不同的 status 的值, 就可以看到不同的响应结果.
实现一个程序, 让浏览器每秒钟自动刷新一次. 并显示当前的时间戳.
创建 AutoRefreshServlet 类
@WebServlet("/autoRefreshServlet")
public class AutoRefreshServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setHeader("Refresh", "1");
long timeStamp = new Date().getTime();
resp.getWriter().write("timeStamp: " + timeStamp);
}
}
Refresh
字段, 可以控制浏览器自动刷新的时机.部署程序, 通过 URL http://127.0.0.1:8080/ServletHelloWorld/autoRefreshServlet
访问, 可以 看到浏览器每秒钟自动刷新一次.
抓包结果
HTTP/1.1 200
Refresh: 1
Content-Length: 24
Date: Mon, 21 Jun 2021 08:14:29 GMT
Keep-Alive: timeout=20
Connection: keep-alive
timeStamp: 1624263269995
实现一个程序, 返回一个重定向 HTTP 响应, 自动跳转到另外一个页面.
创建 RedirectServlet 类
@WebServlet("/redirectServlet")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.sendRedirect("http://www.sogou.com");
}
}
部署程序, 通过 URL http://127.0.0.1:8080/ServletHelloWorld/redirect
访问, 可以看 到, 页面自动跳转到 搜狗主页了.
抓包结果
HTTP/1.1 302
Location: http://www.sogou.com
Content-Length: 0
Date: Mon, 21 Jun 2021 08:17:26 GMT
Keep-Alive: timeout=20
Connection: keep-alive
结合上述 API, 我们可以把之前实现的表白墙程序修改成服务器版本. 这样即使页面关闭, 表白墙的内容也 不会丢失.
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 Applicationdisplay-name>
web-app>
引入依赖, 配置生成 war 包, 以及 war 包名字
<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.0modelVersion>
<groupId>org.examplegroupId>
<artifactId>表白墙服务器版artifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>3.1.0version>
<scope>providedscope>
dependency>
<properties>
<encoding>UTF-8encoding>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>3.1.0version>
<scope>providedscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.45version>
dependency>
<packaging>warpackaging>
<build>
<finalName>testfinalName>
build>
project>
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 Applicationdisplay-name>
web-app>
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.getWriter().write("hello");
}
}
@WebServlet("/getParameter")
public class GetParameter extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String contentType = req.getHeader("Content-Type");
// 或者使用
String contentType = req.getContentType();
}
}
@WebServlet("/getParameter")
public class GetParameter extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
}
}
@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");
req.setCharacterEncoding("utf-8");
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
resp.getWriter().write("userId: " + userId + ", " + "classId: " +
classId);
}
}
@WebServlet("/statusServlet")
public class StatusServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setStatus(200);
}
}
@WebServlet("/autoRefreshServlet")
public class AutoRefreshServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setHeader("Refresh", "1");
}
}
@WebServlet("/redirectServlet")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.sendRedirect("http://www.sogou.com");
}
}
<form action="login" method="POST">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="提交">
</form>
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
HttpSession session = req.getSession(true);
session.setAttribute("username", "admin");
session.setAttribute("loginCount", "0");
}
}
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
HttpSession session = req.getSession(false);
if (session == null) {
// 用户没有登陆, 重定向到 login.html
resp.sendRedirect("login.html");
return;
}
// 如果已经登陆, 则从 Session 中取出数据
String userName = (String)session.getAttribute("username");
String countString = (String)session.getAttribute("loginCount");
}
@MultipartConfig
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Part part = req.getPart("MyImage");
System.out.println(part.getSubmittedFileName());
System.out.println(part.getContentType());
System.out.println(part.getSize());
part.write("d:/MyImage.jpg");
resp.getWriter().write("upload ok");
}
}
<form action="upload" enctype="multipart/form-data" method="POST">
<input type="file" name="MyImage">
<input type="submit" value="提交图片">
</form>
t {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setStatus(200);
}
}
## 9.9设置响应报头
```java
@WebServlet("/autoRefreshServlet")
public class AutoRefreshServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setHeader("Refresh", "1");
}
}
@WebServlet("/redirectServlet")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.sendRedirect("http://www.sogou.com");
}
}
<form action="login" method="POST">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="提交">
</form>
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
HttpSession session = req.getSession(true);
session.setAttribute("username", "admin");
session.setAttribute("loginCount", "0");
}
}
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
HttpSession session = req.getSession(false);
if (session == null) {
// 用户没有登陆, 重定向到 login.html
resp.sendRedirect("login.html");
return;
}
// 如果已经登陆, 则从 Session 中取出数据
String userName = (String)session.getAttribute("username");
String countString = (String)session.getAttribute("loginCount");
}
@MultipartConfig
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Part part = req.getPart("MyImage");
System.out.println(part.getSubmittedFileName());
System.out.println(part.getContentType());
System.out.println(part.getSize());
part.write("d:/MyImage.jpg");
resp.getWriter().write("upload ok");
}
}
<form action="upload" enctype="multipart/form-data" method="POST">
<input type="file" name="MyImage">
<input type="submit" value="提交图片">
</form>
感谢各位读者的阅读,本文章有任何错误都可以在评论区发表你们的意见,我会对文章进行改正的。如果本文章对你有帮助请动一动你们敏捷的小手点一点赞,你的每一次鼓励都是作者创作的动力哦!