JavaWeb 学习笔记 3:Servlet
1.简介
Servlet 是 JavaEE 定义的一套 Web 应用开发标准(接口),实现了该技术的 Web 服务器软件(如 Tomcat)上可以运行一个 Servlet 容器,只要我们使用 Servlet 技术开发 Web 应用,就可以打成 war 包后放在 Web 服务器上,Web 服务器软件可以自动解包,并执行其中 Servlet 相关的 API 实现类,以对外提供服务。
整个过程可以表示为:
2.快速开始
先按照上篇文章说的,创建一个 Maven Web 项目。
这里我使用 Maven 模板的方式创建项目:
mvn archetype:generate ^
-DgroupId=cn.icexmoon ^
-DartifactId=web-demo ^
-DarchetypeArtifactId=maven-archetype-webapp ^
-Dversion=0.0.1-snapshot ^
-DinteractiveMode=false
这里是 CMD 中执行的多行命令,因为分行符号的不同,不同的终端工具写法有所不同。
会自动生成一个 JUnit 依赖,不过版本太老(3.x),这里替换为 4.x:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
添加 Tomcat 启动插件:
<plugins>
<!--Tomcat插件 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
</plugin>
</plugins>
添加 Servlet API 的依赖:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
创建一个类,并实现Servlet
接口:
public class HelloServlet implements Servlet {
public void init(ServletConfig servletConfig) throws ServletException {
}
public ServletConfig getServletConfig() {
return null;
}
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("Hello World!");
}
public String getServletInfo() {
return null;
}
public void destroy() {
}
}
其中负责处理 HTTP 请求的是service
方法,这里简单的在控制台打印一句话。
新版本的 Servlet 可以使用注解的方式指定请求路径:
@WebServlet("/hello")
public class HelloServlet implements Servlet {
// ...
}
访问 http://localhost:8080/web-demo/hello,在服务端控制台可以看到输出。
3.Servlet 生命周期
Servlet 对象由 Web 服务器创建,其中实现的Servlet
接口相关方法也由 Web 服务器在适当的时候执行。这些执行时机是和 Servlet 对象的生命周期相关的。
Servlet 对象有以下生命周期:
- 实例创建:由 Web 服务器创建 Servlet 实例,默认为延迟创建(有相关的 HTTP 请求时才创建)。
- 初始化:Servlet 对象创建后,Web 服务器会调用
Servlet.init()
方法执行初始化工作,一般用于准备 Servlet 处理 HTTP 请求需要的资源等。 - 处理请求:有 Servlet 相关的 HTTP 请求产生后,Web 服务器会调用
Servlet.service()
方法处理请求并返回处理结果(HTTP 响应报文)。 - 销毁:当 Web 服务器(正常)关闭,或者 JVM 执行内存清理时,Web 服务器会调用
Servlet.destroy()
方法执行清理工作,主要包含对 Servlet 拥有资源的清理和关闭等。
3.1.init
默认情况下 init()
方法只会在第一次 HTTP 请求产生后被调用:
@WebServlet("/hello")
public class HelloServlet implements Servlet {
public void init(ServletConfig servletConfig) throws ServletException {
System.out.println("Servlet init...");
}
// ...
}
这是因为我们上边说过的,Servlet 实例创建的方式默认为延迟创建,而初始化是在创建之后执行。
可以将 Servlet 对象的创建方式修改为“急切创建”:
@WebServlet(value = "/hello", loadOnStartup = 1)
public class HelloServlet implements Servlet {
// ...
}
属性loadOnStartup
决定 Servlet 对象创建的时机:
- 负数,延迟创建。在第一次相关的 HTTP 请求产生后创建 Servlet 对象。
- 0 或正整数,急切创建。Web 服务器启动后立即创建 Servlet 对象,数字越小优先级越高。
3.2.service
在有相关的 HTTP 请求产生时被调用。负责处理 HTTP 请求,并生成响应报文。
public class HelloServlet implements Servlet {
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("Hello World!");
}
}
3.3.destroy
Web 服务器(正常)关闭,或者 JVM 执行内存清理时被调用。
public class HelloServlet implements Servlet {
public void destroy() {
System.out.println("Servlet destroy...");
}
// ...
}
要注意的是,像直接终止 Tomcat 进程这种操作属于非正常关闭,Tomcat 是来不及执行正常的关闭流程的,也就不会执行 Servlet 的清理工作。
因此,要触发destroy
方法,需要让 Tomcat 正常退出。其中的一个方法是在命令行下用 Maven 插件启动 Tomcat,并使用热键Ctrl+C
退出程序:
D:\workspace\learn-javaweb\ch3\web-demo>mvn tomcat7:run
[INFO] Scanning for projects...
[INFO]
...
九月 08, 2023 11:33:53 上午 org.apache.coyote.AbstractProtocol start
信息: Starting ProtocolHandler ["http-bio-8080"]
Servlet destroy...
终止批处理操作吗(Y/N)? y
4.Servlet 抽象层级
4.1.请求分发
默认情况下,写在 Servlet.service()
方法中的内容可以用于处理任意 Request Method 类型的 HTTP 请求。这通常是不合适的,通常我们需要对特定 Reuqest Method 的请求使用特定的处理。
比如,如果要对 http://localhost:8080/web-demo/hello 请求时的 POST 和 GET 方法执行不同的处理逻辑:
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
switch (request.getMethod()){
case "GET":
System.out.println("From get request.");
break;
case "POST":
System.out.println("From post request.");
break;
default:
}
System.out.println("Hello World!");
}
测试 POST 请求可以使用 HTTP 调试工具(比如 APIPost)。
4.2.模板模式
如果每个 Servlet 都要这样写,就很麻烦。可以使用模板模式的思想进行重构,抽离出一个 Servlet 的公共基类:
public abstract class HttpServletTemplate implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public final void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
if (!(servletRequest instanceof HttpServletRequest)
|| !(servletResponse instanceof HttpServletResponse)) {
throw new RuntimeException("Servlet 请求和响应对象不是 HTTP 相关");
}
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
switch (request.getMethod()){
case "GET":
this.doGet(request, response);
break;
case "POST":
this.doPost(request, response);
break;
default:
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
这里有一些细节:
- 模板类并不需要生成实例,所以被定义为抽象类(abstract)。
service
方法负责按照 Request Method 类别进行分发,被分发后的方法(比如doGet
)需要被子类继承和重写,所以设置为protected
。考虑到灵活性,这里没有将其设置为抽象方法,否则即使不用处理POST
请求,也必须重写doPost
方法。- 分发请求的
service()
方法不能被子类重写,所以设置为final
。
现在只需要很少的代码就可以实现之前的要求:
@WebServlet("/hello2")
public class Hello2Servlet extends HttpServletTemplate{
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
System.out.println("From post request.");
System.out.println("Hello World2!");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
System.out.println("From get request.");
System.out.println("Hello World2!");
}
}
可以按照需要覆盖doXXX
方法。
4.3.HttpServlet
实际上这种抽象和重构的工作并不需要我们自己完成,Servlet 本身就提供类似的抽象:
因此我们只需要继承HttpServlet
就可以了:
@WebServlet("/hello3")
public class Hello3Servlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
System.out.println("From get request.");
System.out.println("Hello World3!");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
System.out.println("From post request.");
System.out.println("Hello World3!");
}
}
5.urlPattern
Servlet 依赖于 @WebServlet
注解的urlPatterns
属性确定是否与请求路径(url)匹配。
@WebServlet
的value
属性是urlPatterns
属性的别名。
5.1.精确匹配
常见的 urlPattern 都是采用精确匹配:
@WebServlet("/user/list")
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
System.out.println("UserServlet...");
}
}
此时只有请求路径是/user/list
的 HTTP 请求才会被这个 Servlet 处理。
urlPattern 可以配置多个值,此时这些规则都可以用于匹配:
@WebServlet({"/user/list","/user/1"})
现在无论是/user/list
这样的请求还是/user/1
这样的请求都会被这个 Servlet 处理。
5.2.路径匹配
可以使用通配符(*)匹配某个路径下的请求,比如:
@WebServlet("/user/*")
public class User2Servlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
System.out.println("User2Servlet...");
}
}
示例中的 Servlet 可以处理任意以/user/
开头的路径。
需要注意的是,是可以一个路径同时匹配多个 Servlet 的 urlPattern 的,比如目前这个示例中,/user/list
这样的请求同时可以被UserServlet
和User2Servlet
的规则匹配。此时精确匹配的优先级高于路径匹配,所以是UserServlet
处理请求。
5.3.扩展名匹配
在 url 中同样可以使用扩展名作为请求结尾,比如:
http://localhost:8080/web-demo/book/list.do
可以用下面的 Servlet 匹配和处理:
@WebServlet("*.do")
public class User3Servlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
System.out.println("User3Servlet...");
}
}
需要注意的是,扩展名匹配前不能加路径分隔符/
,比如:
@WebServlet("/*.do")
此时应用无法启动,会报错:
java.lang.IllegalArgumentException: Invalid <url-pattern> /*.do in servlet mapping...
5.4.任意匹配
可以用/
或/*
匹配任意路径的请求。
@WebServlet("/")
public class User4Servlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
System.out.println("User4Servlet...");
}
}
上面的示例可以匹配任意没有被其它 Servlet 匹配的请求。
可以同时配置/
和/*
匹配,但后者的优先级更高。此外/
配置后会替换掉 Tomcat 默认的DefaultServlet
,该 Servlet 负责处理对静态资源的请求。换言之,使用/
或/*
匹配路径可能导致无法访问静态资源。所以不推荐在项目中使用/
和/*
匹配路径。
最后,几种匹配模式的优先级是 精确匹配 > 目录匹配> 扩展名匹配 > /* > / 。
6.用 XML 配置 Servlet
Servlet 从 3.0 版本之后支持以注解的方式进行配置,在 3.0 版本之前需要在web.xml
文件中配置。
添加一个不用注解配置的 Servlet:
public class User5Servlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
System.out.println("User5Servlet...");
}
}
修改 web.xml
:
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>user4servlet</servlet-name>
<servlet-class>cn.icexmoon.webdemo.urlpattern.User4Servlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>user4servlet</servlet-name>
<url-pattern>/user/4</url-pattern>
</servlet-mapping>
</web-app>
和用注解配置的方式效果是相同的。
The End,谢谢阅读。
本文的完整示例可以从这里获取。
7.参考资料
- 黑马程序员JavaWeb基础教程