关于spring.net的面向切面编程 (Aspect Oriented Programming with Spring.NET)-使用工厂创建代理(Using the ProxyFactoryObject to create AOP proxies)

本文翻译自Spring.NET官方文档Version 1.3.2

受限于个人知识水平,有些地方翻译可能不准确,但是我还是希望我的这些微薄的努力能为他人提供帮助。

侵删。

如果你正在为你的业务模型使用IoC容器——这是个好主意——你将会想使用某个 Spring.NET's AOP特定的IFactoryObject 的实现(要记住,一个工厂的实例提供了一个间接层,使这个工厂能够创建不同类型的对象—5.3.9节,“通过使用其他类型和实例创建一个对象”)。

一个基本的创建Spring.NET's AOP代理的方式是使用Spring.Aop.Framework.ProxyFactoryObject类。这种做法能够让你能灵活配置切入点的顺序,切入点的应用和将会应用到你的业务中的通知。然而,如果你不需要这种这种配置,有其他的更简便的方法生成代理。

代理基础

和其他的Spring.NET IFactoryObject 的实现一样,ProxyFactoryObject引入了一个中间层。如果你定义了一个ProxyFactoryObject实例,名字叫做foo,那么对foo并不是ProxyFactoryObject 这个代理对象的引用,而是ProxyFactoryObject这个实例的的GetObject() 方法产生的对象。这个方法会包装目标对象然后产生一个AOP代理。

使用ProxyFactoryObject 或者其他的IoC感知类创建AOP代理的最大好处就是我们可以使用IoC来管理通知和切入点。 这个是一个强大的特性,可以完成其他AOP框架不能实现的一些功能。例如,一个通知可能关联到一个应用程序中所有实体(不仅仅是AOP框架中的目标实体),这个得益于依赖注入带来的可拔插性。

代理工厂属性

和Spring.NET中大多的IFactoryObject实现一样,ProxyFactoryObject 也是一个可以灵活配置的Spring.NET实体。它的配置主要用来:

  • 明确需要被代理的目标对象。
  • 明确哪些通知会被应用在代理中。

一些关键的属性继承自Spring.Aop.Framework.ProxyConfig 类:这个类是所有Spring.NET中AOP代理工厂的父类。一些关键的配置包括:

  • 是否直接代理对象(ProxyTargetType):如果目标类被直接代理的话,这个boolean值会设置成true,如果只是代理这个目标类的接口的话,就设置成false。
  • 是否优化(Optimize):是否使用一种激进的优化方式来创建代理。除非你明白相关的AOP代理如何处理优化过程,不然请不要使用优化。这种优化的真正意义在于它能够通过权衡代理的生成事件和运行时的性能,针对性地产生代理。在某些特定的代理实现中,这个优化会被关闭;也会因为其他配置属性,比如说ExposeProxy而静默关闭。
  • 是否冻结通知的改变(IsFrozen):代理工厂配置后,通知就不允许改变。默认为false。
  • 是否暴露代理(ExposeProxy):当前的代理(CurrentProxy)能不能通过AopContext获取,一旦被AopContext获取就也能被目标对象获得(也可以通过IMethodInvocation获取而不需要AopContext )。如果一个目标对象需要获得它的代理并且ExposeProxy 被设置成true,目标对象就能使用AopContext.CurrentProxy属性来获取当前的代理。
  • AopProxyFactory:当生成代理的时候指定IAopProxyFactory的实现。提供了一个可以定制化的生成方法:是否通过远程代理(remoting proxies),IL代码生成或者其他方式生成代理。默认的实现是使用IL代码来生成代理。

其他在ProxyFactoryObject 类中定义的属性包括:

    • 被代理接口(ProxyInterfaces):一个我们要代理的类型名称的字符串数组。
    • 切面名称(InterceptorNames):被应用的IAdvisor,切面或者其他通知名称的字符串数组。它们的顺序是很重要的,排在前面的会优先。列表中的第一个切面的方法调用会被第一个拦截(假设只是配置了一般方法拦截和前置通知)

这些名称都是存在于当前容器中的对象名称,同时也包括容器的所有父类包含的对象。你不可以直接声明容器外的源对象,因为这么做会导致ProxyFactoryObject 忽略通知的单例设置。

  • 引入通知名称(IntroductionNames):容器中可以被用来当作目标的引入通知的实体名称。如果通过名字关联的实体没有实现IIntroductionAdvisor 接口,它就会被传入到默认的DefaultIntroductionAdvisor 构造器中,这样这个实体所有的接口都会被引入的目标实体中。让实体实现IIntroductionAdvisor 接口主要是为了提供一个更好的层次让你去控制是否想暴露特定接口和那些类型想被匹配。
  • 是否单例实现(IsSingleton):指定工厂是否返回一个代理的单例对象,无论GetObject() 被调用多少次。有一些IFactoryObject 提供这这种方法。这个配置的默认值是true。如果你想每一个实例都产生一个代理,那么就把IsSingleton设置成false,同时IsFrozen也要设置成false。如果你想使用一个有状态的通知(stateful advice)——例如状态的混入(stateful mixins)——你就需要使用原形通知(prototype advices )并且把IsSingleton设置成false。

代理接口

让我们看一个简单的ProxyFactoryObject 的例子,这个例子包括:

  • 一个被代理目标实体“personTarget”。
  • 用来提供通知的IAdvisor和IInterceptor 接口。
  • 一个AOP代理实体定义,定义了目标对象(personTarget),要被代理的接口和要应用的通知。
 1 <object id="personTarget" type="MyCompany.MyApp.Person, MyCompany">
 2     <property name="name" value="Tony"/>
 3     <property name="age" value="51"/>
 4 </object>
 5 <object id="myCustomInterceptor" type="MyCompany.MyApp.MyCustomInterceptor, MyCompany">
 6     <property name="customProperty" value="configuration string"/>
 7 </object>
 8 <object id="debugInterceptor" type="Spring.Aop.Advice.DebugAdvice, Spring.Aop">
 9 </object>
10 <object id="person" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">
11     
12     <property name="proxyInterfaces" value="MyCompany.MyApp.IPerson"/>
13     <property name="target" ref="personTarget"/>
14     <property name="interceptorNames">
15         <list>
16             <value>debugInterceptor</value>
17             <value>myCustomInterceptor</value>
18         </list>
19     </property>
20 </object>

要注意的是,InterceptorNames 属性是一个字符串列表:位于当前上下文的切面名称。通知器,拦截器,前置通知,返回后通知和抛出通知实体都能被应用进去。它们的顺序是很重要的。

你可能会奇怪为什么这个列表不是实体的引用。其原因是如果ProxyFactoryObject的singleton属性设置成false,它必须能够针对每个被代理的实体都返回独立的代理实例。如果有任何的通知器是一个原型,一个独立的实例需要被返回,所以没有必要再从上下文获取一个原型的实例,因为仅仅使用同一个引用是明显不够的。

上文中“person”对象的定义可以用一个Iperson接口的实现来代替,如下:

IPerson person = (IPerson) factory.GetObject("person");

其他在同一个IoC上下文的实体可以表达一个强类型的依赖注入,和其他普通的.NET实体一样:

1 <object id="personUser" type="MyCompany.MyApp.PersonUser, MyCompany">
2     <property name="person" ref="person"/>
3 </object>

这里例子中的PersonUser 类会暴露IPerson接口的一些属性。AOP代理会被用来代替“真正的”person的实现。因而这个类型会是一个代理类型。它可以转换成一个IAdvised接口的实现(下文会讨论)。

可以使用内联对象(inline object)来把目标和目标的代理的区别隐藏起来,就像下面这样。(更多关于内联对象的信息可查看5.3.2.3小节, “Inner objects“) 只有ProxyFactoryObject 的定义是不一样的;通知也包含在内,仅仅为了完整性需要:

 1 <object id="myCustomInterceptor" type="MyCompany.MyApp.MyCustomInterceptor, MyCompany">
 2     <property name="customProperty" value="configuration string"/>
 3 </object>
 4 <object id="debugInterceptor" type="Spring.Aop.Advice.DebugAdvice, Spring.Aop">
 5 </object>
 6 <object id="person" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">
 7     
 8     <property name="proxyInterfaces" value="MyCompany.MyApp.IPerson"/>
 9     <property name="target">
10       <!-- Instead of using a reference to target, just use an inline object -->
11       <object type="MyCompany.MyApp.Person, MyCompany">
12         <property name="name" value="Tony"/>
13         <property name="age" value="51"/>
14       </object>
15     </property>
16     <property name="interceptorNames">
17         <list>
18             <value>debugInterceptor</value>
19             <value>myCustomInterceptor</value>
20         </list>
21     </property>
22 </object>

这样做的一个优点是仅仅存在一个Person类型的对象:当我们想防止应用程序上下文的使用者获得一个没有被通知的对象的引用,或者想避免Spring IoC 的自动装配造成的模棱两可的定义的时候是很用用的。还有一个有争议的优点是如果这么做,那么ProxyFactoryObject 的定义是独立完整的。然而有些时候能够从工厂中获得没有被通知的目标对象也许也是一个好处:例如在一些特定的测试环境中。

一个实例一个代理方式实现应用通知

让我们来看一个通过ProxyFactoryObject配置代理对象的例子。

1  <!-- create the object to reference -->
2           <object id="RealObjectTarget" type="MyRealObject" singleton="false"/>
3           <!-- create the proxied object for everyone to use-->
4           <object id="MyObject" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">
5           <property name="proxyInterfaces" value="MyInterface" />
6           <property name="isSingleton" value="false"/>
7           <property name="targetName" value="RealObjectTarget" />
8           </object>
9  

如果你使用一个原型来作为目标你必须把TargetName属性设置成你对象的名字/id而不能使用一个目标引用对象的属性。这样做接下来能够通过一个新的目标原型实例生成一个对应的新代理。

考虑上面的Spring.Net配置的时候,要注意ProxyFactoryObject 的IsSingleton 属性是设置成false的。这就意味着每一个代理对象都是独一无二的。这样你就可以使用下面的方式去配置配置每一个代理对象的通知类型:

1 // Will return un-advised instance of proxy object
2 MyInterface myProxyObject1 = (MyInterface)ctx.GetObject("MyObject");
3 // myProxyObject1 instance now has an advice attached to it.
4 IAdvised advised = (IAdvised)myProxyObject1;
5 advised.AddAdvice( new DebugAdvice() );
6 // Will return a new, un-advised instance of proxy object
7 MyInterface myProxyObject2 = (MyInterface)ctx.GetObject("MyObject");

代理类

当你想代理一个类,而不是一个或者多个接口的时候,应该怎么办呢?

想象一下上面的例子,如果不存在IPerson 接口,我们要通知一个没有实现任何接口的Person类。在这种情况下,如果找不到任何实现的接口的话,ProxyFactoryObject 会代理所有的公共的虚方法和虚属性。我们可以通过设置ProxyTargetType为true来强制Spring.NET 去代理类而不是接口。

类的代理是通过在运行时产生一个目标的子类来实现的。Spring.NET 配置这个子类来委托对原目标对象方法的调用:这个子类被用来实现这种装饰器模式,将通知织入。

类的代理应该对使用者透明。然而需要考虑一个重要问题:非虚的方法不能被通知,就像它们不能被重写一样。这个可能在一些通常不会把方法声明成虚方法的类里面有些限制。

精简的代理定义

尤其是在定义一个事务代理的时候,如果你没有使用事务名称空间,你最终可能会碰到很多类似的代理定义。使用父子对象定义,再加上一些内联对象定义,可以使得代理定义更加精简。

首先,需要为这个代理定义一个父对象模板:

1 <object id="txProxyTemplate"  abstract="true"
2             type="Spring.Transaction.Interceptor.TransactionProxyFactoryObject, Spring.Data">
3         <property name="PlatformTransactionManager" ref="adoTransactionManager"/>
4         <property name="TransactionAttributes">
5             <name-values>
6                 <add key="*" value="PROPAGATION_REQUIRED"/>
7             </name-values>
8         </property>
9     </object>

这样它无法实例化,所以实际上配置还尚未完成。接下来每个需要被创建的代理都是一个子对象定义,这个定义把目标代理包装成一个内联对象,因为目标对象永远不会被自己使用:

1 <object name="testObjectManager" parent="txProxyTemplate">
2         <property name="Target">
3             <object type="Spring.Data.TestObjectManager, Spring.Data.Integration.Tests">
4                 <property name="TestObjectDao" ref="testObjectDao"/>
5             </object>
6         </property> 
7 </object>

当然也可以在子对象中重写父类模板的属性,就像下面的事务传播配置(transaction propagation settings)案例:

 1 <object name="testObjectManager" parent="txProxyTemplate">
 2         <property name="Target">
 3             <object type="Spring.Data.TestObjectManager, Spring.Data.Integration.Tests">
 4                 <property name="TestObjectDao" ref="testObjectDao"/>
 5             </object>
 6         </property> 
 7         <property name="TransactionAttributes">
 8             <name-values>
 9                 <add key="Save*" value="PROPAGATION_REQUIRED"/>
10                 <add key="Delete*" value="PROPAGATION_REQUIRED"/>
11                 <add key="Find*" value="PROPAGATION_REQUIRED,readonly"/>
12             </name-values>
13         </property>
14 </object>

要注意,上面的例子我们已经显示地把父类对象定义成抽象,所以它实际上是不能被实例化的。应用程序上下文(并不是简单的对象工厂)会默认预实例化(pre-instantiate )所有的单例。因此,要注意的是(至少对于单例对象),如果你有一个你想作为一个模板的父对象,并且这个模板定义了一个特定的类,你必须保证将abstract 属性设置成true,不然应用程序上下文会尝试着去预实例化它。

代理机制

Spring在运行时通过使用类型构造器(TypeBuilder )的API创建AOP代理。

两种代理类型会被生成,基于结构(composition based )或者基于继承(inheritance based)。如果目标对象实现了至少一个接口那么就会生成基于结构的代理,不然就会生成基于继承的代理。

基于结构的代理是通过生成一个实现所有目标对象的接口的类型来实现。这个动态类真实的名字是一个类似于”GUID“的东西。通过一个私有字段来保存目标对象的引用,然后这个动态类型的实现会在调用目标类之前或者之后执行通知。

基于继承的代理会生成一个直接继承目标类型的动态类型。这样就可以在需要的时候直接追溯到目标类。要注意的是,两种方式中的目标方法实现如果调用了目标对象中的其他方法,这个方法是不会被通知的。想强制使用基于继承的代理方式,你可以设置ProxyFactory 的ProxyTargetType 属性为true,或者使用将XML的名称控件里的proxy-target-type 设置true。

在.NET 2.0中你可以定义程序集的特性InternalsVisibleTo来使得程序集内部的接口或者类允许被特定的”友好“的程序集访问。如果你需要在一个内部的类或者接口生成一个AOP代理,需要在AssemblyInfo文件中加入以下代码:[assembly: InternalsVisibleTo("Spring.Proxy")] and [assembly: InternalsVisibleTo("Spring.DynamicReflection")]。

基于继承的AOP配置(InheritanceBasedAopConfigurer)

在上文提到的基于继承的代理有一个很大的限制,那就是所有的方法都要被声明成虚方法。不然一些方法的调用会直接获取私有的”目标“字段或者基类。winform对象就是这样一个例子,它们的方法都不是虚方法。为了解决这个问题,在1.2版本中引入了一个新的后处理机制(post-processing mechanism ),这种机制生成一个不包含私有的”目标“字段的代理类型。通知在调用基类方法之前直接被添加到方法体中。

想要使用这种新的基于继承的代理,要在你的配置文件中声明一个InheritanceBasedAopConfigurer实例,和一个IObjectFactoryPostProcessor,一个例子:

 1 <object type="Spring.Aop.Framework.AutoProxy.InheritanceBasedAopConfigurer, Spring.Aop">
 2   <property name="ObjectNames">
 3       <list>
 4           <value>Form*</value>
 5           <value>Control*</value>
 6       </list>
 7   </property>
 8   <property name="InterceptorNames">
 9       <list>
10           <value>debugInterceptor</value>
11       </list>
12   </property>
13 </object>
14 <object id="debugInterceptor" type="AopPlay.DebugInterceptor, AopPlay"/>

这个配置风格和自动代理很相似,尤其是在你想为WinForm类添加通知的时候很好用。

通过代理工厂使用代码创建AOP代理

在Spring.NET中也很容易地通过代码来创建AOP代理。这样你就不需要依赖Spring.NET 的IoC功能。

以下展示了如何为一个目标对象创建代理,其中包含一个拦截器和一个通知器。目标对象实现的接口会被自动代理:

1 ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
2 factory.AddAdvice(myMethodInterceptor);
3 factory.AddAdvisor(myAdvisor);
4 IBusinessInterface tb = (IBusinessInterface) factory.GetProxy();

第一步是构建一个Spring.Aop.Framework.ProxyFactory对象。你可以像上面的例子一样创建一个目标对象,或者在构造器中指定要被代理的接口。

你可以添加拦截器或者通知器,然后配置它们。

ProxyFactory中也有一些方便的方法来允许你添加其他的通知类型。AdvisedSupport 是ProxyFactory 和ProxyFactoryObject的基类。

配置被通知对象

无论你通过那种方式创建AOP代理,你都可以通过使用Spring.Aop.Framework.IAdvised 来配置它们。所有的AOP代理都可以转换成这个接口类型,无论它实现了其他什么接口。这个接口包括以下的方法和属性:

 1 public interface IAdvised
 2 {    
 3     IAdvisor[] Advisors { get; }
 4     
 5     IIntroductionAdvisor[] Introductions { get; }
 6  
 7     void  AddInterceptor(IInterceptor interceptor);
 8  
 9     void  AddInterceptor(int pos, IInterceptor interceptor);  
10     void  AddAdvisor(IAdvisor advisor);
11     void  AddAdvisor(int pos, IAdvisor advisor);
12   
13     void  AddIntroduction(IIntroductionAdvisor advisor);
14   
15     void  AddIntroduction(int pos, IIntroductionAdvisor advisor);    
16     int IndexOf(IAdvisor advisor);
17     int IndexOf(IIntroductionAdvisor advisor);
18     bool RemoveAdvisor(IAdvisor advisor);
19     void RemoveAdvisor(int index);
20  
21     bool RemoveInterceptor(IInterceptor interceptor);
22     bool RemoveIntroduction(IIntroductionAdvisor advisor);
23  
24     void RemoveIntroduction(int index);
25     void ReplaceIntroduction(int index, IIntroductionAdvisor advisor);
26     bool ReplaceAdvisor(IAdvisor a, IAdvisor b);
27 }

通知器属性会被加入到工厂的所有通知器,拦截器或者其他通知类型返回一个IAdvisor接口。如果你添加了一个IAdvisor,返回的通知器就将会是你添加的对象。如果你天界了一个拦截器或者其他的通知类型,Spring.NET将会在一个通知器中使用一个总是返回true的IPointcut 接口包装它。因此如果你添加了一个IMethodInterceptor,通知器就会返回一个DefaultPointcutAdvisor和一个匹配所有类型和方法的IPointcut 接口。

AddAdvisor() 方法可以用来添加任意的IAdvisor接口。通常这个会是一个泛型DefaultPointcutAdvisor,这个通知器可以和任何的通知或者切入点使用(除了引入通知)。

一般来说,一个代理被生成之后也可以添加或者移除通知器或者拦截器。唯一的限制就是不能添加或者移除引入通知,因此工厂中的现存代理的接口不会改变。(你可以从工厂获取一个新的代理来避免这个问题)。

However you create AOP proxies, you can manipulate them using the Spring.Aop.Framework.IAdvised interface. Any AOP proxy can be cast to this interface, whatever other interfaces it implements. This interface includes the following methods and properties:

The Advisors property will return an IAdvisor for every advisor, interceptor or other advice type that has been added to the factory. If you added an IAdvisor, the returned advisor at this index will be the object that you added. If you added an interceptor or other advice type, Spring.NET will have wrapped this in an advisor with a IPointcut that always returns true. Thus if you added an IMethodInterceptor, the advisor returned for this

index will be a DefaultPointcutAdvisor returning your IMethodInterceptor and an IPointcut that matches all types and methods.
The AddAdvisor() methods can be used to add any IAdvisor. Usually this will be the generic DefaultPointcutAdvisor, which can be used with any advice or pointcut (but not for introduction).


By default, it's possible to add or remove advisors or interceptors even once a proxy has been created. The only restriction is that it's impossible to add or remove an introduction advisor, as existing proxies from the factory will not show the interface change. (You can obtain a new proxy from the factory to avoid this problem.) It's questionable whether it's advisable (no pun intended) to modify advice on a business object in production, although there are no doubt legitimate usage cases. However, it can be very useful in development: for example, in tests. I have sometimes found it very useful to be able to add test code in the form of an interceptor or other advice, getting inside a method invocation I want to test. (For example, the advice can get inside a transaction created for that method: for example, to run SQL to check that a database was correctly updated, before marking the transaction for roll back.)
Depending on how you created the proxy, you can usually set a Frozen flag, in which case the IAdvised IsFrozen property will return true, and any attempts to modify advice through addition or removal will result in an AopConfigException. The ability to freeze the state of an advised object is useful in some cases: For example, to prevent calling code removing a security interceptor.

posted @ 2016-05-05 00:00  balavatasky  阅读(432)  评论(0编辑  收藏  举报