摘要:
AOP的理念可以很容易抽象出横切关注点,基于AOP理念我们可以将责任链模式中各具体处理角色中共同的实现责任链结构的行为抽象出来并将其模块化,以便进一步提高代码复用率和系统可维护性。实际上,无论是Java Web中的过滤器,还是Struts2中的Interceptor,它们都是责任链模式与AOP思想互相融合的巧妙实践。为了更进一步理解AOP (Aspect-Oriented Programming,AOP) 和 CoR (Chain of Responsibility),本文还概述了Filter,并手动模拟了Java Web中的过滤器机制。最后,我们结合Struts2的源码和文档解释了拦截器的工作原理,更进一步剖析了AOP理念和CoR模式在Java中的应用,同时也有助于了解Struts2的原理。
版权声明:
本文原创作者:书呆子Rico
作者博客地址:http://blog.csdn.net/justloveyou_/
友情提示:
关于责任链模式的介绍共分为两篇,上篇《责任链模式综述(基础篇)》主要综述了责任链模式的提出动机、原理结构、典型实现和应用场景,并结合具体实例展现了其灵活性、可插拔性和松耦合性。本篇《责任链模式进阶:与AOP思想的融合与应用》主要引入了AOP理念,并在此基础上进一步将责任链结构的实现用切面抽象出来,使得各个具体处理者只需关注自身必须实现的功能性需求。我们知道,无论是Java Web中的过滤器还是Struts2中的 Interceptor,它们都是责任链模式与AOP思想互相融合的巧妙实践。为了更进一步理解AOP和CoR,本篇概述了Filter的提出动机、工作原理和使用流程,并手动模拟了Java Web中的过滤器机制。此外,我们还结合Struts2的源码和文档解释了拦截器的工作原理,更进一步剖析了AOP理念和CoR模式在Java中的应用,同时也有助于了解Struts2的原理。
为了更好地理解本篇内容,请各位看官先对责任链模式有一个基本的了解或者先阅读本篇的姊妹篇《《责任链模式综述(基础篇)》。
一. AOP思想与责任链模式
面向对象(Object-Oriented) 的分析和设计方法可以将真实世界的实体抽象成类和对象,从而实现了从问题域到软件的转换,这种方法能完美的抽象出问题域中的角色。但是,不同的角色可能有着共同的行为,这种共同的行为被称为 横切关注点。利用面向对象的方法不能很好地抽象出 横切关注点,从而导致了类间耦合度高、代码复用率低(冗余)等问题。面向切面的编程思想(Aspect-Oriented Programming,AOP)就是为了解决 横断关注点的抽象 而诞生的。
AOP的核心思想允许将分散在类中的共同逻辑(行为)分离出来,将OOP不能很好处理地横切关注点抽象在“切面”之中。其中,“切面” 是在AOP思想中引入的一种 新的编程单位,它使得 横切关注点模块化 ,这对现有的设计模式产生了非常重大的影响。
用传统的面向对象方法实现责任链模式虽然能够满足责任链模式要求的一切特征,在应用上也有很多实例,但是仍然存在者一些明显的缺陷和不足。比如,各个请求处理者除了实现自身应当处理的逻辑外还要实现责任链的结构(即successor属性及其Setter),也就是说,责任链的建立和指派包含在实现角色的类中,并没有抽象出来,这直接导致责任链的指派不够灵活。
AOP 思想的精髓能够将横向的关注点分离出来,这大大提高了我们认识世界和抽象世界的能力。实际上,责任链模式的缺陷主要在于具体实现角色的对象中存在着共同的行为——实现责任链结构的行为,而这些行为并没有被抽象出来,而用 AOP 改进责任链模式的关键就是要将责任链结构的实现用切面抽象出来,使得各个对象只关注自身必须实现的功能性需求。实际上,用AOP思想实现责任链模式时仍然保留了 Client,Handler 和 ConcreteHandler 三个角色,不同点是增加了实现责任链的切面,即 HandlerChain,下图反映了融合AOP思想后的责任链模式(以Filter为例)。利用AOP理念来改进责任链模式可以准确地分离出责任链模式中不同角色的共同行为。
实际上,无论是 Java Web 中的 Filter,还是 Struts2 中的 Interceptor,它们都是 责任链模式 与 AOP思想 互相融合的巧妙实践。
二. Filter 概述
在我的专栏《Java Web 成神之路》中,已经对 Java Web 基础内容 JSP/Servlet 内容进行了概述。事实上,Servlet API 还提供了一个重要接口 —— Filter。在我们开发 Web 应用时,若编写的Java类实现了这个接口,那么我们就可以将这个 Java 类称为一个过滤器(Filter)。Filter 可以认为是 Servlet 的一种加强版,它主要用于 对用户请求进行预处理 以及 对服务器响应进行后处理,是个 典型的处理链。下面的图示形象地反映了 Filter 的工作流程。
当客户端发出Web资源的请求时,Web服务器根据应用程序配置文件设置的过滤规则进行检查,若客户请求满足过滤规则,则对客户请求/响应进行过滤(拦截),期间我们可以对请求信息 (请求头和请求数据)进行检查或改动,然后对请求放行以便由过滤链中的其他过滤器进行处理,最后把请求/响应交给请求的Web资源(Servlet)处理。同样地,在这个过程中我们可以修改响应信息,从而完成一定的任务,其工作原理如下图所示(本图来自于博文《JAVA学习篇–javaweb之Filter详解》):
过滤链的好处是,发出请求的客户端并不知道链上的哪一个过滤器将处理这个请求,这使得系统可以在不影响客户端的情况下 动态地重新组织链和分配责任,并且在执行过程中的任何时候都可以打断,只要不执行chain.doFilter()就不会再执行后面的过滤器和请求的内容,这显然可以看作是 非纯责任链模式 的一种典型实现。
更多关于 Java Web 中 过滤器机制 的介绍,请移步我的博客 《Java Web 基础 — Filter 综述》。
三. AOP、责任链模式与 JavaWeb 中的 Filter
实质上,Filter 的实现既体现了AOP的理念,也体现了责任链模式的精髓。AOP的主要的意图是将日志记录、性能统计、安全控制、事务处理、异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非主导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。以处理中文字符乱码问题为例,它并非是业务逻辑的内容却又分布在各个请求处理器中,所以对于这些内容的处理,我们就可以基于AOP的思想将其提取出来(AOP中的切面),使用Filter进行整体设置。这种方式相当于对类中的内容做进一步的抽象,使我们的系统更加灵活,更加能应对变化,也进一步提高了代码复用。
此外,Filter 的实现体现了责任链模式的精髓,即将请求的发送者与请求的处理者解耦,从而使得系统更灵活,也更容易扩展。就像Servlet规范对Filter描述的那样,过滤链是由Servlet容器提供给开发者的一种过滤器调用的视图,过滤器使用过滤链去调用链中的下一个过滤器去处理请求,特别地,如果当前过滤器时过滤链中的最后一个过滤器,过滤链将把它交给相应的资源处理器(Servlet)进行处理。更进一步地说,使用过滤链对请求进行过滤的好处就是,发出请求的客户端并不知道链上的哪一个过滤器将处理这个请求,这使得系统可以在不影响客户端的情况下,动态地重新组织链和分配责任。并且,在执行过程中的任何时候都可以直接返回结果,也就是说,只要不执行 chain.doFilter() 就不会对请求放行,也就不会再执行后面的过滤器和请求的内容。这显然可以看作是 非纯责任链模式 的一种典型实现。
显然,FilterChain 本身就是对责任链切面的抽象,是对传统责任链模式的一个改进,整个 Filter 机制本身也是AOP思想与责任链模式的融合的最佳实践。
特别地,为了大家更好地理解AOP理念和责任链模式在 JavaWeb 中的 Filter 中的应用,我们专门写了一个中文解码过滤器,其实现了对中文乱码的统一处理,无论请求方式是 GET 还是 POST。在这里,Filter 相当于责任链模式中的抽象处理者,而 DecodeFilter 实现了Filter接口,相当于责任链模式中的具体处理者,特别地,ServletAPI 对 FilterChain 的抽象则是 AOP 思想的重要体现,也就是将责任链结构的实现用切面(FilterChain)抽象出来了,准确地分离出责任链模式中不同角色的共同行为(责任链的构建与维护)。
/**
* Description: 使用 Filter 解决 GET/POST 提交的中文乱码
* @author rico
* @created 2017-3-4 上午10:55:06
*/
public class DecodeFilter implements Filter {
/** 指定编码方式,默认 utf-8 (@author: rico) */
private String encoding; // Filter 参数
@Override
public void destroy() {
this.encoding = null;
}
@Override
public void doFilter(ServletRequest req, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
// 重新编码后的请求
HttpServletRequest newReq = null;
// 获取请求方式
String method = request.getMethod();
if ("POST".equalsIgnoreCase(method)) { // POST请求的处理方式
request.setCharacterEncoding(encoding);
newReq = request;
} else { // GET请求的处理方式
// 匿名内部类:最终提供给我们的是一个匿名子类对象
newReq = new HttpServletRequestWrapper(request) { // HttpServletRequest 接口的实现类
// 重写对请求参数所有可能的获取方式
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
if (value != null) {
value = this.transCoding(value);
}
return value;
}
// 重写对请求参数所有可能的获取方式
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) {
return values;
}
for (int i = 0; i < values.length; i++) {
values[i] = this.transCoding(values[i]);
}
return values;
}
// 重写对请求参数所有可能的获取方式
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> map = super.getParameterMap();
Map<String, String[]> result = new HashMap<String, String[]>();
Set<Map.Entry<String, String[]>> entrySet = map.entrySet();
for (Map.Entry<String, String[]> set : entrySet) {
String name = set.getKey();
String[] values = set.getValue();
for (int i = 0; i < values.length; i++) {
values[i] = values[i];
}
result.put(name, values);
}
return result;
}
// 代码重用,对中文字符进行解码
public String transCoding(String value) {
try {
value = new String(value.getBytes("iso-8859-1"),
encoding);
} catch (UnsupportedEncodingException e) {
System.out.println(this.getClass().getName()
+ " 发生转码错误: 从 " + "iso-8859-1" + " 到 "
+ encoding);
e.printStackTrace();
}
return value;
}
};
}
// AOP 思想的重要体现,将请求交给其下家继续进行处理,不纯的责任链模式
chain.doFilter(newReq, response);
}
@Override
public void init(FilterConfig config) throws ServletException {
// 从配置文件获取编码参数
encoding = config.getInitParameter("encoding");
encoding = encoding == null ? "utf-8" : encoding;
}
}
要想让该过滤器起作用,还必须将其配置到 Web 中,即需要在 web.xml 中添加如下描述片段。需要注意的是,当一个Web应用包含多个过滤器时,WEB容器会根据其注册顺序进行调用,也就是说,在web.xml文件中越靠前,越先被调用。因此,若想让该过滤器能够对Struts2应用起作用,则必须将其配置到Struts2过滤器前面。
<!-- 当一个Web应用包含多个过滤器时,根据其注册顺序进行调用:在web.xml文件中越靠前,越先被调用 -->
<!-- 因此,若想让该过滤器能够对Struts2应用起作用,则必须将其配置到Struts2过滤器前面 -->
<filter>
<filter-name>DecodeFilter</filter-name>
<filter-class>cn.edu.tju.rico.filter.DecodeFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>DecodeFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
这样,当客户端发出Web资源的请求时,Web容器就会根据应用程序配置文件设置的过滤规则进行检查,由于我们设置的是对所有请求进行过滤(/*),因此我们可以对请求参数进行解码,然后依次通过过滤器链的其他过滤器,最后把请求交给请求的Web资源(Servlet)处理。需要注意的是,在本案例中,我们只对请求做了预处理,而没有对响应做后处理。这正好印证了J2EE中对FilterChain的描述:“ Filters use the FilterChain to invoke the next filter in the chain, or if the calling filter is the last filter in the chain, to invoke the resource at the end of the chain。”
四. 手动模拟 Java Web 中的过滤器 Filter
为了更好的体会AOP思想和责任链模式,我们下面手动模拟了 Java Web 中的过滤器 Filter 的实现,其所模拟的流程与Filter的作用流程相同,如下图所示。
在本实现中,我们包含一个抽象 Filter,三个具体的 Filter,包括 HTMLFilter,SensitiveFilter 和 FaceFilter; FilterChain 用于对处理链(责任链切面)的抽象。此外,Request 和 Response 用于对请求消息和响应消息的抽象,Client 用于对客户端的抽象,其类图如下所示:
下面给出各抽象模块的具体实现,需要指出的是,本示例参考于马士兵老师对责任链模式的讲解,但对其做了改进,尤其是关于 FilterChain 的设计改进(只需接收 Request 和 Response),使其与 Servlet API 中 FilterChain 相一致。
1、抽象处理者:Filter
public interface Filter {
//每个Filter均为FilterChain的成员, Filter持有FilterChain的引用,以便调用链条中的各处理者
void doFilter(Request request, Response response, FilterChain chain);
}
2、具体处理者:HTMLFilter,SensitiveFilter 和 FaceFilter
// 将请求消息中的"<>"替换成"[]"
public class HTMLFilter implements Filter {
@Override
public void doFilter(Request request, Response response, FilterChain chain) {
// process HTML Tag
String msg = request.getRequest().replace("<", "[").replace(">", "]");
request.setRequest(msg);
chain.doFilter(request, response);
response.setResponse(response.getResponse() + "--->HTMLFilter");
}
}
//将请求消息中的"被就业"替换成"就业"
class SensitiveFilter implements Filter {
@Override
public void doFilter(Request request, Response response, FilterChain chain) {
String msg = request.getRequest().replace("被就业", "就业");
request.setRequest(msg);
chain.doFilter(request, response);
response.setResponse(response.getResponse() + "--->SensitiveFilter");
}
}
// 将请求消息中的":)"替换成"笑脸"
class FaceFilter implements Filter {
public void doFilter(Request request, Response response, FilterChain chain) {
String msg = request.getRequest().replace(":)", "笑脸");
request.setRequest(msg);
chain.doFilter(request, response);
response.setResponse(response.getResponse() + "--->FaceFilter");
}
}
3、过滤链的抽象:FilterChain
// 对过滤链的抽象(横切关注点),是多个过滤器的聚集,本质上,FilterChain 也可以看作是一个大的Filter
public class FilterChain {
List<Filter> filters = new ArrayList<Filter>();
int index = 0;
// 链式编程
public FilterChain addFilter(Filter filter){
filters.add(filter);
return this; // 返回自身
}
public void doFilter(Request request, Response response) {
if(index == filters.size()) return;
Filter filter = filters.get(index);
index++;
filter.doFilter(request, response, this);
}
}
4、请求和响应的抽象:Request 和 Response
// 对请求消息的抽象
public class Request {
// 请求消息
private String request;
public String getRequest() {
return request;
}
public void setRequest(String request) {
this.request = request;
}
}
// 对响应消息的抽象
class Response {
// 响应消息
private String response;
public String getResponse() {
return response;
}
public void setResponse(String response) {
this.response = response;
}
}
5、客户端的抽象:Client
public class Client {
public static void main(String[] args) {
// 待处理消息
String msg = "大家好 :),<script>,敏感,被就业,网络授课没感觉...";
// 设置请求消息
Request request = new Request();
request.setRequest(msg);
// 设置响应消息
Response response = new Response();
response.setResponse("Response");
// 设置处理链
FilterChain chain = new FilterChain();
chain.addFilter(new HTMLFilter()).addFilter(new SensitiveFilter())
.addFilter(new FaceFilter());
// 开始处理
chain.doFilter(request, response);
// 消息的预处理结果
System.out.println(request.getRequest());
// 消息的后处理结果
System.out.println(response.getResponse());
}
}/* Output(完全一致):
大家好 笑脸,[script],敏感,就业,网络授课没感觉...
Response--->FaceFilter--->SensitiveFilter--->HTMLFilter
*///:~
实际上,本示例基本模拟了Java Web 中过滤器的工作流程,也反映了AOP思想和责任链模式的精髓。 对于一个给定的请求消息,我们可以从下图中的方法调用栈中看出,将依次由 HTMLFilter,SensitiveFilter 和 FaceFilter 三者进行预处理,最后再依次由 FaceFilter,SensitiveFilter 和 HTMLFilter 处理(这个可以从输出中看出)。
实际上,FilterChain 本身也可以看作是一个大的Filter,更进步地说,FilterChain 本身也可以实现 Filter 接口,这样做的优点是,我们不但可以在客户端可以任意添加具体的过滤器,还可以添加过滤链;但带来的缺点是将 FilterChain 和 Filter 耦合在了一起,也就是说,FilterChain与Filter的doFilter方法必须一样,而实际上FilterChain的doFilter方法并不需要FilterChain参数。花开生两面,有利就有弊,读者可以尝试让FilterChain 本身实现 Filter 接口,体会一下这个思想。
五. Struts2 中AOP与COR模式的体现:拦截器
拦截器(Interceptor)是Struts2最强大的特性之一,是struts2的核心,实际上我们完全可以把Struts2看作是一个空的容器,而大量的內建拦截器(包括但不仅限于请求参数的解析、 类型转化、异常处理、数据校验,文件上传)完成了该框架的大部分工作。拦截器可以让我们在Action和Result被执行之前或之后进行一些处理(如下图所示),同时它也可以让我们将通用的代码模块化并作为可重用的类,这正是AOP的精髓,也是AOP的一种实现策略。此外,我们知道Struts2的拦截器是 可插拔式的设计,也就是说,若我们需要使用某个拦截器,只需在配置文件中应用该拦截器即可;若我们不需要使用该拦截器,只需要在配置文件中取消该拦截器。最重要的是,无论我们是否应用该拦截器,其对我们的 Struts2 没有任何影响,这正是责任链模式的精髓所在。
1、Struts2 对拦截器的介绍
下面四段文字是Struts2官方对Interceptor的介绍,揭示了拦截器的提出动机、作用原理和配置流程,并折射出其对AOP理念和CoR模式的理解和使用,为保证原汁原味,本人不作进一步翻译,并将原文摘抄如下:
Many Actions share common concerns. Some Actions need input validated. Other Actions may need a file upload to be pre-processed. Another Action might need protection from a double submit. Many Actions need drop-down lists and other controls pre-populated before the page displays.
The framework makes it easy to share solutions to these concerns using an “Interceptor” strategy. When you request a resource that maps to an “action”, the framework invokes the Action object. But, before the Action is executed, the invocation can be intercepted by another object. After the Action executes, the invocation could be intercepted again. Unsurprisingly, we call these objects “Interceptors.”
Interceptors can execute code before and after an Action is invoked. Most of the framework’s core functionality is implemented as Interceptors. Features like double-submit guards, type conversion, object population, validation, file upload, page preparation, and more, are all implemented with the help of Interceptors. Each and every Interceptor is pluggable, so you can decide exactly which features an Action needs to support.
Interceptors can be configured on a per-action basis. Your own custom Interceptors can be mixed-and-matched with the Interceptors bundled with the framework. Interceptors “set the stage” for the Action classes, doing much of the “heavy lifting” before the Action executes.
这四段的介绍正是AOP理念的精髓,也就是对横切关注点的抽象,将分散在类中的共同逻辑(行为)分离出来,将OOP理念不能很好处理地横切关注点抽象在“切面”之中,以增强代码复用和灵活性。当然,这也是 Interceptor 的设计理念。此外,拦截器可插拔的设计背后正是CoR模式的应用。
2、结合源码分析Struts2拦截器背后的原理
我们知道,大部分时候拦截器方法都是通过代理的方式来调用的。当请求到达Struts2的ServletDispatcher时,Struts2会查找配置文件,并根据其配置实例化相对的拦截器对象,然后串成一个列表(list),以便最后由容器依次调用列表中的拦截器作对请求/响应做进一步处理。我们可以在 DefaultActionInvocation 类中找对应的Struts源码如下:
public void init(ActionProxy proxy) {
...
// get a new List so we don‘t get problems with the iterator if someone changes the list
// 将所有拦截器对象串成一个列表,以便容器调用
List<InterceptorMapping> interceptorList = new ArrayList<InterceptorMapping>(proxy.getConfig().getInterceptors());
interceptors = interceptorList.iterator();
}
同时,我们可以通过调试观察到上面所提到的拦截器列表,如下图所示:
事实上,我们之所以能够如此灵活地使用拦截器,完全归功于“动态代理”的使用。动态代理是代理对象根据客户的需求做出不同的处理。也就是说,对客户而言,其只要知道一个代理对象就行了。特别地,在 Struts2 中,当Action请求到来的时候,会由系统的代理生成一个Action的代理对象,由这个代理对象调用Action的execute()或指定的方法,并在struts.xml中查找与该Action对应的拦截器。如果有对应的拦截器,就在Action的方法执行前(后)调用这些拦截器;如果没有对应的拦截器,则直接执行Action的方法。其中Struts2 对拦截器的调用,是通过ActionInvocation来实现的,代码如下:
public String invoke() throws Exception {
String profileKey = "invoke: ";
try {
UtilTimerStack.push(profileKey);
if (executed) {
throw new IllegalStateException("Action has already executed");
}
// 由容器依次调用拦截器链中的各拦截器
if (interceptors.hasNext()) {
final InterceptorMapping interceptor = (InterceptorMapping) interceptors.next();
String interceptorMsg = "interceptor: " + interceptor.getName();
UtilTimerStack.push(interceptorMsg);
try {
// 不断从拦截器栈中取出新的拦截器,并调用拦截器的intercept方法
resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);
}finally {
UtilTimerStack.pop(interceptorMsg);
}
} else {
// 若拦截器栈中所有的拦截器都已调用,则开始调用Action的指定方法进行业务逻辑的处理
resultCode = invokeActionOnly();
}
// 其余代码省略
同时,我们可以通过调试观察到方法调用栈(拦截器调用栈),如下图所示:
总的来说,Struts2 的拦截器又是AOP理念和责任链模式融合的又一次成功实践。
六. 总结
AOP的理念可以很容易抽象出横切关注点,基于AOP理念我们可以将责任链模式中各具体处理角色中共同的实现责任链结构的行为抽象出来并将其模块化,以便进一步提高代码复用率和系统可维护性。实际上,无论是Java Web中的过滤器,还是Struts2中的Interceptor,它们都是责任链模式与AOP思想互相融合的巧妙实践。为了更进一步理解AOP和CoR,本文还概述了Filter的提出动机、工作原理和使用流程,并手动模拟了Java Web中的过滤器机制。最后,我们结合Struts2的源码和文档解释了拦截器的工作原理,更进一步剖析了AOP理念和CoR模式在Java中的应用,同时也有助于了解Struts2的原理。
七. 更多
更多关于 protected 关键字 的介绍, 请移步我的博文《Java 访问权限控制:你真的了解 protected 关键字吗?》。
更多关于 Java Web 中 过滤器机制 的介绍,请移步我的博客 《Java Web 基础 — Filter 综述》。
更多对 责任链模式的提出动机、原理结构、典型实现和应用场景等切面的介绍, 请移步本文的姊妹篇《责任链模式综述(基础篇)》。
引用