AOP容器





5.00/5 (1投票)
讨论如何使用可配置的动态装饰器模型将AOP功能添加到IoC容器中。
引言
最近,我写了几篇关于 MEF 可配置方面、Unity 容器可配置方面 和 Windsor 容器可配置方面 的文章。虽然我可以继续为其他 IoC 容器撰写类似的 G文章,但我认为最好提出一个通用的 AOP 模型,它可以应用于任何 IoC 容器。
在本文中,我将讨论一个 AOP 模型,该模型能够从配置文件中检索方面、维护运行时方面的清单并将方面附加到对象上。然后,我将展示如何将其应用于 MEF、Unity 和 Windsor 等 IoC 容器。通过这些示例,您应该能够将其应用于其他 IoC 容器。最后,我将演示如何定义方面、在应用程序配置文件中配置它们,并使用此模型为应用程序添加 AOP 功能。
背景
动态装饰器是一种通过将行为附加到对象上来扩展对象功能的方法,而不是通过修改其类或创建新类。它很有吸引力,因为它避免了设计或重新设计类的麻烦。请阅读 动态装饰器模式 和 使用动态装饰器向对象添加方面 这两篇文章,以更深入地了解动态装饰器、它试图解决的问题、如何解决这些问题及其局限性。
还有一些其他文章讨论了动态装饰器如何用于应用程序开发以及它与其他类似技术的比较。请参阅本文的“参考文献”部分。
一旦您阅读了其中的一些文章,您就会发现动态装饰器易于使用且功能强大。在本文中,我将描述动态装饰器的可配置模型,这使其使用更加简单,同时仍然保持其强大功能。该模型被抽象为一个 AOP 容器,该容器能够从配置文件中检索方面、维护运行时方面的清单并将方面附加到对象上。
AOPContainer
AOPContainer
是一个 abstract
类。它解析应用程序配置文件中的方面配置,并在运行时维护方面的清单。它为对象创建提供了一个模板,并实现了将方面附加到对象的机制。它定义了几个 public
方法作为应用程序编程接口。在不涉及实现细节的情况下,以下文本展示了该类的高级设计。
abstract public class AOPContainer
{
//code omitted.
private object target;
abstract protected object ObjectResolver<T>();
public V Resolve<T, V>() where T : V
{
target = ObjectResolver<T>();
//code omitted.
}
public V Resolve<T, V>(string methods) where T : V
{
target = ObjectResolver<T>();
//code omitted.
}
public T GetTarget<T>()
{
//code omitted.
}
}
Resolve<T, V>()
方法使用 ObjectResolver<T>()
创建一个 T
类型的目标对象,并返回一个由 T
实现的 V
接口。请注意,Resolve<T, V>()
是一个模板方法。派生类必须实现 abstract
方法 ObjectResolver<T>()
来返回一个对象。其余的逻辑(代码已省略)会匹配配置文件中配置的方面,并将目标与方面连接起来。Resolve<T, V>(string methods)
是 Resolve<T, V>()
的一个重载,它基本上与 Resolve<T, V>()
具有相同的功能,并增加了匹配方面配置与方法名的功能。GetTarget<T>()
仅返回目标对象。有关详细信息,请参阅下载文件中的完整代码。
要将 AOPContainer
的功能应用于特定的 IoC 容器,您需要创建一个继承自 AOPContainer
的类。派生类应提供与 IoC 容器相关的具体信息,并实现 ObjectResolver<T>()
方法以使用 IoC 容器创建的对象。
三个类分别继承自 AOPContainer
,如下所示。它们分别是 AOPMefContainer
、AOPUnityContainer
和 AOPWindsorContainer
,分别用于 MEF、Unity 和 Windsor。

public class AOPMefContainer : AOPContainer
{
private CompositionContainer container;
public AOPMefContainer(CompositionContainer compositecontainer)
{
container = compositecontainer;
}
override protected object ObjectResolver<T>()
{
return container.GetExport<T>().Value;
}
}
public class AOPUnityContainer : AOPContainer
{
private IUnityContainer container;
public AOPUnityContainer(IUnityContainer unitycontainer)
{
container = unitycontainer;
}
override protected object ObjectResolver<T>()
{
return container.Resolve<T>();
}
}
public class AOPWindsorContainer : AOPContainer
{
private IWindsorContainer container;
public AOPWindsorContainer(IWindsorContainer windsorcontainer)
{
container = windsorcontainer;
}
override protected object ObjectResolver<T>()
{
return container.Resolve<T>();
}
}
正如您所见,扩展 AOPContainer
以适应 IoC 容器非常容易。您所要做的就是向构造函数提供 IoC 容器的特定对象,并重写 ObjectResolver<T>()
方法以使用 IoC 容器创建对象。
AOPContainer
的设计揭示了几个要点。首先,AOP 模型独立于 IoC 容器。方面在不关心使用何种 IoC 容器或如何创建对象的情况下进行定义、配置和附加到对象上。其次,凭借 AOPContainer
提供的所有功能,为喜欢的 IoC 容器添加 AOP 功能非常容易。只需像上面所示,通过提供 IoC 容器的特定信息来派生一个类。第三,AOPContainer
的编程接口在所有 IoC 容器中是一致的。因此,更改 IoC 容器不会改变使用应用程序中编程接口的代码。
在接下来的部分中,将通过一个示例来演示如何定义方面、配置它们以及使用派生类为应用程序添加 AOP 功能。
Using the Code
假设您有一个简单的组件 Employee
,它实现了 IEmployee
和 INotifyPropertyChanged
这两个接口,如下所示。
public interface IEmployee
{
System.Int32? EmployeeID { get; set; }
System.String FirstName { get; set; }
System.String LastName { get; set; }
System.DateTime DateOfBirth { get; set; }
System.Int32? DepartmentID { get; set; }
System.String DetailsByLevel(int iLevel);//1-full name;
//2-full name plus birth day; 3-full name plus birth day and department id.
}
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}
[PartCreationPolicy(CreationPolicy.NonShared)]
[Export]
public class Employee : IEmployee, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#region Properties
public System.Int32? EmployeeID { get; set; }
public System.String FirstName { get; set; }
public System.String LastName { get; set; }
public System.DateTime DateOfBirth { get; set; }
public System.Int32? DepartmentID { get; set; }
#endregion
public Employee(
System.Int32? employeeid
, System.String firstname
, System.String lastname
, System.DateTime bDay
, System.Int32? departmentID
)
{
this.EmployeeID = employeeid;
this.FirstName = firstname;
this.LastName = lastname;
this.DateOfBirth = bDay;
this.DepartmentID = departmentID;
}
public Employee() { }
public System.String DetailsByLevel(int iLevel)
{
System.String s = "";
switch (iLevel)
{
case 1:
s = "Name:" + FirstName + " " + LastName + ".";
break;
case 2:
s = "Name: " + FirstName + " " + LastName + ",
Birth Day: " + DateOfBirth.ToShortDateString() + ".";
break;
case 3:
s = "Name: " + FirstName + " " + LastName + ",
Birth Day: " + DateOfBirth.ToShortDateString() + ",
Department:" + DepartmentID.ToString() + ".";
break;
default:
break;
}
return s;
}
}
上面的代码是接口和类定义的普通 C# 代码。[PartCreationPolicy(CreationPolicy.NonShared)]
和 [Export]
这两个属性由 MEF 使用,以指定 Employee
的实例是导出的对象,并且每次请求都会创建一个新的非共享实例。Unity 或 Windsor 不需要它们,Unity 和 Windsor 会忽略它们。
定义方面
方面是横切关注点。对于动态装饰器,方面是一个方法,它接受一个 AspectContext
类型的对象作为第一个参数,一个 object[]
类型的对象作为第二个参数,并返回 void
。
您可以以更通用的方式设计您的方面,以便在各种情况下使用它们。您也可以为特定情况设计方面。例如,您可以以通用方式定义进入/退出日志方面,并将它们放在 SysConcerns
类中,如下所示。
public class SysConcerns
{
static SysConcerns()
{
ConcernsContainer.runtimeAspects.Add
("DynamicDecoratorAOP.SysConcerns.EnterLog",
new Decoration(SysConcerns.EnterLog, null));
ConcernsContainer.runtimeAspects.Add
("DynamicDecoratorAOP.SysConcerns.ExitLog",
new Decoration(SysConcerns.ExitLog, null));
}
public static void EnterLog(AspectContext ctx, object[] parameters)
{
StackTrace st = new StackTrace(new StackFrame(4, true));
Console.Write(st.ToString());
IMethodCallMessage method = ctx.CallCtx;
string str = "Entering " + ctx.Target.GetType().ToString() + "." +
method.MethodName +
"(";
int i = 0;
foreach (object o in method.Args)
{
if (i > 0)
str = str + ", ";
str = str + o.ToString();
}
str = str + ")";
Console.WriteLine(str);
Console.Out.Flush();
}
public static void ExitLog(AspectContext ctx, object[] parameters)
{
IMethodCallMessage method = ctx.CallCtx;
string str = "Exiting " + ctx.Target.GetType().ToString() + "." +
method.MethodName +
"(";
int i = 0;
foreach (object o in method.Args)
{
if (i > 0)
str = str + ", ";
str = str + o.ToString();
}
str = str + ")";
Console.WriteLine(str);
Console.Out.Flush();
}
}
正如您所见,这些方法以通用方式访问 Target
和 CallCtx
,并且可以被各种类型的对象共享,以写入进入/退出日志。
另一方面,一些方面可能需要访问更具体的信息。例如,您只想在 Employee
对象的属性被设置时为其附加更改通知功能。以下代码定义了一些特定方面。
class LocalConcerns
{
static LocalConcerns()
{
ConcernsContainer.runtimeAspects.Add
("ConsoleUtil.LocalConcerns.NotifyChange",
new Decoration(LocalConcerns.NotifyChange, null));
ConcernsContainer.runtimeAspects.Add
("ConsoleUtil.LocalConcerns.SecurityCheck",
new Decoration(LocalConcerns.SecurityCheck, null));
}
public static void NotifyChange(AspectContext ctx, object[] parameters)
{
((Employee)ctx.Target).NotifyPropertyChanged(ctx.CallCtx.MethodName);
}
public static void SecurityCheck(AspectContext ctx, object[] parameters)
{
Exception exInner = null;
try
{
if (parameters != null && parameters[0] is WindowsPrincipal &&
((WindowsPrincipal)parameters[0]).IsInRole
("BUILTIN\\" + "Administrators"))
{
return;
}
}
catch ( Exception ex)
{
exInner = ex;
}
throw new Exception("No right to call!", exInner);
}
}
在上面的代码中,NotifyChange
方法只能由 Employee
的目标使用,而 SecurityCheck
需要一个 WindowsPrincipal
对象作为参数。
注意:您可能已经注意到,在 SysConcerns
和 LocalConcerns
类中的每个类中都有一个 static
构造函数。在 static
类中,类中定义的每个方面方法都用于创建一个 Decoration
实例,然后将其添加到 ConcernsContainer.runtimeAspects
字典中。ConcernsContainer
的定义如下。
public class ConcernsContainer
{
static public Dictionary<string, Decoration> runtimeAspects =
new Dictionary<string, Decoration>();
}
此 Dictionary
和 static
构造函数的目的是维护应用程序中定义的方面方法的所有 Decoration
对象清单,并使其可以通过相应的方法名进行访问。这使得通过在应用程序配置文件中指定相应的方法名来配置方面成为可能。
配置方面
在配置文件中,您指定方面如何与对象关联。以下是一些示例,说明了方面是如何配置的。
<configuration>
<configSections>
<section name="DynamicDecoratorAspect"
type="DynamicDecoratorAOP.Configuration.DynamicDecoratorSection,
DynamicDecoratorAOP.Configuration" />
</configSections>
<DynamicDecoratorAspect>
<objectTemplates>
<add name="1"
type="ThirdPartyHR.Employee"
interface="ThirdPartyHR.IEmployee"
methods="DetailsByLevel,get_EmployeeID"
predecoration="SharedLib.SysConcerns.EnterLog,
SharedLib.SysConcerns"
postdecoration=""/>
<add name="2"
type="ThirdPartyHR.Employee"
interface="ThirdPartyHR.IEmployee"
methods="set_EmployeeID"
predecoration="ConsoleUtil.LocalConcerns.SecurityCheck"
postdecoration="ConsoleUtil.LocalConcerns.NotifyChange"/>
</objectTemplates>
</DynamicDecoratorAspect>
</configuration>
首先,您需要在配置文件中添加一个 <DynamicDecoratorAspect>
部分。然后,在 <DynamicDecoratorAspect>
的 <objectTemplates>
中,添加 individual 元素。对于 <objectTemplates>
中的每个元素,需要指定以下属性。
type
- 目标类型interface
- 返回的接口methods
- 将由predecoration
和postdecoration
指定的方面附加到的目标方法的名称。predecoration
- 预处理方面postdecoration
- 后处理方面
注释
methods
属性值中的名称用逗号分隔。例如,"DetailsByLevel,get_EmployeeID"
。predecoration
属性的值包含两部分,并用逗号分隔。第一部分指定方面名称,第二部分指定定义该方面的程序集名称,例如"SharedLib.SysConcerns.EnterLog,SharedLib.SysConcerns"
。如果未指定第二部分,则假定该方面定义在入口程序集中,例如"ConsoleUtil.LocalConcerns.SecurityCheck"
。postdecoration
属性的值包含两部分,并用逗号分隔。第一部分指定方面名称,第二部分指定定义该方面的程序集名称。如果未指定第二部分,则假定该方面定义在入口程序集中。
使用 AOP 容器
您可以像往常一样创建 IoC 容器并向其中注册类型。然后,您不必直接从容器解析对象,而是将容器传递给其扩展的 AOP 容器,然后使用 AOPContainer
的 Resolve<T, V>()
方法。它会创建一个对象并附加方面。从此方法返回的对象已经具有 AOP 功能。以下代码演示了如何使用 AOPMefContainer
、AOPUnityContainer
和 AOPWindsorContainer
为 Employee
对象添加 AOP 功能。
class Program
{
static void Main(string[] args)
{
AOPContainer aopcontainer = null;
IDisposable container = null;
switch (args[0])
{
case "0": //Aspects for Windsor Container
{
container = new WindsorContainer();
((IWindsorContainer)container).Register(AllTypes
.FromAssembly(Assembly.LoadFrom("Employee.dll"))
.Where(t => t.Name.Equals("Employee"))
.Configure(c => c.LifeStyle.Transient)
);
aopcontainer = new AOPWindsorContainer
((IWindsorContainer)container);
Console.WriteLine("Using AOPWindsorContainer.");
}
break;
case "1": //Aspects for Unity Container
{
container = new UnityContainer();
((IUnityContainer)container).RegisterType<IEmployee,
Employee>(new InjectionConstructor());
aopcontainer = new AOPUnityContainer
((IUnityContainer)container);
Console.WriteLine("Using AOPUnityContainer.");
}
break;
case "2": //Aspects for MEF
{
AggregateCatalog catalog = new AggregateCatalog();
catalog.Catalogs.Add(new AssemblyCatalog
(Assembly.LoadFrom("Employee.dll")));
container = new CompositionContainer(catalog);
aopcontainer = new AOPMefContainer
((CompositionContainer)container);
Console.WriteLine("Using AOPMefContainer.");
}
break;
default:
{
Console.WriteLine("Invalid number for an AOP container.");
Console.ReadLine();
}
return;
}
IEmployee emp = aopcontainer.Resolve<Employee, IEmployee>();
emp.EmployeeID = 1;
emp.FirstName = "John";
emp.LastName = "Smith";
emp.DateOfBirth = new DateTime(1990, 4, 1);
emp.DepartmentID = 1;
emp.DetailsByLevel(2);
Employee target = null;
IEmployee emp1 = aopcontainer.Resolve<Employee, IEmployee>
("set_EmployeeID");
try
{
//Commenting out this line will throw out an exception
Thread.GetDomain().SetPrincipalPolicy
(PrincipalPolicy.WindowsPrincipal);
Decoration dec = ConcernsContainer.runtimeAspects
["ConsoleUtil.LocalConcerns.SecurityCheck"];
dec.Parameters = new object[] { Thread.CurrentPrincipal };
target = aopcontainer.GetTarget<Employee>();
target.PropertyChanged += new PropertyChangedEventHandler
(PropertyChanged_Listener);
emp1.EmployeeID = 2;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
container.Dispose();
Console.ReadLine();
}
static void PropertyChanged_Listener(object sender, PropertyChangedEventArgs e)
{
Console.WriteLine(e.PropertyName.ToString() + " has changed.");
}
}
switch
语句中的语句特定于 IoC 容器。以 case "0"
为例。创建 WindsorContainer
对象并将其分配给 container
,然后使用它来注册类型。不是用它来解析对象,而是通过将 container
传递给其构造函数来创建一个 AOPWindsorContainer
实例,并将其分配给 aopcontainer
。switch
语句之后的其余代码与 IoC 无关。
要创建对象并为其附加方面,Resolve<T, V>()
方法通过指定目标类型 T
和返回的接口类型 V
来使用。例如,在上面的代码中,Employee
被指定为目标类型,IEmployee
被指定为返回的接口类型。它返回一个 IEmployee
类型的代理。就是这样。现在,在使用 emp
时,它开始写入进入日志。
Resolve<T, V>()
通过查找 type
属性为 T
且 interface
属性为 V
的第一个匹配项来查找配置文件中的第一个匹配项,然后使用该项的设置创建代理。对于 Resolve<Employee, IEmployee>()
,它会尝试在配置文件中查找 type
属性为 "ThirdPartyHR.Employee"
且 interface
属性为 "ThirdPartyHR.IEmployee"
的项。配置文件中的第一项匹配。因此,使用 methods
属性的值 "DetailsByLevel,get_EmployeeID"
、predecoration
属性的值 "SharedLib.SysConcerns.EnterLog,SharedLib.SysConcerns"
和 postdecoration
属性的值 ""
来创建代理。在使用 emp
时,只有 DetailsByLevel
和 get_EmployeeID
方法会写入进入日志。
Resolve<T, V>(string methods)
通过查找 type
属性为 T
、interface
属性为 V
且 methods
属性为 methods
的第一个匹配项来查找配置文件中的第一个匹配项。例如,代码 Resolve<Employee, IEmployee>("set_EmployeeID")
尝试匹配 type
属性为 "ThirdPartyHR.Employee"
、interface
属性为 "ThirdPartyHR.IEmployee"
和 methods
属性为 "set_EmployeeID"
的项,并且配置文件中的第二项匹配。因此,使用 predecoration
属性的值 "ConsoleUtil.LocalConcerns.SecurityCheck"
和 postdecoration
属性的值 "ConsoleUtil.LocalConcerns.NotifyChange"
来创建代理。在使用 emp1
时,只有 set_EmployeeID
方法会在调用之前检查安全性,并在调用之后触发通知。
还有几点值得注意。在 emp1
调用方面 LocalConcerns.SecurityCheck
之前,其 parameters
参数需要更新为 WindowsPrincipal
对象。这可以通过使用 ConcernsContainer
的 Dictionary
获取与该方面关联的 Decoration
对象,然后设置其 Parameters
属性来实现。上面 try
块中的前三行代码就是这样做的。
为了捕获方面 LocalConcerns.NotifyChange
在方法 set_EmployeeID
设置属性后引发的事件,您需要在调用 set_EmployeeID
方法之前向 Employee
的目标对象注册一个监听器。为了实现这一点,您使用 AOPWindsorContainer
的 GetTarget<T>()
方法获取目标对象,然后向目标对象注册监听器。try
块中代码的最后三行就是这样做的。
执行程序时,您将看到以下输出。

如果您注释掉 Thread.GetDomain().SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal)
这一行,您将看到以下输出。

关注点
任何 IoC 容器都可以通过扩展 AOPContainer
来获得 AOP 功能。
AOPContainer
提供的可配置方面与 IoC 容器结合,使得 AOP 非常简单。您定义您的方面方法,然后在应用程序配置文件中添加元素将您的对象与方面关联起来。大多数情况下,您只需要做这些。在某些特殊情况下,您仍然可以通过代码更新方面的参数参数并获取目标对象。
参考文献
以下文章可以帮助您了解动态装饰器及其功能。
以下文章讨论了动态装饰器如何帮助您改进应用程序开发。
- 组件、方面和动态装饰器
- 组件、方面和动态装饰器在 ASP.NET 应用程序中的应用
- 组件、方面和动态装饰器在 ASP.NET MVC 应用程序中的应用
- 组件、方面和动态装饰器在 Silverlight / WCF 服务应用程序中的应用
- 组件、方面和动态装饰器在 MVC/AJAX/REST 应用程序中的应用
以下文章比较了动态装饰器与其他一些类似工具。
以下文章讨论了关于动态装饰器和 AOP 的一些杂项主题。
历史
- 2011 年 9 月 19 日:初次发布