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

再次享受自定义属性的乐趣(第一部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (32投票s)

2007年9月17日

CPOL

6分钟阅读

viewsIcon

126587

downloadIcon

748

如果您认为自己对自定义特性无所不知,请阅读本文。PostSharp 将让您将自定义特性提升到新的水平,并让它们真正为您的代码添加新行为。

引言

如果您认为自己对自定义特性无所不知,请阅读本文。PostSharp 将让您将自定义特性提升到新的水平,并让它们真正为您的代码添加新行为。了解如何将日志记录、性能检测或字段验证封装到自定义特性中。将乐趣转化为实实在在的优势!

日志记录很痛苦

有很多强大的日志记录框架,但每个框架都需要一些痛苦的工作:在*每个*方法中添加样板代码。如果在项目中期,您决定从一个框架切换到另一个框架怎么办?您将不得不修改所有已记录的方法,而这可能涉及数千个方法!

日志记录很痛苦,因为它像大多数非功能性需求(安全性、事务、缓存等)一样,横切所有功能性需求。如果您有一百个业务流程需要日志记录、安全性和事务,那么您可能在每个业务流程中都有与日志记录、安全性和事务相关的指令。因此,我们需要一种更好的方法来封装横切关注点,这种方法不会强制我们修改它所应用的每个方法。

自定义特性是解决此问题的好方法。

一个简单的日志记录自定义特性

我们想要的东西很简单:一个自定义特性,在应用它的方法执行前后记录一条消息。我们还希望使用自定义特性构造函数指定跟踪类别。

所以,理想情况下,我们希望像这样使用自定义特性

[Trace("MyCategory")]
void SomeTracedMethod() 
{ 
   // Method body.

}

这就是我们想要的,现在让我们来实现它!我们可以像往常一样声明自定义特性。我们只需要一个名为category的字段和一个初始化此字段的构造函数

public sealed class TraceAttribute : Attribute
{ 
    private readonly string category; 

    public TraceAttribute( string category ) 
    { 
        this.category = category; 
    } 
  
    public string Category { get { return category; } } 
}

然后,我们可以添加使此自定义特性实际更改其所应用方法的内容。如引言所述,我们将使用 PostSharp 来完成此工作,所以让我们将其添加到项目中

Image

我们所要做的就是让我们的自定义特性继承 PostSharp.Laos.OnMethodBoundaryAspect 而不是 System.Attribute。此类定义了在目标方法执行时将在运行时调用的新方法

  • OnEntry – 方法执行前。
  • OnSuccess – 方法成功返回时(没有异常)。
  • OnException – 方法因异常退出时。
  • OnExit – 方法退出时,无论成功与否。

我们只实现我们感兴趣的方法:OnEntryOnExit

public override void OnEntry(MethodExecutionEventArgs eventArgs)
{
  // Add trace code here. 

}

public override void OnExit(MethodExecutionEventArgs eventArgs) 
{
  // Add trace code here. 

}

这些方法的实现应调用 System.Diagnostics.Trace.WriteLine。但是,我们如何知道我们实际所在方法的名称呢?一点问题都没有;所有必要的信息都包含在传递给 OnEntryOnExitMethodExecutionEventArgs 对象中。我们对 eventArgs.Method 属性感兴趣,但如果我们也想记录参数值,我们可以使用 GetArguments() 方法检索它们。

这是我们跟踪自定义特性的最终实现

[Serializable] 
public sealed class TraceAttribute : OnMethodBoundaryAspect 
{ 
    private readonly string category; 
   
    public TraceAttribute( string category ) 
    { 
        this.category = category; 
    } 
     
    public string Category { get { return category; } } 
     
    public override void OnEntry( MethodExecutionEventArgs eventArgs ) 
    { 
        Trace.WriteLine( 
            string.Format( "Entering {0}.{1}.", 
                           eventArgs.Method.DeclaringType.Name, 
                           eventArgs.Method.Name ), 
            this.category ); 
    } 
     
    public override void OnExit( MethodExecutionEventArgs eventArgs ) 
    { 
        Trace.WriteLine( 
            string.Format( "Leaving {0}.{1}.", 
                           eventArgs.Method.DeclaringType.Name, 
                           eventArgs.Method.Name ), 
            this.category ); 
    } 
}

你注意到了吗?该类用 Serializable 自定义特性修饰。这是从 PostSharp Laos 构建的每个自定义特性所必需的。

现在让我们在一些示例代码上尝试这个自定义特性

internal static class Program 
{ 
    private static void Main() 
    { 
        Trace.Listeners.Add(new TextWriterTraceListener( Console.Out)); 
       
        SayHello(); 
        SayGoodBye(); 
    } 
  
    [Trace( "MyCategory" )] 
    private static void SayHello() 
    { 
        Console.WriteLine("Hello, world." ); 
    } 
  
    [Trace("MyCategory")] 
    private static void SayGoodBye() 
    { 
        Console.WriteLine("Good bye, world."); 
    } 
}

现在,执行程序…奇迹出现了,方法调用被记录了!

Image

它是如何工作的?如果您查看 Visual Studio 的输出窗口,您会看到 PostSharp 在构建过程中被调用。

Image

PostSharp 实际上修改了 C# 编译器的输出并增强了程序集,以便在程序执行期间调用我们跟踪自定义特性的方法。

使用 Lutz Roeder 的 Reflector 查看生成的程序集非常有启发性

Image

如您所见,我们最初微小的方法现在变得复杂得多,因为 PostSharp 添加了指令以在运行时调用我们的自定义特性。

似曾相识?

如果你认为这与面向方面编程 (AOP) 非常相似,那么你是对的——PostSharp Laos 实际上就是一个 AOP 框架。

维基百科将*方面*定义为“程序中横切其核心关注点,因此违反其关注点分离原则的部分”。在大多数情况下,在业务应用程序中,方面是一个非功能性需求,例如日志记录、安全性、事务管理、异常处理或缓存。关注点分离是软件工程中主要的​​设计原则之一。它指出实现相同关注点的代码片段应该在组件中分组在一起。设计质量的衡量标准是组件的高内聚低耦合。

AOP 框架使得将方面封装到模块化实体中成为可能;在 PostSharp Laos 中,这些实体就是自定义特性。这种方法的主要优点是它的简单性:您可以在没有典型学习曲线的情况下获得 AOP。此外,PostSharp Laos 与语言无关,并且它与 Visual Studio(Intellisense、调试器等)的集成非常出色。

PostSharp 另一个与众不同之处在于它在编译后在 MSIL 级别运行。它没有基于代理的解决方案的局限性:您可以向私有方法添加方面,您不必让您的类派生自 MarshalByRefObject,等等。

如果您对 AOP 感兴趣,您可能需要查看 Microsoft Enterprise Library 的策略注入应用程序块或 Spring .NET Framework,这是 .NET 的另外两种可行的 AOP 解决方案。

更进一步:自定义特性多播

太棒了,我们有了一个跟踪自定义特性!但是,如果我们有数百个方法要跟踪怎么办?我们是否必须将此自定义特性添加到每个方法中?当然不用!多亏了一个名为*特性多播*的功能,我们可以在一行代码中将自定义特性应用到许多方法。

例如,将 TraceAttribute 添加到类 Program 实际上会将该特性应用到此类的每个方法

[Trace( "MyCategory" )] 
internal static class Program 
{
… 

而且,如果我们不希望自定义特性应用于 Main 方法,我们可以限制方法集并命名它所应用的方法

[Trace( "MyCategory", AttributeTargetMembers = "Say*")] 
internal static class Program 
{
…

或者,我们可以在程序集级别添加自定义特性以跟踪程序集中定义的所有方法

[assembly: Trace("MyCategory")] 

但是,有必要*排除*该特性应用于 TraceAttribute 类本身,因为一个方面不能自身被方面化

[Trace( null, AttributeExclude = true )]
[Serializable]
public sealed class TraceAttribute : OnMethodBoundaryAspect
{
…

摘要

在本文中,我展示了如何开发自定义特性,这些特性实际上为您的 .NET 程序添加了新行为。为了进行演示,我使用了 PostSharp Laos,这是一个用于 .NET 框架的面向方面解决方案。

PostSharp 诞生三年后,已经是一个成熟的项目。截至撰写本文时,PostSharp 已被下载数千次。目前处于 1.0 版本,并正在进入发布候选阶段。据报道,许多公司都在使用 PostSharp,包括独立软件供应商和系统集成商。该项目背后有一名全职开发人员,提供支持、咨询和赞助开发。bug 通常在一周内得到纠正。

正如我在本文中试图说明的那样,PostSharp 实际上修改了 MSIL 指令,以便在运行时调用额外的行为。人们可以使用 Lutz Roeder 的 Reflector 了解它是如何工作的。本文仅介绍了添加 try-catch 块到方法的自定义特性,但也可以拦截另一个程序集中的调用、拦截字段访问或在类型中注入接口。

在文章的第二部分,我将展示如何开发两个新的自定义特性:一个性能计数器和一个字段验证器。在此之前,祝您使用 PostSharp 愉快!

© . All rights reserved.