1. 概述
在这个教程中,我们将介绍拦截过滤器模式 —— 表现层的核心J2EE设计模式。
这是我们的模式系列 的第二个教程,紧随前一个关于前端控制器模式 的指南。
拦截过滤器是在请求被处理器处理之前或之后触发动作的过滤器。它们代表了Web应用中的中心组件,对所有请求通用,并且可以扩展而不影响现有的处理器。
2. 使用场景
让我们扩展上一章的例子,实现身份验证机制、请求日志记录和访问计数器。此外,我们还想让系统能够以不同的编码方式 交付页面。
这些都属于拦截过滤器的应用场景,因为它们对所有请求都是通用的,并且应该独立于处理器。
3. 过滤器策略
现在,让我们介绍不同的过滤器策略和示例用例。要在Jetty Servlet容器中运行代码,请执行:
$> mvn install jetty:run
3.1. 定制过滤器策略
定制过滤器策略适用于每个需要有序处理请求的场景,即一个过滤器基于执行链中先前过滤器的结果。
这些链通过实现FilterChain
接口并注册多个Filter
类来创建。
当使用具有不同关注点的多个过滤链时,可以在过滤器管理器中将它们组合在一起:
在我们的例子中,访问计数器是通过统计已登录用户的唯一用户名来工作的,这意味着它依赖于身份验证过滤器的结果,因此两个过滤器必须串联起来。
让我们实现这个过滤器链。
首先,我们将创建一个身份验证过滤器,检查是否存在设置为'username'属性的会话,并在没有会话时启动登录过程:
public class AuthenticationFilter implements Filter {
...
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
HttpSession session = httpServletRequest.getSession(false);
if (session == null || session.getAttribute("username") == null) {
FrontCommand command = new LoginCommand();
command.init(httpServletRequest, httpServletResponse);
command.process();
} else {
chain.doFilter(request, response);
}
}
...
}
接下来,我们创建访问计数器过滤器。这个过滤器维护一个独特的用户名哈希集,并在请求中添加一个'counter'属性:
public class VisitorCounterFilter implements Filter {
private static Set<String> users = new HashSet<>();
...
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) {
HttpSession session = ((HttpServletRequest) request).getSession(false);
Optional.ofNullable(session.getAttribute("username"))
.map(Object::toString)
.ifPresent(users::add);
request.setAttribute("counter", users.size());
chain.doFilter(request, response);
}
...
}
然后,我们将实现一个FilterChain
,遍历注册的过滤器并执行doFilter
方法:
public class FilterChainImpl implements FilterChain {
private Iterator<Filter> filters;
public FilterChainImpl(Filter... filters) {
this.filters = Arrays.asList(filters).iterator();
}
@Override
public void doFilter(ServletRequest request, ServletResponse response) {
if (filters.hasNext()) {
Filter filter = filters.next();
filter.doFilter(request, response, this);
}
}
}
为了将我们的组件连接起来,我们将创建一个简单的静态管理器,负责实例化过滤器链、注册其过滤器,并启动它:
public class FilterManager {
public static void process(HttpServletRequest request,
HttpServletResponse response, OnIntercept callback) {
FilterChain filterChain = new FilterChainImpl(
new AuthenticationFilter(callback), new VisitorCounterFilter());
filterChain.doFilter(request, response);
}
}
最后一步是,在FrontCommand
中将我们的FilterManager
作为请求处理序列的常规部分调用:
public abstract class FrontCommand {
...
public void process() {
FilterManager.process(request, response);
}
...
}
3.2. 基础过滤器策略
在本节中,我们将介绍基础过滤器策略,其中所有实现的过滤器都使用一个共同的超类。
这种策略与上一节的自定义策略以及下一节将要介绍的标准过滤器策略 配合良好。
抽象基类可以用来应用属于过滤器链的自定义行为。在我们的示例中,我们将使用它来减少与过滤器配置相关的样板代码和调试日志:
public abstract class BaseFilter implements Filter {
private Logger log = LoggerFactory.getLogger(BaseFilter.class);
protected FilterConfig filterConfig;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("Initialize filter: {}", getClass().getSimpleName());
this.filterConfig = filterConfig;
}
@Override
public void destroy() {
log.info("Destroy filter: {}", getClass().getSimpleName());
}
}
让我们扩展这个基类,创建一个请求日志记录过滤器,它将在下一节中整合进来:
public class LoggingFilter extends BaseFilter {
private static final Logger log = LoggerFactory.getLogger(LoggingFilter.class);
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) {
chain.doFilter(request, response);
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String username = Optional
.ofNullable(httpServletRequest.getAttribute("username"))
.map(Object::toString)
.orElse("guest");
log.info(
"Request from '{}@{}': {}?{}",
username,
request.getRemoteAddr(),
httpServletRequest.getRequestURI(),
request.getParameterMap());
}
}
3.3. 标准过滤器策略
更灵活地应用过滤器的方式是实现标准过滤器策略。这可以通过部署描述符声明,或者自从Servlet规范3.0以来,通过注解实现。
标准过滤器策略允许您在没有明确定义过滤器管理器的情况下将新过滤器插件到默认链中:
请注意,通过注解指定过滤器的执行顺序是不可能的。如果你需要有序执行,你必须坚持使用部署描述符或实现自定义过滤器策略。
让我们实现一个由注解驱动的编码过滤器,同时也使用基础过滤器策略:
@WebFilter(servletNames = {"intercepting-filter"},
initParams = {@WebInitParam(name = "encoding", value = "UTF-8")})
public class EncodingFilter extends BaseFilter {
private String encoding;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
super.init(filterConfig);
this.encoding = filterConfig.getInitParameter("encoding");
}
@Override
public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain) {
String encoding = Optional
.ofNullable(request.getParameter("encoding"))
.orElse(this.encoding);
response.setCharacterEncoding(encoding);
chain.doFilter(request, response);
}
}
在Servlet场景中,如果有一个部署描述符,我们的web.xml
将包含这些额外的声明:
<filter>
<filter-name>encoding-filter</filter-name>
<filter-class>
com.baeldung.patterns.intercepting.filter.filters.EncodingFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>encoding-filter</filter-name>
<servlet-name>intercepting-filter</servlet-name>
</filter-mapping>
让我们选择我们的日志记录过滤器并进行注解,以便被Servlet使用:
@WebFilter(servletNames = "intercepting-filter")
public class LoggingFilter extends BaseFilter {
...
}
3.4. 模板过滤器策略
模板过滤器策略 和基础过滤器策略基本相同,只是它使用基类中声明的模板方法,这些方法必须在实现中重写:
让我们创建一个基类,其中包含两个抽象的过滤方法,它们将在进一步处理之前和之后被调用。
由于这种策略不太常见,我们在示例中并没有使用,具体实现和用例留给你想象:
public abstract class TemplateFilter extends BaseFilter {
protected abstract void preFilter(HttpServletRequest request,
HttpServletResponse response);
protected abstract void postFilter(HttpServletRequest request,
HttpServletResponse response);
@Override
public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
preFilter(httpServletRequest, httpServletResponse);
chain.doFilter(request, response);
postFilter(httpServletRequest, httpServletResponse);
}
}
4. 总结
拦截过滤器模式捕捉了可以独立于业务逻辑发展的跨切关注点。从业务操作的角度来看,过滤器作为预处理或后处理动作的一系列执行。
到目前为止,我们已经看到了拦截过滤器模式可以用不同的策略实现。在实际应用中,这些不同的方法可以结合使用。
如往常一样,您可以在GitHub上的源代码找到这些资源。