一、生命周期

Servlet 生命周期可被定义为从创建直到毁灭的整个过程。以下是 Servlet 遵循的过程:

  • Servlet 初始化后调用init ()方法。
  • Servlet 调用service()方法来处理客户端的请求。
  • Servlet 销毁前调用destroy() 方法。
  • 最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。

现在让我们详细讨论生命周期的方法。

1、初始化(init)

init()方法被设计成只调用一次。它在第一次创建 Servlet 时被调用,在后续每次用户请求时不再调用。

1
2
3
4
@Override
public void init(ServletConfig servletConfig) throws ServletException {
// 初始化代码
}

默认情况下Servlet在第一次被访问时创建,但可以指定Servlet的创建时机,只需在web.xml文件中的<servlet/>标签内使用<load-on-startup/>标签来指定即可,配置如下所示:

1
2
3
4
5
6
7
8
9
<!-- 配置Servlet -->
<servlet>
<!-- Servlet名称 -->
<servlet-name>demo</servlet-name>
<!-- 对应的Servlet实现类 -->
<servlet-class>cn.frankfang.servlet.ServletDemo</servlet-class>
<!-- 指定Servlet的创建时机 -->
<load-on-startup>-1</load-on-startup>
</servlet>

下面对<load-on-startup/>标签的值进行详细介绍:

创建时机
第一次被访问时 负数,默认为-1
服务器启动时 零或正整数

注:Servlet 的 init() 方法只执行一次,说明一个 Servlet 在内存中只存在一个对象,Servlet 是单例的。当多个用户同时访问时可能存在线程安全问题,因此尽量不要在 Servlet 中定义成员变量。

2、提供服务(service)

service()方法是执行实际任务的主要方法。Servlet 容器(即 Web 服务器)调用service()方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。

每次服务器接收到一个 Servlet 请求时,服务器会产生一个新的线程并调用服务。service()方法检查 HTTP 请求类型(GET、POST、PUT、DELETE 等),并在适当的时候调用doGet()doPost()doPut()doDelete() 等方法。

每次访问 Servlet 时,service()方法都会被调用一次。

1
2
3
4
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
// 处理请求代码
}

3、销毁(destroy)

destroy()方法只会在 Servlet 生命周期结束时被调用一次。destroy()方法可以让 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘,并执行其他类似的清理活动。

在调用destroy()方法之后,Servlet 对象被标记为垃圾回收。destroy()方法定义如下所示:

1
2
3
4
@Override
public void destroy() {
// 销毁之前要执行的代码
}

注:只有服务器正常关闭时,才会执行destroy()方法,destroy()方法在 Servlet 被销毁之前执行,一般用于释放资源。

二、过滤器

1、概述

当访问服务器的资源时,过滤器可以将请求拦截下来,完成一些功能,如登录验证、统一编码处理等。

2、使用

(1)实现接口

实现javax.servlet.Filter接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package cn.frankfang.filter;

import javax.servlet.*;
import java.io.IOException;

public class FilterDemo1 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

}

@Override
public void destroy() {

}
}
(2)复写方法

在 Filter 接口中定义了以下三个方法:

  • public void init(FilterConfig filterConfig)

web 应用程序启动时,web 服务器将创建Filter 的实例对象,并调用其init方法,读取web.xml配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter对象只会创建一次,init方法也只会执行一次)。开发人员通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。

下面将介绍如何使用其中的FilterConfig类型参数。首先在web.xml文件中添加以下内容:

1
2
3
4
5
6
7
8
<filter>
<filter-name>FilterDemo</filter-name>
<filter-class>cn.frankfang.filter.FilterDemo</filter-class>
<init-param>
<param-name>param1</param-name>
<param-value>value1</param-value>
</init-param>
</filter>

在 init 方法使用 FilterConfig 对象获取参数:

1
2
3
4
5
6
public void  init(FilterConfig config) throws ServletException {
// 获取初始化参数
String value1 = config.getInitParameter("param1");
// 输出初始化参数
System.out.println("参数值: " + value1);
}
  • public void doFilter (ServletRequest request, ServletResponse response, FilterChain filterChain)

该方法完成实际的过滤操作,当客户端请求方法与过滤器设置匹配的URL时,Servlet容器将先调用过滤器的doFilter方法。FilterChain用户访问后续过滤器。

下面是复写 doFilter 方法的一个例子:

1
2
3
4
5
6
7
8
9
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 执行拦截操作
System.out.println("拦截请求");
// 把请求传回过滤链
filterChain.doFilter(servletRequest, servletResponse);
// 在执行Servlet中的方法之后返回这里
System.out.println("已放行");
}

注意:在执行拦截操作之后在没有出现异常或检查通过的情况下需要把请求传回过滤链中,否则客户端将无法接收到正确的响应信息。

  • public void destroy()

Servlet容器在销毁过滤器实例前调用该方法,在该方法中释放Servlet过滤器占用的资源。

下面是复写 destroy 方法的一个例子:

1
2
3
4
@Override
public void destroy() {
// 释放资源
}
(3)配置拦截路径

web.xml中添加以下内容:

1
2
3
4
5
6
7
8
9
<filter>
<filter-name>demo1</filter-name>
<filter-class>cn.frankfang.filter.FilterDemo1</filter-class>
</filter>

<filter-mapping>
<filter-name>demo1</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

自 Servlet 3.0 起开始支持注解配置方式,如果使用注解方式进行配置,则需在自定义的过滤器类上加上@WebFilter注解,并将其value属性或urlPatterns属性赋值为需要进行拦截的路径。若需获取更多关于该注解的信息,请参阅:javax.servlet.annotation.WebFilter

下面对拦截路径的类型进行说明:

  • 具体资源路径:如/index.jsp表示只有访问index.jsp资源时,过滤器才会被执行
  • 拦截目录:如/user/* 表示访问/user下的所有资源时,过滤器都会被执行
  • 后缀名拦截:如*.jsp表示访问所有后缀名为.jsp资源时,过滤器都会被执行
  • 拦截所有资源:/*表示访问所有资源时,过滤器都会被执行
(4)配置拦截方式

除了需要配置拦截路径之外,还需要配置拦截方式。所谓拦截方式,就是指资源被访问的方式,下面将通过表格对所有拦截方式进行介绍:

方式 说明
FORWARD 转发访问资源
INCLUDE 包含访问资源
REQUEST 直接请求资源(默认值)
ASYNC 异步访问资源
ERROR 错误跳转资源

如果使用XML配置方式,需要在web.xml配置文件中添加以下内容:

1
2
3
4
5
<filter-mapping>
<filter-name>demo1</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>

如果使用注解方式,则需将@WebFilter注解的dispatcherTypes为枚举类型DispatcherType中的值,下面给出该枚举的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
package javax.servlet;

public enum DispatcherType {
FORWARD,
INCLUDE,
REQUEST,
ASYNC,
ERROR;

private DispatcherType() {
}
}

3、生命周期

  • 在服务器启动后会创建 Filter 对象,然后调用 init 方法,只执行一次,用于加载资源

  • 在服务器关闭后 Filter 对象被销毁,如果服务器正常关闭,则会执行 destroy 方法,只执行一次,用于释放资源

  • 每一次请求被拦截资源时会执行 doFilter 方法,执行多次

4、过滤器链

Web 应用程序可以根据特定的目的定义若干个不同的过滤器。假设现在有AuthFilterLogFilter两个过滤器,都是对/demo1路径的请求进行拦截,下面将通过一张图来说明这两个过滤器的执行顺序:

过滤器链

客户端首先向服务器发送请求,Web服务器首先对请求的访问路径进行判断,当访问路径为/demo1时,Web容器将多个 Filter 组合为一个过滤器链,过滤器链中各个 Filter 的拦截顺序与它们在web.xml中映射的顺序一致(就是按照<filter-mapping/>定义的顺序,此外如果采用注解的方式,则是按照@WebFilter注解中filterName属性值的字符串比较规则比较,值小的先执行),下面给出相关代码。

LogFilter类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package cn.frankfang.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter(urlPatterns = "/demo1", filterName = "0_LogFilter", dispatcherTypes = DispatcherType.REQUEST)
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 执行拦截操作
System.out.println("0_LogFilter已拦截请求");
// 把请求传回过滤链
filterChain.doFilter(servletRequest, servletResponse);
// 在执行Servlet中的方法之后返回这里
servletResponse.getWriter().print("<h1>Hello ");
System.out.println("0_LogFilter已放行");
}
}

AuthFilter类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package cn.frankfang.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter(urlPatterns = "/demo1", filterName = "1_AuthFilter", dispatcherTypes = DispatcherType.REQUEST)
public class AuthFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 执行拦截操作
System.out.println("1_AuthFilter已拦截请求");
// 把请求传回过滤链
filterChain.doFilter(servletRequest, servletResponse);
// 在执行Servlet中的方法之后返回这里
servletResponse.getWriter().print("World!</h1>");
System.out.println("1_AuthFilter已放行");
}
}

ServletDemo1类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package cn.frankfang.servlet;

import javax.servlet.*;
import java.io.IOException;

@WebServlet("/demo1")
public class ServletDemo1 implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {

}

@Override
public ServletConfig getServletConfig() {
return null;
}

@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
servletResponse.setContentType("text/html;charset=utf-8");
System.out.println("Hello Servlet!");
}

@Override
public String getServletInfo() {
return null;
}

@Override
public void destroy() {

}
}

启动服务器并访问/demo1,则会在控制台中输出以下内容:

控制台输出结果

此外在浏览器中会展示以下内容:

浏览器内容

看到这里或许你就明白了 Filter 的大致原理:Filter 是通过 Java 中的动态代理来实现的,过滤器链本质上就是对 Servlet 实现类进行增强。结合下面这张图可更好的进行理解:

过滤器链原理

三、异常处理

1、声明式

当一个 Servlet 抛出一个异常时,Web容器在使用了exception-type元素的web.xml中搜索与抛出异常类型相匹配的配置。下面将举例说明如何在web.xml文件中采用声明式的异常处理方式。

假如有一个名为ExceptionHandler的 Servlet 在已定义的异常或错误出现时被调用,则在web.xml中需添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!-- 错误处理的Servlet -->
<servlet>
<servlet-name>ExceptionHandler</servlet-name>
<servlet-class>cn.frankfang.exception.ExceptionHandler</servlet-class>
</servlet>

<!-- 路径映射 -->
<servlet-mapping>
<servlet-name>ExceptionHandler</servlet-name>
<url-pattern>/error</url-pattern>
</servlet-mapping>

<!-- 状态码 -->
<error-page>
<error-code>403</error-code>
<location>/error</location>
</error-page>
<error-page>
<error-code>404</error-code>
<location>/error</location>
</error-page>

<!-- 异常类型 -->
<error-page>
<exception-type>javax.servlet.ServletException</exception-type>
<location>/error</location>
</error-page>
<error-page>
<exception-type>java.io.IOException</exception-type>
<location>/error</location>
</error-page>

如果需要对所有的异常有一个通用的错误处理程序,可采用如下写法:

1
2
3
4
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/error</location>
</error-page>

上面介绍了声明式异常处理中web.xml的写法,下面来完善ExceptionHandler这个异常处理的 Servlet 的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package cn.frankfang.exception;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ExceptionHandler extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取信息
Throwable throwable = (Throwable) req.getAttribute("javax.servlet.error.exception");
Integer statusCode = (Integer) req.getAttribute("javax.servlet.error.status_code");
String servletName = (String) req.getAttribute("javax.servlet.error.servlet_name");
String requestUri = (String) req.getAttribute("javax.servlet.error.request_uri");

// 设置响应内容类型
resp.setContentType("text/html;charset=utf-8");

// 输出错误信息
resp.getWriter().println(
"<!DOCTYPE html>\n"
+ "<html>\n"
+ "<head><title>Error Page</title></head>\n"
+ "<body>\n"
+ "<h1>ERROR INFO</h1>\n"
+ "<h2>HTTP STATUS CODE: " + statusCode + "</h2>\n"
+ "<h2>EXCEPTION TYPE: " + throwable.getClass().getName() + "</h2>\n"
+ "<h2>SERVLET NAME: " + servletName + "</h2>\n"
+ "<h2>REQUEST URI: " + requestUri + "</h2>\n"
+ "</body>\n"
+ "</html>"
);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}

上面的代码使用了ServletRequest接口中的getAttribute(String name)方法来获取Servlet的相关属性,下面将通过表格对可获取的属性进行介绍:

属性 解释
javax.servlet.error.status_code 该属性给出状态码,状态码可被存储,并在存储为java.lang.Integer数据类型后可被分析。
javax.servlet.error.exception_type 该属性给出异常类型的信息,异常类型可被存储,并在存储为java.lang.Class数据类型后可被分析。
javax.servlet.error.message 该属性给出确切错误消息的信息,信息可被存储,并在存储为java.lang.String数据类型后可被分析。
javax.servlet.error.request_uri 该属性给出有关 URL 调用 Servlet 的信息,信息可被存储,并在存储为java.lang.String数据类型后可被分析。
javax.servlet.error.exception 该属性给出异常产生的信息,信息可被存储,并在存储为java.lang.Throwable数据类型后可被分析。
javax.servlet.error.servlet_name 该属性给出 Servlet 的名称,名称可被存储,并在存储为java.lang.String数据类型后可被分析。

2、程序式

程序式的异常处理最常用的就是使用try-catch语句块进行异常的捕获并处理,例如在用户上传文件时出现异常时可采用如下写法:

1
2
3
4
5
6
7
8
try {
// 处理上传的文件
} catch(IOException e) {
// 添加错误日志信息
this.getServletContext().log("文件上传失败! 异常类型: " + e.toString());
// 返回状态码和信息
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "IO异常");
}

除了可以在try-catch语句块中直接处理异常,还可以使用RequestDispatcher 处理异常。下面将举例介绍如何使用RequestDispatcher 进行异常处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
int i = 1 / 0;
} catch (ArithmeticException e) {
// 在HttpServletRequest对象中添加属性
req.setAttribute("javax.servlet.error.exception",e);
req.setAttribute("javax.servlet.error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
req.setAttribute("javax.servlet.error.servlet_name", "servletDemo");
req.setAttribute("javax.servlet.error.request_uri",req.getRequestURI());
// 获取RequestDispatcher
RequestDispatcher requestDispatcher = req.getRequestDispatcher("error");
// 请求转发
requestDispatcher.forward(req, resp);
}

可以看到,在代码中使用了RequestDispatcher对象的forward方法进行请求转发,将Servlet信息以及错误信息保存在Request中,并将请求转发到ExceptionHandler这个 Servlet 进行处理,该 Servlet 的内容与上文相同,这里不再赘述。