65.9K
CodeProject 正在变化。 阅读更多。
Home

AOP容器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2011 年 9 月 19 日

CPOL

11分钟阅读

viewsIcon

29512

downloadIcon

279

讨论如何使用可配置的动态装饰器模型将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,如下所示。它们分别是 AOPMefContainerAOPUnityContainerAOPWindsorContainer,分别用于 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,它实现了 IEmployeeINotifyPropertyChanged 这两个接口,如下所示。

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();
    }
}

正如您所见,这些方法以通用方式访问 TargetCallCtx,并且可以被各种类型的对象共享,以写入进入/退出日志。

另一方面,一些方面可能需要访问更具体的信息。例如,您只想在 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 对象作为参数。

注意:您可能已经注意到,在 SysConcernsLocalConcerns 类中的每个类中都有一个 static 构造函数。在 static 类中,类中定义的每个方面方法都用于创建一个 Decoration 实例,然后将其添加到 ConcernsContainer.runtimeAspects 字典中。ConcernsContainer 的定义如下。

public class ConcernsContainer
{
    static public Dictionary<string, Decoration> runtimeAspects =
				new Dictionary<string, Decoration>();
}

Dictionarystatic 构造函数的目的是维护应用程序中定义的方面方法的所有 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 - 将由 predecorationpostdecoration 指定的方面附加到的目标方法的名称。
  • predecoration - 预处理方面
  • postdecoration - 后处理方面

注释

  1. methods 属性值中的名称用逗号分隔。例如,"DetailsByLevel,get_EmployeeID"
  2. predecoration 属性的值包含两部分,并用逗号分隔。第一部分指定方面名称,第二部分指定定义该方面的程序集名称,例如 "SharedLib.SysConcerns.EnterLog,SharedLib.SysConcerns"。如果未指定第二部分,则假定该方面定义在入口程序集中,例如 "ConsoleUtil.LocalConcerns.SecurityCheck"
  3. postdecoration 属性的值包含两部分,并用逗号分隔。第一部分指定方面名称,第二部分指定定义该方面的程序集名称。如果未指定第二部分,则假定该方面定义在入口程序集中。

使用 AOP 容器

您可以像往常一样创建 IoC 容器并向其中注册类型。然后,您不必直接从容器解析对象,而是将容器传递给其扩展的 AOP 容器,然后使用 AOPContainerResolve<T, V>() 方法。它会创建一个对象并附加方面。从此方法返回的对象已经具有 AOP 功能。以下代码演示了如何使用 AOPMefContainerAOPUnityContainerAOPWindsorContainerEmployee 对象添加 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 实例,并将其分配给 aopcontainerswitch 语句之后的其余代码与 IoC 无关。

要创建对象并为其附加方面,Resolve<T, V>() 方法通过指定目标类型 T 和返回的接口类型 V 来使用。例如,在上面的代码中,Employee 被指定为目标类型,IEmployee 被指定为返回的接口类型。它返回一个 IEmployee 类型的代理。就是这样。现在,在使用 emp 时,它开始写入进入日志。

Resolve<T, V>() 通过查找 type 属性为 Tinterface 属性为 V 的第一个匹配项来查找配置文件中的第一个匹配项,然后使用该项的设置创建代理。对于 Resolve<Employee, IEmployee>(),它会尝试在配置文件中查找 type 属性为 "ThirdPartyHR.Employee"interface 属性为 "ThirdPartyHR.IEmployee" 的项。配置文件中的第一项匹配。因此,使用 methods 属性的值 "DetailsByLevel,get_EmployeeID"predecoration 属性的值 "SharedLib.SysConcerns.EnterLog,SharedLib.SysConcerns"postdecoration 属性的值 "" 来创建代理。在使用 emp 时,只有 DetailsByLevelget_EmployeeID 方法会写入进入日志。

Resolve<T, V>(string methods) 通过查找 type 属性为 Tinterface 属性为 Vmethods 属性为 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 对象。这可以通过使用 ConcernsContainerDictionary 获取与该方面关联的 Decoration 对象,然后设置其 Parameters 属性来实现。上面 try 块中的前三行代码就是这样做的。

为了捕获方面 LocalConcerns.NotifyChange 在方法 set_EmployeeID 设置属性后引发的事件,您需要在调用 set_EmployeeID 方法之前向 Employee 的目标对象注册一个监听器。为了实现这一点,您使用 AOPWindsorContainerGetTarget<T>() 方法获取目标对象,然后向目标对象注册监听器。try 块中代码的最后三行就是这样做的。

执行程序时,您将看到以下输出。

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

关注点

任何 IoC 容器都可以通过扩展 AOPContainer 来获得 AOP 功能。

AOPContainer 提供的可配置方面与 IoC 容器结合,使得 AOP 非常简单。您定义您的方面方法,然后在应用程序配置文件中添加元素将您的对象与方面关联起来。大多数情况下,您只需要做这些。在某些特殊情况下,您仍然可以通过代码更新方面的参数参数并获取目标对象。

参考文献

以下文章可以帮助您了解动态装饰器及其功能。

  1. 动态装饰器模式
  2. 使用动态装饰器为对象添加方面

以下文章讨论了动态装饰器如何帮助您改进应用程序开发。

  1. 组件、方面和动态装饰器
  2. 组件、方面和动态装饰器在 ASP.NET 应用程序中的应用
  3. 组件、方面和动态装饰器在 ASP.NET MVC 应用程序中的应用
  4. 组件、方面和动态装饰器在 Silverlight / WCF 服务应用程序中的应用
  5. 组件、方面和动态装饰器在 MVC/AJAX/REST 应用程序中的应用

以下文章比较了动态装饰器与其他一些类似工具。

  1. 动态装饰器与 Castle DynamicProxy 对比
  2. 动态装饰器、Unity 和 Castle DynamicProxy 对比

以下文章讨论了关于动态装饰器和 AOP 的一些杂项主题。

  1. 动态装饰器的性能
  2. 泛型动态装饰器
  3. 面向对象的方面 vs. 面向类的方面

历史

  • 2011 年 9 月 19 日:初次发布
© . All rights reserved.