9.2 Filter配置

当完成Filter的实现后,就可以开始配置FilterFilter的配置需要如下步骤:

  • 确认哪些资源需要使用这个Filter拦截处理。
  • 配置Filter的初始化参数值,这些参数可以在Filterinit方法中读取到;
  • Filter取一个名称。一般来说,这个名称没有什么特别的含义,但在一些特殊的情况下,这个名字十分有用。例如,要记录Filter的初始化时间,但这个应用中有许多的Filter,这时它就可以用来识别Filter了。

FilterConfig接口

getServletContext方法

FilterConfig接口允许通过它的getServletContext的方法来访问ServletContext

1
ServletContext getServletContext()

getFilterName方法

如果配置了Filter的名字,在FilterConfiggetFilterName中就可以获取Filter的名字。getFilterName的定义如下:

1
java.lang.String getFilterName()

获取Filter的初始化参数

当然,最重要的还是要获取到开发者或者运维给Filter配置的初始化参数。为了获取这些初始化参数,需要用到FilterConfig中的两个方法,第一个方法是getParameterNames

1
2
java.util.Enumeration<java.lang.String> 
getInitParameterNames()

这个方法返回Filter参数名字的Enumeration对象。如果没有给这个Filter配置任何参数,该方法返回的是空的Enumeration对象。
第二个方法是getParameter:

1
2
java.lang.String getInitParameter(
java.lang.String parameterName)

配置Filter的两种方法

有两种方法可以配置Filter:一种是通过WebFilterAnnotation来配置Filter,另一种是通过部署描述来注册。

  • 使用@WebFilter的方法,只需要在Filter的实现类中增加一个注解即可,不需要重复地配置部署描述。当然,此时要修改配置参数,就需要重新构建Filter实现类了。
  • 使用部署描述意味着修改Filter配置只要修改一下文本文件就可以了。

WebFilter的属性

使用@WebFilter,你需要熟悉表9.1中所列出来的参数,这些参数是在WebFilterAnnotation里定义的。所有参数都是可选

属性 描述
asyncSupported Filter是否支持异步操作
description Filter的描述
dispatcerTypes Filter所生效范围
displayName Filter的显示名
filterName Filter的名称
initParams Filter的初始化参数
largeIcon Filter的大图名称
servletName Filter所生效的Servlet名称
smallIcon Filter的小图名称
urlPatterns Filter所生效的URL路径
value Filter所生效的URL路径

举个例子,下述@WebFilter标注配置了一个Filter,该名称为DataCompressionFilter,且适用于所有资源:

1
2
3
4
@WebFilter(
filterName="DataCompressionFilter",
urlPatterns={"/*"}
)

如果使用部署描述中的filterfilter-mapping元素定义,那么它的内容如下:

1
2
3
4
5
6
7
8
9
10
<filter>
<filter-name>DataCompressionFilter</filter-name>
<filter-class>
the fully-qualified name of the filter class
</filter-class>
</filter>
<filter-mapping>
<filter-name>DataCompresionFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

再举个例子,下述的Filter配置,描述了两个初始化参数:

1
2
3
4
5
6
7
8
@WebFilter(
filterName = "Security Filter",
urlPatterns = { "/*"},
initParams = {
@WebInitParam(name = "frequency", value = "1909"),
@WebInitParam(name = "resolution", value = "1024")
}
)

如果使用部署描述中的filterfilter-mapping元素,那么该配置应该为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<filter>
<filter-name>Security Filter</filter-name>
<filter-class>filterClass</filter-class>
<init-param>
<param-name>frequency</param-name>
<param-value>1909</param-value>
</init-param>
<init-param>
<param-name>resolution</param-name>
<param-value>1024</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>DataCompresionFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

关于部署描述将在第13章“部署”中讨论。

9.1 Filter API

接下来几节主要介绍Filter相关的接口,包含FilterFilterConfgFilterChain

Filter生命周期方法

Filter的实现必须继承javax.servlet.Filter接口。这个接口包含了Filter的**3个生命周期方法:initdoFilterdestroy**。

init方法

Servlet容器初始化Filter时,会触发Filterinit方法,一般来说是在应用开始时。也就是说,init方法并不是在该Filter相关的资源使用到的时候才初始化的,而且这个方法只调用一次,用于初始化Filterinit方法的定义如下:

1
void init(FilterConfig filterConfig)

注意
FilterConfig实例是由Servlet容器传入init方法中的。FilterConfig将在后面的章节中讲解。

doFilter方法

Servlet容器每次处理Filter相关的资源时,都会调用该Filter实例的doFilter方法。FilterdoFilter方法包含ServletRequestServletResponseFilterChain这3个参数。
doFilter的定义如下:

1
2
void doFilter(ServletRequest request, ServletResponse response,
FilterChain filterChain)

接下来,说明一下doFilter的实现中访问ServletRequetServletResponse。这也就意味着允许给ServletRequest增加属性或者增加Header。当然也可以修饰ServletRequest或者ServletRespone来改变它们的行为。在第10章中,“修饰RequestsResponses”中将会有详细的说明。

FilterdoFilter的实现中,最后一行需要调用FilterChain中的doChain方法。注意FilterdoFilter方法里的第3个参数,就是filterChain的实例:

1
filterChain.doFilter(request, response)

一个资源可能需要被多个Filter关联到(更专业一点来说,这应该叫作Filter链条),这时Filter.doFilter()的方法将触发Filter链条中下一个Filter。只有在Filter链条中最后一个Filter里调用的FilterChain.doFilter(),才会触发处理资源的方法。
如果在Filter.doFilter()的实现中,没有在结尾处调用FilterChain.doFilter()的方法,那么该Request请求中止,后面的处理就会中断。
注意:
FilterChaing接口中,唯一的方法就是doFilter。该方法与Filter中的doFilter的定义是不一致的:在FilterChaing中,doFilter方法只有两个参数,但在Filter中,doFilter方法有三个参数。

destroy方法

Filter接口中,最后一个方法是destroy,它的定义如下:

1
void destroy()

该方法在Servlet容器要销毁Filter时触发,一般在应用停止的时候进行调用。
除非Filter在部署描述中被多次定义到,否则**Servlet容器只会为每个Filter创建单一实例**。由于Serlvet/JSP的应用通常要处理用户并发请求,此时Filter实例需要同时被多个线程所关联到,因此需要非常小心地处理多线程问题。关于如何处理线程安全问题的例子,可以参考9.5节“下载计数Filter”。

第9章 Filters

Filter是拦截Request请求的对象在用户的请求访问资源前处理ServletRequest以及ServletResponse,它可用于日志记录、加解密、Session检查、图像文件保护等。通过Filter可以拦截处理某个资源或者某些资源Filter的配置可以通过Annotation或者部署描述来完成。当一个资源或者某些资源需要被多个Filter所使用到,且它的触发顺序很重要时,只能通过部署描述来配置

8.5 小结

本章,我们学习了Servlet API提供的多个监听器类型。这些监听器可以分成三类:application范围、session范围和request范围。监听器的使用很简单,可以通过两种方式注册监听器:在实现类上使用@WebListener注解或者在部署描述文件中增加listener元素。
Servlet 3.0新增了一个监听器接口javax.servlet.AsyncListener,我们将在第11章中进行介绍。

8.4.2 ServletRequestAttributeListener

当一个ServletRequest范围的属性被添加、删除或替换时,ServletRequestAttributeListener接口会被调用。ServletRequestAttributeListener接口提供了三个方法:attributeAddedattributeReplacedattributeRemoved。如下所示:

1
2
3
void attributeAdded(ServletRequestAttributeEvent event)
void attributeRemoved(ServletRequestAttributeEvent event)
void attributeReplaced(ServletRequestAttributeEvent event)

这些方法都可以从参数列表中获得一个继承自ServletRequestEventServletRequestAttributeEvent对象。通过ServletRequestAttributeEvent类提供的getNamegetValue方法可以访问到属性的名称和值:

1
2
java.lang.String getName()
java.lang.Object getValue()

8.4 ServletRequestListeners

ServletRequest范围的监听器接口有三个:ServletRequestListenerServletRequestAttributeListenerAsyncListener。前两个接口会在本节进行介绍,而AsyncListener接口则会在第11章中进行介绍。

8.4.1 ServletRequestListener

ServletRequestListener监听器会对ServletRequest的创建和销毁事件进行响应。容器会通过一个池来存放并重复利用多个ServletRequestServletRequest的创建时刻是从容器池里被分配出来的时候,而它的销毁时刻是放回容器池里的时间。
ServletRequestListener接口有两个方法,requestInitializedrequestDestroyed

1
2
void requestInitialized(ServletRequestEvent event)
void requestDestroyed(ServletRequestEvent event)

当一个ServletRequest创建(从容器池里取出)时,requestInitialized方法会被调用,当ServletRequest销毁(被容器回收)时,requestDestroyed方法会被调用。这两个方法都会接收到一个ServletRequestEvent对象,可以通过使用这个对象的getServletRequest方法来获取ServletRequest对象:

1
ServletRequest getServletRequest()

另外,ServletRequestEvent接口也提供了一个getServletContext方法来获取ServletContext,如下所示:

1
ServletContext getServletContext()

实例

app08a项目里的PerfStatListener类为例。这个监听器用来计算每个ServletRequest从创建到销毁的生存时间。

PerfStatListener实现了ServletRequestListener接口,来计算每个HTTP请求的完成时间。由于容器在请求创建时会调用ServletRequestListenerrequestInitialized方法,在销毁时会调用requestDestroyed,因此很容易就可以计算出时间。只需要在记录下两个事件的事件,并且相减,就可以计算出一次HTTP请求的完成时间了。

PerfStatListener

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
package app08a.listener;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
@WebListener
public class PerfStatListener
implements
ServletRequestListener
{
@Override
public void requestInitialized(ServletRequestEvent sre)
{
ServletRequest servletRequest =
sre.getServletRequest();
servletRequest.setAttribute("start",
System.nanoTime());
}
@Override
public void requestDestroyed(ServletRequestEvent sre)
{
ServletRequest servletRequest =
sre.getServletRequest();
Long start =
(Long) servletRequest.getAttribute("start");
Long end = System.nanoTime();
HttpServletRequest httpServletRequest =
(HttpServletRequest) servletRequest;
String uri = httpServletRequest.getRequestURI();
System.out.println("time taken to execute "
+ uri + ":"+ ((end - start) / 1000)
+ " microseconds");
}
}

方法说明

requestInitialized方法

requestInitialized 方法调用System.nanoTime()获取当前系统时间的数值(Long类型),并将这个数值保存到ServletRequest中:

1
2
3
4
5
6
7
8
@Override
public void requestInitialized(ServletRequestEvent sre)
{
ServletRequest servletRequest =
sre.getServletRequest();
servletRequest.setAttribute("start",
System.nanoTime());
}

nanoTime返回一个long类型的数值来表示任意时间。这个数值和系统或是时钟时间都没什么关系,但是同一个JVM上调用两次nanoTime得到的数值可以计算出两次调用之间的时间。

requestDestroyed方法

requestDestroyed方法中再次调用nanoTime方法,并且减去第一次调用获得的数值,就得到HTTP请求的完成时间了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void requestDestroyed(ServletRequestEvent sre)
{
ServletRequest servletRequest =
sre.getServletRequest();
Long start =
(Long) servletRequest.getAttribute("start");
Long end = System.nanoTime();
HttpServletRequest httpServletRequest =
(HttpServletRequest) servletRequest;
String uri = httpServletRequest.getRequestURI();
System.out.println("time taken to execute "
+ uri + ":"+ ((end - start) / 1000)
+ " microseconds");
}

运行效果

调用app08a应用中的countries.jsp页面,在控制台中可以看到PerfStatListener的运行效果,下面是几次调用时控制台的输出:

1
2
3
4
time taken to execute /app08a/countries.jsp:31243 microseconds
time taken to execute /app08a/countries.jsp:6671 microseconds
time taken to execute /app08a/countries.jsp:4701 microseconds
time taken to execute /app08a/countries.jsp:4242 microseconds

8.3.4 HttpSessionBindingListener

当有属性绑定或者解绑到HttpSession上时,HttpSessionBindingListener监听器会被调用。如果对HttpSession属性的绑定和解绑动作感兴趣,就可以实现HttpSessionBindingListener来监听。例如可以在HttpSession属性绑定时更新状态,或者在属性解绑时释放资源。

实例

Product类

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
42
43
44
45
46
47
package app08a.model;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
public class Product
implements
HttpSessionBindingListener
{
private String id;
private String name;
private double price;
public String getId()
{
return id;
}
public void setId(String id)
{
this.id = id;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public double getPrice()
{
return price;
}
public void setPrice(double price)
{
this.price = price;
}
@Override
public void valueBound(HttpSessionBindingEvent event)
{
String attributeName = event.getName();
System.out.println(attributeName + " valueBound");
}
@Override
public void valueUnbound(HttpSessionBindingEvent event)
{
String attributeName = event.getName();
System.out.println(attributeName + " valueUnbound");
}
}

这个监听器会在HttpSession属性绑定和解绑时在控制台打印信息。

8.3.3 HttpSessionActivationListener

在分布式环境下,会用多个容器来进行负载均衡,有可能需要将session保存起来,在容器之间传递。例如当一个容器内存不足时,会把很少用到的对象转存到其他容器上。这时候,容器就会通知所有HttpSessionActivationListener接口的实现类来处理。

HttpSessionActivationListener接口有两个方法,sessionDidActivatesessionWillPassivate

1
2
void sessionDidActivate(HttpSessionEvent event)
void sessionWillPassivate(HttpSessionEvent event)

HttpSession被转移到其他容器之后,sessionDidActivate方法会被调用。容器将一个HttpSessionEvent对象作为参数传递到方法里,可以从这个HttpSessionEvent对象获得HttpSession
当一个HttpSession将要失效时,容器会调用sessionWillPassivate方法。和sessionDidActivate方法一样,容器将一个HttpSessionEvent方法传递到方法里,可以从这个对象获得HttpSession

8.3.2 HttpSessionAttributeListener

HttpSessionAttributeListener接口和ServletContextAttributeListener类似,它响应的是HttpSession范围属性的添加删除替换
HttpSessionAttributeListener接口有以下方法:

1
2
3
void attributeAdded(HttpSessionBindingEvent event)
void attributeRemoved( HttpSessionBindingEvent event)
void attributeReplaced( HttpSessionBindingEvent event)
  • attributeAdded方法在一个HttpSession范围属性被添加时被容器调用。
  • attributeRemoved方法在一个HttpSession范围属性被删除时被容器调用。
  • attributeReplaced方法在一个HttpSession范围属性被新的替换时被容器调用。

这三个方法都能从参数列表中获取到一个HttpSessionBindingEvent的对象,通过这个对象可以获取属性的名称和值:

1
2
java.lang.String getName()
java.lang.Object getValue()

由于HttpSessionBindingEventHttpSessionEvent的子类,因此也可以在HttpSessionAttributeListener实现类中获得HttpSession

8.3 SessionListeners

一共有四个HttpSession相关的监听器接口:HttpSessionListener,HttpSessionActivationListenerHttpSessionAttributeListenerHttpSessionBindingListener。这四个接口都在javax.servlet.http包中,下面分别对它们进行介绍。

8.3.1HttpSessionListener

当一个HttpSession创建或者销毁时,容器都会通知所有的HttpSessionListener监听器,HttpSessionListener接口有两个方法:sessionCreatedsessionDestroyed

1
2
void sessionCreated(HttpSessionEvent event)
void sessionDestroyed(HttpSessionEvent event)

这两个方法都可以接收到一个继承于java.util.EventHttpSessionEvent对象。可以通过调用形参列表中的HttpSessionEvent对象的getSession方法来获取当前的HttpSessiongetSession方法如下:

1
HttpSession getSession()

实例

app08a应用中的SessionListener类。这个监听器来统计HttpSession的数量。它使用了一个AtomicInteger对象来统计,并且将这个对象保存成ServletContext范围的属性。每当有一个HttpSession被创建时,这个AtomicInteger对象就会加一。每当有一个HttpSession被销毁时,这个AtomicInteger对象就会减一。所以这个对象会保存着当前存活的HttpSession数量。这里使用了AtomicInteger 来代替Integer类型是为了保证能同步进行加减的操作。

SessionListener类

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package app08a.listener;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
@WebListener
public class SessionListener
implements
HttpSessionListener,
ServletContextListener
{
// 项目启动时创建ServletContext对象时调用
@Override
public void contextInitialized(ServletContextEvent sce)
{
ServletContext servletContext =
sce.getServletContext();
servletContext.setAttribute("userCounter",
new AtomicInteger());
}
// 项目销毁时销毁ServletContext对象时调用
@Override
public void contextDestroyed(ServletContextEvent sce)
{}
// 一个浏览器第一次访问该项目时会创建一个对应的Session内置对象
// 创建Session对象时调用该方法
@Override
public void sessionCreated(HttpSessionEvent se)
{
HttpSession session = se.getSession();
ServletContext servletContext =
session.getServletContext();
AtomicInteger userCounter =
(AtomicInteger) servletContext
.getAttribute("userCounter");
//加1
int userCount = userCounter.incrementAndGet();
System.out.println("userCount incremented to :"
+ userCount);
}
//Session内置对象被销毁时调用
@Override
public void sessionDestroyed(HttpSessionEvent se)
{
HttpSession session = se.getSession();
ServletContext servletContext =
session.getServletContext();
AtomicInteger userCounter =
(AtomicInteger) servletContext
.getAttribute("userCounter");
//减1
int userCount = userCounter.decrementAndGet();
System.out.println("----------"
+ " userCount decremented to:"
+ userCount);
}
}

SessionListener类实现了ServletContextListenerHttpSessionListener接口。所以需要实现这两个接口的所有方法。

contextInitialized方法说明

contextInitialized方法创建了一个AtomicInteger对象并将其保存在ServletContextuserCounter属性中。由于是在应用启动的时候创建,因此这个AtomicInteger对象的初始值为0

1
2
3
4
5
6
7
8
9
// 项目启动时创建ServletContext对象时调用
@Override
public void contextInitialized(ServletContextEvent sce)
{
ServletContext servletContext =
sce.getServletContext();
servletContext.setAttribute("userCounter",
new AtomicInteger());
}

sessionCreated方法说明

sessionCreated方法在每个HttpSession创建时被调用。当有HttpSession创建时,从ServletContext中获取userCounter属性。然后调用userCounterincrementAndGet 方法让计数加一。最后在控制台将userCounter的值打印出来,可以直观地看到效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建Session对象时调用该方法
@Override
public void sessionCreated(HttpSessionEvent se)
{
HttpSession session = se.getSession();
ServletContext servletContext =
session.getServletContext();
AtomicInteger userCounter =
(AtomicInteger) servletContext
.getAttribute("userCounter");
//加1
int userCount = userCounter.incrementAndGet();
System.out.println("userCount incremented to :"
+ userCount);
}

sessionDestroyed方法说明

sessionDestroyed方法会在HttpSession销毁之前被调用。这个方法的实现和sessionCreated类似,只不过对userCounter改为减一操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Session内置对象被销毁时调用
@Override
public void sessionDestroyed(HttpSessionEvent se)
{
HttpSession session = se.getSession();
ServletContext servletContext =
session.getServletContext();
AtomicInteger userCounter =
(AtomicInteger) servletContext
.getAttribute("userCounter");
//减1
int userCount = userCounter.decrementAndGet();
System.out.println("----------"
+ " userCount decremented to:"
+ userCount);
}

运行效果

可以通过不同的浏览器访问countries.jsp页面来查看监听器的效果,下面是countries.jsp的访问URL
http://localhost:8080/app08a/countries.jsp
第一次访问时,Java IDE控制台会打印如下信息:

1
userCount incremented to :1

用同一个浏览器再次访问这个URL并不会改变userCounter,因为这属于同一个HttpSession。使用不同的浏览器访问才能增加userCounter的值。这时候控制台打印内容如下:

1
2
userCount incremented to :1
userCount incremented to :2

如果你有时间等待HttpSession过期的话,在控制台也能看到HttpSession销毁时打印的信息,如下所示:

1
2
---------- userCount decremented to:1
---------- userCount decremented to:0