构建面向对象的应用软件系统框架 第6章
第6章 面向方面编程
6.1 AOP概念
AOP是Aspect Oriented Programming的简写,中文通常译作面向方面编程,其核心内容就是所谓的“横切关注点”。[17]
我们知道,使用面向对象方法构建软件系统,我们可以利用OO的特性,很好的解决纵向的问题,因为,OO的核心概念,如继承等,都是纵向结构的。但是,在软件系统中,往往有很多模块,或者很多类共享某个行为,或者说,某个行为存在于软件的各个部分中,这个行为可以看作是“横向”存在于软件之中,他所关注的是软件的各个部分的一些共有的行为,而且,在很多情况下,这种行为不属于业务逻辑的一部分。例如,操作日志的记录,这种操作并不是业务逻辑调用的必须部分,但是,我们却往往不得在代码中显式进行调用,并承担由此带来的后果(例如,当日志记录的接口发生变化时,不得不对调用代码进行修改)。这种问题,使用传统的OO方法是很难解决的。AOP的目标,便是要将这些“横切关注点”与业务逻辑代码相分离,从而得到更好的软件结构以及性能、稳定性等方面的好处。
![]() |
图6.1
AOP包含以下主要概念[18]:
Ø Aspect方面:一个关注点的模块化,这个关注点实现可能另外横切多个对象。事务管理是J2EE应用中横切关注点中一个很好的例子。
Ø Joinpoint连接点:程序执行过程中明确的点,如方法的调 用或特定的异常被抛出。
Ø Advice通知:在特定的连接点AOP框架执行的动作。各种类型的通知包括“around”、“before”和“throws”通知。通知类型将在下面讨论。许多AOP框架都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器链。
Ø Pointcut切入点:指定一个通知将被引发的一系列连接点 的集合。AOP框架必须允许开发者指定切入点:例如,使用正则表达式。
Ø Introduction引入:添加方法或字段到通知化类。
Ø IsModified接口,来简化缓存。
Ø Target object目标对象:包含连接点的对象。也被用来 引用通知化或代理化对象。
Ø AOP proxy AOP代理: AOP框架创建的对象,包含通知。
Ø Weaving织入:组装方面创建通知化对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他一些纯Java AOP框架, 使用运行时织入。
各种通知类型包括:
Ø Around通知: 包围一个连接点的通知,如方法调用。这是最 强大的通知。Aroud通知在方法调用前后完成自定义的行为。它们负责选择继续执行连接点或直接返回它们自己的返回值或抛出异常来短路执行。
Ø Before通知: 在一个连接点之前执行的通知,但这个通知 不能阻止流程继续执行到连接点(除非它抛出一个异常)。
Ø Throws通知: 在方法抛出异常时执行的通知。
Ø After returning通知: 在连接点正常完成后执行的通知, 例如,如果一个方法正常返回,没有抛出异常。
Around通知是最通用的通知类型。大部分基于拦截器的AOP框架如Nanning和JBoss4只提供 Around通知。
AOP,给我们的软件设计带来了一个新的视角和软件架构方法。使用AOP,我们可以专注于业务逻辑代码的编写,而将诸如日志记录、安全检测等系统功能交由AOP框架,在运行时刻自动耦合进来。
通常,我们可以在如下情景中使用AOP技术:
Ø Authentication 权限
Ø Caching 缓存
Ø Context passing 内容传递
Ø Error handling 错误处理
Ø Lazy loading 懒加载
Ø Debugging 调试
Ø logging, tracing, profiling and monitoring 记录跟踪 优化 校准
Ø Performance optimization 性能优化
Ø Persistence 持久化
Ø Resource pooling 资源池
Ø Synchronization 同步
Ø Transactions 事务
Websharp实现了一个基于.Net的轻量级的AOP框架。
6.2 Websharp AOP的使用
6.2.1.使用AOP实现松散耦合
下面,我们通过一个例子来具体讨论AOP技术的应用。为了更好的说明这个问题,我们会给出部分代码,因此,需要选用一个具体的AOP框架。在这里,我们依然选用Websharp框架来进行说明。在Websharo框架中,我们也实现了一个Aspect的框架。
考虑如下情况:对于应用软件系统来说,权限控制是一个常见的例子。为了得到好的程序结构,通常使用OO的方法,将权限校验过程封装在一个类中,这个类包含了一个校验权限的代码,例如:
public class Security { public bool CheckRight(User currentUser , Model accessModel , OperationType operation) { ……//校验权限 } } |
然后,在业务逻辑过程中进行如下调用:
public class BusinessClass { public void BusinessMethod() { Security s = new Security(); if (!s. CheckRight(……)) { return ; } ……//执行业务逻辑 } } |
这种做法在OO设计中,是常见的做法。但是,这种做法会带来以下一些问题:
1、 不清晰的业务逻辑:从某种意义上来说,权限校验过程并不是业务逻辑执行的一部分,这个工作是属于系统的,但是,在这种情况下,我们不得不把系统的权限校验过程和业务逻辑执行过程掺杂在一起,造成代码的混乱。
2、 代码浪费:使用这种方法,我们必须所有的业务逻辑代码中用Security类,使得同样校验的代码充斥在整个软件中,显然不是很好的现象。
3、 紧耦合:使用这种方法,我们必须在业务逻辑代码中显式引用Security类,这就造成了业务逻辑代码同Security类的紧耦合,这意味着,当Security发生变化时,例如,当系统进化时,需要对CheckRight的方法进行改动时,可能会影响到所有引用代码。下面所有的问题都是因此而来。
4、 不易扩展:在这里,我们只是在业务逻辑中添加了权限校验,哪一天,当我们需要添加额外的功能,例如日志记录功能的时候,我们不得不同样在所有的业务逻辑代码中添加这个功能。
5、 不灵活:有的时候,由于某些特定的需要,我们需要暂时禁止,或者添加某项功能,采用传统的如上述的做法,我们不得不采用修改源代码的方式来实现。
为了解决这些问题,我们通常会采用诸如设计模式等方式,对上面的方案进行改进,这往往需要很高的技巧。利用AOP,我们可以很方便的解决上述问题。
我们以Websharp Aspect为例,看看如何来对上面的代码进行改动,以获得一个更好的系统结构。
首先,Security并不需要做任何修改。
然后,我们对BusinessClass做一个小小的改动:为BusinessClass添加一个名为AspectManaged的Attribute,并使得BusinessClass继承AspectObject,然后,删除代码中对Security的调用,这样,我们的代码就变成了如下的样子:
[AspectManaged(true)] public class BusinessClass : AspectObject { public void BusinessMethod() { ……//执行业务逻辑 } } ……//执行业务逻辑 |
然后,我们为系统增加一个SecurityAspect。
public class SecurityAspect : IAspect { public void Execute(object[] paramList) { if(!Security.CheckRight(......)) { throw new SecurityException("你没有权限!"); } } } |
最后,我们在系统配置文件中添加必要的信息:
<Websharp.Aspects> <Aspect type="MyAPP.SecurityAspect, MyAPP" deploy-model="Singleton" pointcut-type="Method" action-position="before" match="*,*" /> </Websharp.Aspects> |
这样,我们就完成了代码的重构。当BusinessClass被调用的时候,AOP框架会自动拦截BusinessClass的BusinessMethod方法,并调用相应的权限校验方法。
采用这种方式,我们在BusinessClass中没有显式引用Security类及其相应方法,并且,在所有业务逻辑代码中,都没有必要引用Security类。这样,借助AOP机制,我们就实现了BusinessClass和Security类的松散耦合,上面列出的所有问题都迎刃而解了。同时,这也是一种易于扩展的机制,例如,当我们需要添加日志记录功能的时候,只需要添加相应的Aspect类,然后在配置文件中进行配置即可,而无需对业务逻辑代码进行任何改动。
6.2.2.使用AOP组合两个业务逻辑
使用AOP,我们不仅仅可以用来分离系统功能和业务逻辑,就象上面我们做的那样,也可以用来耦合不同的业务逻辑,得到更加灵活的软件结构。下面,我们通过一个具体的案例,来看看怎么通过AOP,组合两个业务逻辑过程。
我们假设有如下一个场景:
我们设计了一个ERP系统,其中,库存管理系统需要同财务系统相交互,例如,当某个库存商品报废的时候,需要有相应的财务处理过程。因此,我们通常需要在库存商品报废的业务逻辑中引用相关的财务处理逻辑。这必然会造成两个部分的耦合。当然,为了使两个部分尽量耦合程度降低,我们通常会使用Façade等设计模式来进行解耦。
由于某些原因,我们需要将库存管理系统单独出售,这就需要我们需要从库存商品报废的业务逻辑中将引用的相关的财务处理逻辑去除,这意味着我们需要修改原有的代码。为了解决这个问题,即可以随时将财务处理逻辑添加或者从库存商品报废的业务逻辑中删除,我们可以采用一些方法,例如,设置一些开关参数,在库存商品报废的业务逻辑中,根据这些开关参数的值,来判断是否需要执行财务处理逻辑。
问题是,这仍旧不是理想的解决方案。采用这种方式,你必须事先知道所有需要设置的开关参数,并且,在业务逻辑代码中添加相应的判断。当为系统增加一个类似的需要灵活处理的部分时,开发人员不得不添加相应的参数,并且修改相应的代码(添加相应的判断代码)。修改代码总是不好的事情,因为按照软件工程的要求,当有新的需求是,尽量不要修改原来的代码,而是新增相应的代码。但是,在这种情况下,你做不到。
使用AOP,我们可以通过一种更加自然的方式来实现这个目标。基本方法如下:
首先,编写相关的库存商品报废业务逻辑,不需要添加任何其他的内容,并且,把这个逻辑的代码设置为可AOP的。
其次,按照正常的方式,编写财务处理逻辑。
添加一个把库存商品报废业务逻辑和财务处理逻辑组合起来的Aspect,这个Aspect可以拦截库存商品报废业务逻辑的执行,动态的加入财务处理逻辑的过程,并且,在配置文件中进行配置。
这样,我们就通过一个Aspect,组合了这两个业务逻辑。并且,我们随时可以通过修改配置文件的方式把财务处理从库存商品报废业务逻辑中去除,而不用修改任何代码。
从上面的例子可以看出,采用AOP的好处是,我们可以独立的编写各个业务逻辑,使得系统各个部分之间的耦合度降到最低,然后,可以在系统中根据需要随时组合两个逻辑,而不用修改原来的任何代码。
6.3 Websharp AOP的实现
应该认识到,完全的AOP实现,需要开发语言的支持。因为对于AOP的研究,还正在进行之中,目前的开发语言,都还没有完全支持AOP的,但是,我们可以利用现有的一些语言功能,来实现AOP的部分功能。
实现AOP的关键,是拦截正常的方法调用,将我们需要额外附加的功能透明的“织入”到这些方法中,以完成一些额外的要求。从总体方法上来说,织入的方法有两大类:静态织入和动态织入。
静态织入方法,一般都是需要扩展编译器的功能,将需要织入的代码,通过修改字节码(Java)或者IL代码(.Net)的方法,直接添加到相应的被织入点;或者,我们需要为原来语言添加新的语法结构,从语法上支持AOP。AspectJ[19]就是采用的这种方式。使用这种方式来实现AOP,其优点是代码执行的效率高,缺点是实现者需要对虚拟机有很深的了解,才能够做到对字节码修改。由于织入方法是静态的,当需要添加新的织入方法时,往往需要重新编译,或者说运行字节码增强器重新执行静态织入的方法。当然,在.Net平台上,我们也可以使用Emit提供的强大功能来实现这一点。另外,字节码增强器带来了很大的不透明性,程序员很难直观的调试增强后的字节码,因此很多程序员总是在心理上抵制这种字节码增强器。
动态织入的方法,具体实现方式就有很多选择了。在Java平台上,可以使用Proxy模式,或者定制ClassLoader来实现AOP功能。在.Net平台上,要实现AOP的动态织入,归纳起来,可以采用以下几种方法:
l 使用ContextAttribute和ContextBoundObject来对对象的方法进行拦截。关于ContextAttribute的具体使用方法,读者可以参考MSDN等相关资料。
l 使用Emit来,在运行时刻动态构建被织入代码后的类,当程序调用被织入类时,实际上调用的是被修改后的类。LOOM使用的就是这种方式,但是,个人认为,LOOM目前的实现非常生硬,其可扩展性和灵活性都不是很好。
l 使用Proxy模式。这也是Websharp的实现方法。
Websharp的实现,是利用了对象代理(Proxy)机制。所谓Proxy,就是“为其他对象提供一种代理以控制对这个对象的访问”。可以用下面的图(图5.2)来表示Proxy模式:
图6.2
关于Proxy模式的更多信息和资料,可以参见注解。
在WebsharpAspect中,当一个对象被标记为AspectManaged后,这个类的实例的创建过程,以及方法的调用会被WebsharpAspect控制。因此,当你在调用如下语句:
BusinessClass bc = new BusinessClass();
的时候,你得到的实际上并不是BusinessClass类的一个实例,而是他的一个代理(关于其中的实现机理,可以参见相关的源代码)。因此,当调用这个“实例”的方法的时候,所有的调用都会被代理所捕获,代理会在实际的方法调用之前,透明的执行一些预定义的操作,然后再执行实际的方法,最后,在实际的方法调用之后,再执行一些预定义的操作。这样,就实现了AOP的功能。
注意,AOP并不仅仅等同于方法调用拦截,当然,这也是最常用的和非常有效的AOP功能。
在Websharp AOP中,定义的主要类和接口如下:
6.3.1 AspectObject抽象类
首先定义了抽象类AspectObject,所有需要AOP管理的类,都必须从这个类继承下来。这个类的定义如下:
public abstract class AspectObject : ContextBoundObject |
之所以定义这个类,并且让它继承ContextBoundObject,其原因是因为, ContextBoundObject的子类,.Net运行环境会为其建立一个绑定的上下文,我们据此可以对其在运行时刻的行为做出一些自定义的控制。AspectObject继承ContextBoundObject,除此之外,没有给AspectObject添加其他属性和方法。实际上,WebsharpAspect能够拦截任何直接从ContextBoundObject派生的类,只所以定义AspectObject,目的是为了将来可能的扩充性。
当某个业务逻辑类需要接受Aspect管理的时候,必须继承AspectObject,并且加上AspectManaged特性。例如:
[AspectManaged(true)] public class BusinessClass : AspectObject { …… } |
关于AspectManaged特性,将来后面说明
6.3.2 IAspect接口
IAspect定义一个方面,这个方面可以在被拦截类的方法的执行之前或之后截获方法的执行,然后,执行相应的Advice通知的方法。Websharp AOP的IAspect方法定义了PreProcess和PostProcess方法,以支持Before和After通知。IAspect的定义如下:
public interface IAspect { void PreProcess(IMessage msg); void PostProcess(IMessage msg); } |
6.3.3 AspectManagedAttribute
这是一个非常关键的类,其作用是拦截类的构造函数。如前所述,当你在执行诸如:
BusinessClass cls=new BusinessClass()
的时候,你实际上得到的不是BusinessClass,而是BusinessClass的一个代理,正因为如此,我们才能够在执行这个对象的方法的时候,把他们拦截下来,插入我们自己的代码。
这个类的定义如下:
[AttributeUsage(AttributeTargets.Class)] [SecurityPermissionAttribute(SecurityAction.Demand, Flags=SecurityPermissionFlag.Infrastructure)] public class AspectManagedAttribute : ProxyAttribute { private bool aspectManaged; public AspectManagedAttribute() { aspectManaged=true; } public AspectManagedAttribute(bool AspectManaged) { aspectManaged=AspectManaged; } } |
在AspectManagedAttribute中,最重要的方法是MarshalByRefObject方法,你必须重载他,当我们拦截构造函数的时候,就会执行这个方法,在这里,我们可以对被拦截的构造函数的类进行一些处理,生成被实例化的类的代理,原理代码如下:
public override MarshalByRefObject CreateInstance(Type serverType) { MarshalByRefObject mobj= base.CreateInstance(serverType); if(aspectManaged) { RealProxy realProxy = new AspectProxy(serverType,mobj); MarshalByRefObject retobj = realProxy.GetTransparentProxy() as MarshalByRefObject; return retobj; } else { return mobj; } } |
在这里,我们为被拦截的类添加了一个名为AspectProxy的代理。这个代理的定义在下面讨论。
6.3.4 定义AspectProxy类
这个类的定义如下:
public class AspectProxy : RealProxy |
这个类就是我们定义的Proxy类,对于方法的织入就是在这里进行的。当被代理的某个对象的方法执行时,就会被代理所拦截,代理会执行Invoke方法。我们所需要额外执行的方面代码就是在这里织入的。这部分的代码如下:
public override IMessage Invoke(IMessage msg) { PreProcess(msg); IMessage retMsg; if (msg is IConstructionCallMessage) { IConstructionCallMessage ccm = (IConstructionCallMessage)msg; RemotingServices.GetRealProxy(target).InitializeServerObject(ccm); ObjRef oRef = RemotingServices.Marshal(target); RemotingServices.Unmarshal(oRef); retMsg = EnterpriseServicesHelper.CreateConstructionReturnMessage (ccm,(MarshalByRefObject)this.GetTransparentProxy()); } else { IMethodCallMessage mcm = (IMethodCallMessage) msg; retMsg = RemotingServices.ExecuteMessage(target, mcm); } PostProcess(msg); return retMsg; } |
可以看到,我们在这里,分别在方法执行前后执行后,进行了一些我们的处理。这些处理,就是根据配置文件,查找匹配的我们在前面定义的IAspect的派生类,并执行相应的Advice通知方法。
6.3.5 其他一些辅助类
上面的一些类完成了我们的AOP框架的主要功能,当然,我们还需要一些辅助来来完成一些其他工作,例如,如何查找匹配的Aspect织入类,如何读取配置文件等,这些类的方法在这里就不一一列举了,可以参见源代码。
6.3.6 配置文件
配置文件的格式定义如下:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="Websharp.Aspects" type="Websharp.Aspect.AspectConfigHandler,Websharp.Aspect" /> </configSections>
<Websharp.Aspects> <Aspect type="WeaveTest.FirstAspect,WeaveTest" deploy-model="None" pointcut-type="Method|Construction|Property" action-position="Both" match="*,Get*" /> </Websharp.Aspects> </configuration>
|
首先,需要在配置文件中添加一个配置节(configSections),在配置节中指明使用读取配置文件的类的细节。在Windows Form程序中,配置文件一般可以是app.config,对于Web项目,可以在Web.config文件中添加配置信息。关于配置文件的其他详细信息,可以参考MSDN中相关的
在<Websharp.Aspects>节中,具体描述Aspect的信息。可以为一个系统添加多个Aspect。
在Aspect配置中,各个属性的说明如下:
u type属性说明Aspect类的类型,采用“Aspect类全称,Assembly”的格式,分别说明Aspect类的类型,以及所在的Assembly。
u deploy-model属性指明Aspect类在运行时刻的行为,可以后Singleton和None两种属性值。当属性值是Singleton的时候,在系统中只有该Aspect类的一个实例,当属性值是None的时候,对于该类的每次调用,都会生成该类的实例。使用Singleton模式,可以得到性能上的好处
u pointcut-type属性,指明该Aspect类拦截点的类型,可以是Method、Construction、Property三种,分别表示拦截方法,构造器和属性。可以使用“|”符号来指明多种类型的拦截点,例如:“Method|Construction”。
u action-position指明拦截方相对于拦截点的位置,可以有Before,After,Both三个值,分别表示相对于拦截点的前面、后面还是前后都来执行Aspect类的方法。
u match指明被拦截类的匹配方式,格式是“名称空间,类名”,例如,“MyNamespace,GetString”指明拦截MyNamespace名称空间下,名称为GetString的方法;又如“*,Get*”指明拦截所有名称空间下以Get开头的方法,他可以拦截诸如GetString、GetName等以Get开头的方法。
6.4 关于AOP和过滤器
在某些开发中,我们可能使用过滤器来完成某些AOP功能。例如,当用户访问某些资源时,我们可以对访问信息进行一些过滤处理。一个常见的场景是,在JSP开发中,为了实现对中文的正确处理,我们通常需要对浏览器同服务器之间传递的数据进行转码处理,以获得正确的文字编码。在每个Request中手工进行转码肯定不是一个好的解决方案。一个比较好的例子,是为应用程序编写一个Filter,自动进行转码处理。例如,我们可以为TOMCAT写如下一个过滤器来实现转码:
public class SetCharacterEncodingFilter implements Filter { public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(“gb2312”); chain.doFilter(request, response); } } |
这样,我们就不必在具体业务处理中来进行手工的转码。实现业务逻辑同转码这样的系统功能的分离。
目前,常见的Web服务器都提供类似的机制。例如,在IIS中,也可以使用过滤器功能。传统的开发方式是使用VC开发一个ISAPI Filter。在.Net发布后,可以使用HttpHandler和HttpModule实现相同的功能,但是,开发难度要低很多[20]。
使用过滤器的另外一个场景,可以是权限控制。例如,在客户请求某个Web页面的时候(这个Web页面通常同某个业务功能相关联),可以使用过滤器截获这个请求,然后,判断这个用户是否具备对请求资源的访问权限。如果是,那么,过滤器可以把这个请求放过去,什么都不做,否则,过滤器可以重定向到某个页面,告诉用户不能访问的原因,或者,直接抛出异常,交由前面的处理者处理。通过这种方式,我们可以同样的分离诸如身份验证这样的系统功能和业务逻辑,实现更好的系统结构。
通过象Web服务器这样的应用程序环境提供的功能,我们还可以实现其他一些AOP的功能,构建更好的系统框架。
6.5 小结
AOP给了我们一个新的视角来看待软件的架构,有的时候,即使不使用AOP技术,只使用AOP的某些观念和现有的技术来搭建系统架构,分离某些本来是紧耦合的关注点,对我们也是非常有益的。