JavaWeb②Servlet 那些事
Java Web 三大组件:Servlet、Filter、Listener
Servlet(Server Applet)
服务器端程序
- 交互式处理客户端请求并完成响应。
- Java Web 的核心,实现动态网页技术。
1、Servlet 开发
回顾 Tomcat 服务器
- 以原生方式搭建项目,缺点十分明显,即不方便。
- 接下来通过 IDE 集成 Tomcat 开发。
1.1、原生方式开发
原生方式开发步骤
- 导入依赖:
tomcat/lib/servlet-api
- 编写 Servlet:实现 Servlet 接口的 5 个方法
- 部署 Servlet
- 使用 javac 编译为 class 文件
- 将 class 文件复制到
WEB-INF/classes
- 在
web.xml
中配置 Servlet
- 重启 Tomcat,访问 Servlet。
1.2、IDE 集成开发
基于 Maven 开发
1.2.1、web 工程
建议使用第一种方式。
方式一:添加 Web Application 框架支持
-
创建空 Maven 项目
-
右键项目,添加框架支持
方式二:webapp 模板
-
选择
maven-archetype-webapp
-
更新 web.xml 文件
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> </web-app>
1.2.2、项目结构
1.2.3、集成 Tomcat
步骤
-
设置 IDEA 的应用服务器
-
配置项目的 Tomcat 服务器
-
添加配置
-
配置模块
-
说明
-
应用上下文:默认与 Artifact 同名,表示项目的 URL
-
服务器信息
1.3、开发步骤(❗)
- 导入依赖:导入 Maven 依赖即可,而无需手动导入 jar 包
- 编写 Servlet:实现 Servlet 接口的 5 个方法
- 部署 Servlet:在 web.xml 中配置 Servlet
- 启动 Tomcat:IDEA 自动编译并将 class 文件输出到
WEB-INF/classes
- 访问 Servlet
1.3.1、导入依赖
在父工程导入依赖并刷新 Maven,所有子模块都会生效。
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
1.3.2、编写 Servlet
重点关注 service() 方法
public class HelloServlet implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {...}
@Override
public ServletConfig getServletConfig() {...}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("Hello, Servlet!");
}
@Override
public String getServletInfo() {...}
@Override
public void destroy() {...}
}
1.3.3、部署 Servlet
-
servlet-name
:Servlet 的名称(别名) -
servlet-class
:Servlet 所在类 -
url-pattern
:Servlet 访问路径<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- Servlet节点 --> <servlet> <servlet-name>helloServlet</servlet-name> <servlet-class>indi.jaywee.intro.HelloServlet</servlet-class> </servlet> <!-- Servlet映射节点 --> <servlet-mapping> <servlet-name>helloServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> </web-app>
1.4、打包
1.4.1、常见打包方式
项目开发完成后,就需要进行打包发布
常见的打包方式
全称 | 包含资源 | 适用容器 | |
---|---|---|---|
jar | Java Application Archive | Java类的普通库、资源(resources)、辅助文件(auxiliary files) | 应用服务器 (application servers) |
war | Web Application Archive | web 应用程序。即所有 Java 类、配置信息、静态资源 | 小型服务程序容器 (servlet containers) |
ear | Enterprise Application Archive | 企业应用程序。即所有 jar 文件、资源、Java 类和 Web 应用程序 | EJB 容器 (EJB containers) |
1.4.2、Tomcat 打包
Tomcat 以 war 包方式进行打包。
-
在项目结构中,选择要打包的模块
-
构建项目,项目自动打包输出到
out\artifacts
目录
1.4.3、Maven 打包
Maven 以 jar 包方式进行打包。
-
选择要打包的模块
-
先 clean 再 package,项目打包输出模块的
target
目录下
2、HTTP(❗)
了解 HTTP协议 相关知识,建议全文阅读
HTTP 协议 是 TCP/IP 协议族的一个子集。
- 用于 C/S 通信,通过请求和响应的交换达成通信。
- 客户端发出请求,服务器端响应请求并返回。
从 JavaWeb 开发的角度来看,至少要掌握以下概念。
2.1、基础知识
- URI 和 URL
- HTTP 版本:HTTP/1.0、HTTP/1.1
- 相关协议:DNS、TCP、IP
2.2、特点
- 无状态协议、Cookie
- 持久连接、管线化
- 内容协商(涉及的首部)
2.3、HTTP 报文
HTTP 报文:用于 HTTP 协议交互的信息。
-
分类:请求报文、响应报文
-
结构
- 报文首部:请求行 / 响应行、首部字段(通用、请求/响应、实体、其它)
- 报文主体:传输实体主体,如 Web 表单
-
常用请求方法:GET、POST
-
常见响应状态码
示例
3、聊聊 Servlet(❗)
3.1、Servlet API
Servlet 体系
- Servlet 接口
- GenericServlet 抽象类
- HttpServlet 抽象类
3.1.1、说明
Servlet 接口
-
所有 Servlet 都会(直接或间接)实现该接口,接口方法如下
-
以实现 Servlet 接口方式创建 Servlet,需要实现所有方法。
public interface Servlet { void init(ServletConfig var1) throws ServletException; ServletConfig getServletConfig(); void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; String getServletInfo(); void destroy(); }
GenericServlet 抽象类
- 实现 Servlet 接口:实现了除 service() 以外的其它方法。
- 以继承 GenericServlet 方式创建 Servlet,只需实现 service() 方法。
HttpServlet 类
- 继承 GenericServlet 抽象类:在抽象类的基础上扩展,以适用 HTTP。
- 以继承 HttpServlet 方式创建 Servlet,需要实现处理对应请求的方法(doGet/ doPost/ doPut/ doDelete)
3.1.2、HttpServlet
Java Web 中推荐以继承 HttpServlet 方式创建 Servlet
HttpServlet 中有两个 service() 方法
service(ServletRequest,ServletResponse) | service(HttpServletRequest,HttpServletResponse) | |
---|---|---|
来源 | 重写自父类 | 重载 |
权限 | public | protected |
说明 | 接收客户端请求,并分发给受保护的 service() | 接收 HTTP 请求,并分发给特定 doXxx() 方法 |
作用 | 接收请求 | 处理请求 |
接收请求的 service()
判断客户端请求(响应)是否为 HTTP 请求(响应)。
-
是:调用重载的 service()
-
否:抛出异常
@Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request; HttpServletResponse response; if (!(req instanceof HttpServletRequest && res instanceof HttpServletResponse)) { throw new ServletException("non-HTTP request or response"); } request = (HttpServletRequest) req; response = (HttpServletResponse) res; service(request, response); }
处理请求的 service()
判断请求类型,委托给对应的处理方法。
-
模板方法模式 的体现
-
请求首部
If-Modified-Since
:指定时间,只有服务器资源在指定时间之后被修改过才返回。protcted void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); if (method.equals(METHOD_GET)) { long lastModified = getLastModified(req); if (lastModified == -1) { doGet(req, resp); } else { long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE); // 最近修改时间 > 指定时间,符合条件 if (ifModifiedSince < lastModified) { maybeSetLastModified(resp, lastModified); doGet(req, resp); } else { resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } } } else if (method.equals(METHOD_HEAD)) { long lastModified = getLastModified(req); maybeSetLastModified(resp, lastModified); doHead(req, resp); } else if (method.equals(METHOD_POST)) { doPost(req, resp); } else if (method.equals(METHOD_PUT)) { doPut(req, resp); } else if (method.equals(METHOD_DELETE)) { doDelete(req, resp); } else if (method.equals(METHOD_OPTIONS)) { doOptions(req,resp); } else if (method.equals(METHOD_TRACE)) { doTrace(req,resp); } else { // // Note that this means NO servlet supports whatever method was requested, anywhere on this server. // String errMsg = lStrings.getString("http.method_not_implemented"); Object[] errArgs = new Object[1]; errArgs[0] = method; errMsg = MessageFormat.format(errMsg, errArgs); resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg); } }
3.2、配置方式
3.2.1、web.xml
基于 XML 配置文件
之前的 Servlet 开发中使用的就是这种方式。
-
servlet
servlet-name
:Servlet 的名称(别名)servlet-class
:Servlet 所在类(全限类名)load-on-startup
:启动时加载
-
servlet-mapping
servlet-name
url-pattern
:Servlet 访问路径
-
welcome-file-list
:欢迎页(首页)<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- Servlet配置 --> <servlet> <servlet-name>helloServlet</servlet-name> <servlet-class>indi.jaywee.intro.HelloServlet</servlet-class> <load-on-startup>0</load-on-startup> </servlet> <!-- 映射配置 --> <servlet-mapping> <servlet-name>helloServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> <!-- 首页 --> <welcome-file-list> <welcome-file>hello.jsp</welcome-file> </welcome-file-list> </web-app>
启动时加载
标记该 Servlet 是否在容器启动时加载
值必须是整数,越小优先级越高。
- 非负数:容器在应用启动时加载并初始化这个 Servlet,值越小优先级越高。
- 负数或没有设置:当 Servlet 被请求时才加载。
URL 匹配规则
匹配顺序:精确匹配 > 后缀匹配 > 通配
含义 | 说明 | |
---|---|---|
/具体URL |
精确匹配 | 通过指定 URL 访问 Servlet |
*.xxx |
后缀匹配 | 通过 xxx 后缀访问 Servlet |
/* |
通配 | 通过任意 URL 访问 Servlet |
/ |
通配 | 通过任意 URL 访问 Servlet(除 JSP) |
3.2.2、@WebServlet
基于注解
Servlet 3.0 后支持,推荐使用
常用属性
- name:Serlvet 名称(可省略)
- value / urlPatterns:配置 URL,可以配置多个
- loadOnStartup
示例
-
字面量方式:相当于省略
value=
@WebServlet("/my") public class MyServlet1 extends HttpServlet { }
-
name、多个 URL、启动时加载
@WebServlet(name = "my", value = {"/my1", "my2"}, loadOnStartup = 0) public class MyAnnoServlet extends HttpServlet { }
3.3、请求 & 响应(❗)
HTTP 基于 请求/响应 的交换达成通信。
- request 对象:处理客户端请求。
- response 对象:向客户端返回请求资源。
3.3.1、request
常用方法
getParameter(String name)
:根据表单组件名称,获取相应数据。setCharacterEncoding(String charset)
:设置请求报文的字符编码格式。
GET & POST
GET | POST | |
---|---|---|
数据 | 追加在 URL 之后 | 在报文主体中 |
特点 | 数据量小,明文传输,不安全 | 数据量大,密文传输,安全 |
效率 | 高,浏览默认请求方式 | 比 GET 低 |
中文乱码问题
原因:客户端和服务器端使用的字符编码方式不同。
- GET 请求
- Tomcat 7 及以下:客户端采用 UTF-8,而服务器端 request 对象采用 IS08859-1。
- Tomcat 8 及以上:服务器端根据请求报文的编码格式,自动转换编码。
- POST 请求:服务器端不会自动转换编码。
解决:使用 setCharacterEncoding()
设置统一编码。
3.3.2、response
常用方法
setCharacterEncoding(String)
:设置响应报文的字符编码格式。setHeader(name,value)
:设置响应报文首部setContentType(String)
:设置响应报文主体的媒体类型(实体首部)getWriter()
:获取打印流(属于 I/O 的字符输出流)
中文乱码问题
原因:服务器端默认采用 IS08859-1 编码。
两种解决方案
-
设置响应报文的字符编码格式和报文首部
response.setCharacterEncoding("utf-8"); response.setHeader( "Content-type", " text/html;charset=UTF-8" );
-
设置响应报文主体的媒体类型(推荐)
response.setContentType("text/html;charset=UTF-8");
3.3.3、示例
案例:模拟登录
- 客户端表单通过 POST 方法向 Servlet 发起请求。
- request 对象接收客户端请求,response 对象通过打印流向客户端输出信息。
页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<form action="login" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="psw"><br>
<input type="submit"> <input type="reset">
</form>
</body>
</html>
LoginServlet
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 请求
req.setCharacterEncoding("utf-8");
String username = req.getParameter("username");
String psw = req.getParameter("psw");
System.out.println(username + ":" + psw);
// 响应
resp.setContentType("text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.println("登录成功");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
3.4、请求转发 & 重定向(❗)
3.4.1、request 转发
服务器端内部将请求发送到服务器上的另一个资源。
-
说明:通常是一个 Servlet 用于接收并初步处理请求,转发给另一个资源生成响应。
-
核心方法:
forward()
request.getRequestDispatcher("/目标URL").forward (request, response);
特点
forward 将请求发送到服务器上的另一个资源(Servlet、JSP 或 HTML)
- 属于服务器行为:内部跳转
- 客户端只发送一次请求。
- 服务器内部对请求进行转发。
- 浏览器地址栏不变。
- 转发范围:同一个 Web 应用中的资源。
- request 作用域:一次请求(多次转发可以共享同一作用域)
- 数据存取(键值对)
- 存数据:
setAttribute(String key, Object value)
,可以存储任意类型数据 - 取数据:
- 作用域中的数据:
getAttribute(String key)
- 请求中的数据:
getParameter(String name)
- 作用域中的数据:
- 删数据:
req.removeAttribute(String name)
- 存数据:
3.4.2、response 重定向
客户端发送请求给服务器,服务器响应给客户端一个新的请求地址;
客户端重新发送请求。
-
说明:服务器向客户端发送一个临时重定向响应,设置状态码为 302,并将 相对URL 转换为 绝对URL
-
核心方法:
sendRedirect()
response.sendRedirect("目标URI");
特点
- 属于客户端行为:外部跳转
- 客户端至少发送两次请求:对 Servlet 的请求、对重定向地址的请求。
- 服务器发送临时重定向响应,客户端对新地址重新发出请求。
- 浏览器地址栏是重定向后的地址。
- 重定向范围:任意位置资源。
- response 无作用域:重定向使客户端发送了不同的 request 请求,无法共享 request 数据。
- 数据传递:无法存储数据到作用域
- 传数据:以 URI 拼接形式传递数据(类似 GET),只能是字符串类型
- 取数据:
req.getParameter(String)
说明:当需要传递数据时,建议使用 forward() 请求转发,而不是 sendRedirect() 重定向。
3.4.3、示例
案例:使用 3.3.3 中的表单(修改 action 属性),对请求转发和重定向进行测试。
请求转发
-
客户端表单向 ServletA 发起请求。
-
ServletA 接收并初步处理请求(将属性保存作用域中)转发给 ServletB。
-
ServletB 处理请求(获取作用域中的数据,向控制台输出)
-
客户端的地址栏不会发生改变。
(伪代码如下,仅展示关键逻辑)
@WebServlet("/fa") public class ForwardServletA extends HttpServlet { doGet(req, resp) { String username = req.getParameter("username"); req.setAttribute("username", username); req.getRequestDispatcher("/fb").forward(req, resp); } // doPost() } @WebServlet("/fb") public class ForwardServletB extends HttpServlet { @Override doGet(req, resp) { String username = (String) req.getAttribute("username"); System.out.println(username); } // doPost() }
重定向
-
客户端表单向 ServletA 发起请求。
-
ServletA 重定向给 ServletB,通过 URI 附带参数。
-
ServletB 处理请求(获取 URI 参数数据,向控制台输出)
-
客户端的地址栏发生改变。
(伪代码如下)
@WebServlet("/ra") public class RedirectServletA extends HttpServlet { doGet(req, resp) { String username = req.getParameter("username"); resp.sendRedirect("rb?username=" + username); } // doPost() } @WebServlet("/rb") public class RedirectServletB extends HttpServlet { doGet(req, resp) { String username = req.getParameter("username"); System.out.println(username); } // doPost() }
3.4.4、URI 路径说明
URI 常涉及的问题:
- 路径前面加不加
/
,区别是什么。- 什么情况是基于当前 Web 应用访问。
- 什么情况是基于当前 Web 服务器访问。
根据转发和重定向的特点,就可以分辨区别。
请求转发
- 转发范围:只能访问同一个 Web 应用中的资源。
- 路径:无论是否有前缀
/
,都是访问当前 Web 应用的资源。
重定向
-
转发范围:任意位置资源。
- 当前 Web 应用
- 当前 Web 服务器的其它应用
- 其它 Web 服务器的应用
-
路径
- 无前缀:访问当前 Web 应用中的资源。
- 有前缀
/
:访问当前 Web 服务器端口下的资源。
3.5、生命周期(❗)
Servlet 生命周期 4 阶段
service() 通常会执行多次,其它阶段只执行一次。
-
实例化:两种情况
- 用户首次访问 Servlet 时,容器调用 Servlet 构造器进行实例化。
- loadOnStart:容器启动时加载 Servlet 实例。
-
初始化:容器调用
init()
,方法参数为 ServletConfig。 -
服务:当服务器端接收到客户端请求,容器调用
service()
将请求调度给 Servlet。- 方法参数:ServletRequest 与 ServletResponse 对象
- Servlet 通常被访问多次,因此 service() 相应被执行多次。
-
销毁:当容器停止或重启,调用
destroy()
方法销毁 Servlet 对象。
3.6、线程安全
Servlet 被访问之后会产生一个实例
多线程并发访问同一个 Servlet 实例,会有线程安全问题。
情景:
-
线程 A 访问 Servlet,执行代码 1 判断用户不合法(isValid == false)
-
线程 A 执行代码 2 之前,线程 B 访问 Servlet
-
线程 B 执行代码 1 判断用户合法(isValid == true)
-
此时线程 A 继续执行,执行代码 2 时 isValid 已经被修改为 true 了。
public class SynServlet extends HttpServlet { private boolean isValid; doGet(req, resp) { String username = req.getParameter("username"); String psw = req.getParameter("psw"); // 1、判断用户合法性,若合法则赋值true // 2、isValid成立则输出 if (isValid) { resp.getWriter().print("OK"); } } // doPost() }
保证线程安全的方式
- synchronized 同步代码块
- SingleThreadModel 接口
- 改用局部变量
3.6.1、synchronized
-
说明:将存在线程安全问题的代码放到同步代码块中。
-
会阻塞其它线程,不推荐。
synchronized (isValid) { // 1、判断用户合法性,若合法则赋值true // 2、isValid成立则输出 if (isValid) { resp.getWriter().print("OK"); } }
3.6.2、SingleThreadModel
-
说明:每个线程访问 Servlet 时都会创建 Servlet 实例。
-
浪费内存资源,效率低,不推荐。
public class SynServlet extends HttpServlet implements SingleThreadModel { // ... }
3.6.3、局部变量
-
说明:尽量不要使用成员变量,改用局部变量。
-
每个线程都会在方法内声明一个临时变量,推荐。
public class SynServlet extends HttpServlet { doGet(req, resp) { boolean isValid; //... } }
小结
Servlet 开发
- 开发方式
- 原生
- IDE 集成开发:Maven、集成 Tomcat
- 开发步骤(❗)
- 导入依赖
- 编写 Servlet:重点是 service() 方法
- 部署 Servlet
- 打包方式:jar、war、ear
- Tomcat 打 war 包
- Maven 打 jar 包
HTTP(❗)
- 很重要,回顾 第 2 节
- 有时间的话建议系统学习计算机网络(附 HTTP协议)
Servlet 小结
- API
- Servlet 接口:5 个方法
- GenericServlet 抽象类:service() 抽象方法
- HttpServlet 类:doXxx(),模板方法模式
- HttpServlet 的 2 个 service() 方法
- 重写,用于接收请求
- 重载,用于处理请求
- Servlet 配置方式
- 配置:web.xml
- 注解:@WebServlet
- 请求、响应(❗)
- 请求转发、重定向(❗)
- 生命周期(❗):实例化、初始化、服务(执行多次)、销毁
- 线程安全
Servlet vs 框架
- Serlert:性能最高,但需要手动实现很多功能。
- Spring MVC:基于 Servlet 的扩展,如同 MyBatis 封装 JDBC。
- 封装共性代码,专注于业务代码。
- 实现了 Servlet 的功能,只需在配置文件中注册。
- Spring Boot:自动装配,不用注册。