javaEE - 23( 21000 字 Servlet 入门 -1 )
一: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"?>
<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>
</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).
2) 创建 web.xml
然后在 webapp 目录内部创建一个 WEB-INF 目录, 并创建一个 web.xml 文件(注意单词拼写.)
3) 编写 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>
webapp 目录就是未来部署到 Tomcat 中的一个重要的目录. 当前我们可以往 webapp 中放一些静态资源, 比如 html , css 等.
在这个目录中还有一个重要的文件 web.xml. Tomcat 找到这个文件才能正确处理 webapp 中的动态资源.
1.2.4 编写代码
在 java 目录中创建一个类 HelloServlet, 代码如下:
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {@Overrideprotected 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 {@Overrideprotected 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 {@Overrideprotected 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 服务器// 并用线程池的方式分别处理每一个 RequestServerSocket 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 {@Overrideprotected 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 访问页面.
点击 “发送 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 {@Overrideprotected 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 {@Overrideprotected 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 {@Overrideprotected 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.8userId=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 {@Overrideprotected 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
2) 把中央仓库中的依赖配置添加到 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 {@Overrideprotected 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 的对应字段.
相关文章:

javaEE - 23( 21000 字 Servlet 入门 -1 )
一:Servlet 1.1 Servlet 是什么 Servlet 是一种实现动态页面的技术. 是一组 Tomcat 提供给程序猿的 API, 帮助程序猿简单高效的开发一个 web app. 构建动态页面的技术有很多, 每种语言都有一些相关的库/框架来做这件事,Servlet 就是 Tomcat 这个 HTTP…...
【sentinel流量卫兵搭建与微服务整合】
sentinel流量卫兵搭建与微服务整合 搭建sentinel dashboard控制台微服务整合搭建sentinel dashboard控制台 1、下载 官网链接 由于官网github网络原因,导致长时间下载失败。 网盘链接 网盘提取码:dwgj 2、运行 将下载jar包放在任意非中文、不包含特殊字符的目录下,重名为…...

Linux环境下配置mysql主从复制
主从配置需要注意的地方 1、主DB server和从DB server数据库的版本一致 2、主DB server和从DB server数据库数据一致[这里就会可以把主的备份在从上还原,也可以直接将主的数据目录拷贝到从的相应数据目录] 3、主DB server开启二进制日志,主DB server和从DB serve…...

生物素-PEG4-酪胺,Biotin-PEG4-TSA,应用于酶联免疫吸附实验
您好,欢迎来到新研之家 文章关键词:生物素-PEG4-酪胺,Biotin-PEG4-Tyramide,Biotin-PEG4-TSA 一、基本信息 产品简介:Biotin PEG4 Tyramine is a reagent used for tyramine signal amplification (TSA) through ca…...
Android:文件读写
3.10 Android读写文件 1、读写文件 Android读写文件操作,不能写入到系统根目录,只能在应用包下文件夹进行读写。 使用getCacheDir()方法,获取当前应用的Cache目录路径; 使用getFilesDir()方法,获取当前应用的files目录路径; 示例: //读取数据public void readData(){…...
2024/2/5
第四章 堆与拷贝构造函数 一 、程序阅读题 1、给出下面程序输出结果。 #include <iostream.h> class example {int a; public: example(int b5){ab;} void print(){aa1;cout <<a<<"";} void print()const {cout<<a<<endl;} …...

政安晨:示例演绎Python的函数与获取帮助的方法
调用函数和定义我们自己的函数,并使用Python内置的文档,是成为一位Pythoner的开始。 通过我的上篇文章,相信您已经看过并使用了print和abs等函数。但是Python还有许多其他函数,并且定义自己的函数是Python编程的重要部分。 在本…...

88 docker 环境下面 前端A连到后端B + 前端B连到后端A
前言 呵呵 最近出现了这样的一个问题, 我们有多个前端服务, 分别连接了对应的后端服务, 前端A -> 后端A, 前端B -> 后端B 但是 最近的时候 却会出现一种情况就是, 有些时候 前端A 连接到了 后端B, 前端B 连接到了 后端A 我们 前端服务使用 nginx 提供前端 html, js…...
k8s学习-Service Account和RBAC授权
1.1 ServiceAccount 介绍 首先Kubernetes中账户区分为:User Accounts(用户账户) 和 Service Accounts(服务账户) 两种,它们的设计及用途如下: UserAccount是给kubernetes集群外部用户使用的&am…...
SpringMVC-响应数据
一、引子 我们在上一篇文章SpringMVC-组件解析里介绍了SpringMVC框架执行一个请求的过程,并演示了快速使用Controller承接请求。本篇我们将深入介绍SpringMVC执行请求时,如何响应客户端。 二、响应类型 SpringMVC的数据响应方式主要分为两类ÿ…...

数学建模:数据相关性分析(Pearson和 Spearman相关系数)含python实现
相关性分析是一种用于衡量两个或多个变量之间关系密切程度的方法。相关性分析通常用于探索变量之间的关系,以及预测一个变量如何随着另一个变量的变化而变化。在数学建模中,这是常用的数据分析手段。 相关性分析的结果通常用相关系数来表示ÿ…...

使用pandas将excel转成json格式
1.Excel数据 2.我们想要的JSON格式 {"0": {"raw_data1": "Sam","raw_data2": "Wong","raw_data3": "Good","layer": "12v1"},"1": {"raw_data1": "Lucy…...

双向链表的插入、删除、按位置增删改查、栈和队列区别、什么是内存泄漏
2024年2月4日 1.请编程实现双向链表的头插,头删、尾插、尾删 头文件: #ifndef __HEAD_H__ #define __HEAD_H__ #include<stdio.h> #include<stdlib.h> #include<string.h> typedef int datatype; enum{FALSE-1,SUCCSE}; typedef str…...

Linux 驱动开发基础知识——总线设备驱动模型(七)
个人名片: 🦁作者简介:学生 🐯个人主页:妄北y 🐧个人QQ:2061314755 🐻个人邮箱:2061314755qq.com 🦉个人WeChat:Vir2021GKBS 🐼本文由…...
RTthread线程间通信(邮箱,消息队列,信号/软件中断)---03信号(软件中断)源码分析
信号 实际使用看这一个 #if defined(RT_USING_SIGNALS)rt_sigset_t sig_pending; /**< the pending signals 记录来了的信号 */rt_sigset_t sig_mask; /**< the mask bits of signal 记录屏蔽的信号 */rt_sigh…...

老版本labelme如何不保存imagedata
我的版本是3.16,默认英文且不带取消保存imagedata的选项。 最简单粗暴的方法就是在json文件保存时把传递过来的imagedata数据设定为None,方法如下: 找到labelme的源文件,例如:D:\conda\envs\deeplab\Lib\site-packages…...

vscode 如何修改c/c++格式化风格,大括号不换行
在Visual Studio Code(VSCode)中,若要修改C代码格式化的风格以实现大括号不换行,通常会借助于插件C/C扩展中的ClangFormat配置。以下是具体的步骤: 确保已安装了C/C扩展: 打开VSCode的扩展市场(…...

IP协议(2) 和 数据链路层协议基础
IP协议续 1.路由选择 在复杂的网络结构中,我们需要找到一个通往终点的路线,这就是路由选择 举个例子:我们在没有手机导航之前,想去一个地方得是到一个地方问一下路的方式最终找到目的地 路由的过程,其实就是样子问路的过程 1.当IP数据包到达路由器的时候,会查看目的IP 2.路由器…...
Flink-1.18.1环境搭建
下载 下载flink安装包 Index of /dist/flink/flink-1.18.1 下载flink-cdc安装包 Release Release 3.0.0 ververica/flink-cdc-connectors GitHub 安装 添加环境变量 vi ~/.bash_profile export FLINK_HOME=/home/postgres/flink/flink-1.18.1 export PATH=$PATH:$FL…...
deepin20.9安装及配置
安装deepin20.9很简单,刻录u盘 安装 一路next apt install nginx global vim-nox debian11 使用apt安装php, 使php多版本共存_debain11 php5-CSDN博客 vim LeaderF安装问题 - 知乎 debian10安装vue环境, 包括安装node.js-CSDN博客 debian安装vue3 nodejs20-CSD…...

7.4.分块查找
一.分块查找的算法思想: 1.实例: 以上述图片的顺序表为例, 该顺序表的数据元素从整体来看是乱序的,但如果把这些数据元素分成一块一块的小区间, 第一个区间[0,1]索引上的数据元素都是小于等于10的, 第二…...
可靠性+灵活性:电力载波技术在楼宇自控中的核心价值
可靠性灵活性:电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中,电力载波技术(PLC)凭借其独特的优势,正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据,无需额外布…...

微信小程序 - 手机震动
一、界面 <button type"primary" bindtap"shortVibrate">短震动</button> <button type"primary" bindtap"longVibrate">长震动</button> 二、js逻辑代码 注:文档 https://developers.weixin.qq…...

2021-03-15 iview一些问题
1.iview 在使用tree组件时,发现没有set类的方法,只有get,那么要改变tree值,只能遍历treeData,递归修改treeData的checked,发现无法更改,原因在于check模式下,子元素的勾选状态跟父节…...

SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现
摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序,以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务,提供稳定高效的数据处理与业务逻辑支持;利用 uniapp 实现跨平台前…...
sqlserver 根据指定字符 解析拼接字符串
DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...

优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...

基于TurtleBot3在Gazebo地图实现机器人远程控制
1. TurtleBot3环境配置 # 下载TurtleBot3核心包 mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3.git git clone -b noetic https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git git clone -b noetic-dev…...

push [特殊字符] present
push 🆚 present 前言present和dismiss特点代码演示 push和pop特点代码演示 前言 在 iOS 开发中,push 和 present 是两种不同的视图控制器切换方式,它们有着显著的区别。 present和dismiss 特点 在当前控制器上方新建视图层级需要手动调用…...

DingDing机器人群消息推送
文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人,点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置,详见说明文档 成功后,记录Webhook 2 API文档说明 点击设置说明 查看自…...