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

将方法调用注入程序集中的现有方法

starIconstarIconstarIconstarIconstarIcon

5.00/5 (21投票s)

2012 年 4 月 24 日

CPOL

7分钟阅读

viewsIcon

69739

downloadIcon

1312

这是关于如何通过向现有程序集添加方法调用来使用 Mono.Cecil 基本功能的演示。

引言

自 2008 年以来,我一直是 The Code Project 文章的常客,自那时以来,我从许多其他 .NET 专家那里学到了很多东西。

这是我在这个网站上的第一篇文章,也是我通过为 Dev 社区做出贡献来分享我的知识的机会。

请随时通过“评论和讨论”部分给我积极或消极的反馈。我很想听听您对本文的看法,并有机会学到更多。

背景

在 2009/2010 年,我创建了一个业务组件,用于允许客户端应用程序(与该组件集成)进行访问控制、用户身份验证和资源保护(业务对象和 UI 对象/表单)。

该组件的其中一个功能是允许记录已登录用户在运行时执行的方法,以进行审计。

使用该组件的开发人员无需编写大量代码即可实现此功能。

为了让开发人员编写更少的代码,我创建了一个属性,当将其添加到方法上时,它将开始记录该方法执行的开始和结束,并将日志信息持久化到数据库。

该属性本身并没有太多作用,它主要用于标记一个需要在编译后——程序集修改——阶段进行代码注入的方法。

概述

为方便起见,已更改下载的源代码,使得“客户端代码”中注入的方法调用的处理方式可以泛化,而不仅仅是记录带有 CodeInjectionAttribute 装饰的方法的开始和结束。

为了实现代码注入/程序集发射,我评估了 System.Reflection.Emit 命名空间中的类,但是我在修改目标程序集时遇到了一些困难,因为要修改它,我们首先需要将其加载到 AppDomain 中,这反过来又会导致已加载的程序集被使用/分配,使其无法在磁盘上进行更改(保存)。

为了解决这个问题,我决定使用 Mono.Cecil,它比 Reflection.Emit 更易于使用,因为它具有对象模型,并且可以完美地修改磁盘上的程序集。

我为 C# 爱好者感到抱歉,源代码只能以 VB.NET 形式提供,但我过去 6 年一直在使用 VB.NET 进行开发,并且对 VB.NET 更熟悉。一旦有机会,我会将其移植到 C# 并提供下载。

我再次为代码注释道歉。注释混合了英语和葡萄牙语(我的母语)。由于最初是一个商业组件,为了方便使用 SandCastle 进行文档记录,具有公共可见性的代码注释是英文的,否则是葡萄牙语。

Visual Studio 解决方案

可下载的 Visual Studio 解决方案的源代码分为三个项目

  1. CodeInjection.Framework:负责将方法调用注入到程序集目标方法的框架。
  2. CodeInjection.App:Windows Forms 应用程序,它引用 CodeInjection.Framework 类,以便将方法调用注入到磁盘上存在的目标程序集的目标方法中。
  3. DummyConsoleApp:控制台应用程序,将在本文中用作目标程序集的示例,其中一些方法将被代码注入。

您应该编译所有提到的项目。

Using the Code

让我们看看在目标应用程序的方法上需要做什么,以便为代码注入做好准备。打开“DummyConsoleApp”项目,这个应用程序唯一的作用是实例化 Dummy 类的一个对象并调用 Dummy.DoSomething(),如下所示:

Module Module1
    Sub Main()
        Dim obj As New Dummy
        obj.DoSomething()
        Console.ReadKey()
    End Sub
End Module

下面展示了 Dummy 类:

CodeInjection.Framework.Attributes.CodeInjectionAttribute(GetType(DummyHandler))
  Public Class Dummy
    CodeInjection.Framework.Attributes.CodeInjectionAttribute()
    Public Sub DoSomething()
        Console.WriteLine("Doing something...")
        DoingOtherThing()
    End Sub
    Private Sub DoingOtherThing()
        Console.WriteLine("Doing other thing...")
        ImDone()
    End Sub
 
    CodeInjection.Framework.Attributes.CodeInjectionAttribute()
    Private Sub ImDone()
        Console.WriteLine("I'm Done!")
    End Sub
End Class

请注意,Dummy 类以及 DoSomething()ImDone() 方法都用 CodeInjectionAttribute 属性进行了装饰。

该属性指示将为哪些方法进行代码注入。

处理目标方法执行

这是我更改过的源代码部分,因为在原始版本中,当执行目标方法时,其执行的开始和结束信息会被持久化到数据库以进行审计。

为了提供更大的灵活性,我为 CodeInjection 属性创建了一个重载的构造函数,允许您告知哪个实现 IMethodHandler 接口的 System.Type 将负责处理注入到特定方法或类的目标方法中的方法调用。

下面展示了 IMethodHandler 接口:

顾名思义,该接口的每个方法都在目标方法开始和完成执行时被调用,这使得实现您需要的任何逻辑成为可能,例如将信息记录到数据库或触发其他方法。

为了找到实现 IMethodHandlerSystem.Type,框架首先在目标方法中的 CodeInjectionAttribute 中搜索,如果找不到,则在方法所在类的 CodeInjectionAttribute 中搜索。当找到 System.Type 时,它会创建一个实例来使用它。如下图所示的时序图:

DummyConsoleApp 项目中,我创建了一个名为 DummyHandler 的类,它实现了 IMethodHandler,该类只是将消息写入控制台。如下所示:

Public Class DummyHandler
    Implements CodeInjection.Framework.IMethodHandler
 
    Public Sub MethodBegin(context As CodeInjection.Framework.IInvocationContext) _
           Implements CodeInjection.Framework.IMethodHandler.MethodBegin
        Console.WriteLine(String.Format("Begin of method {0}", context.Name))
    End Sub
 
    Public Sub MethodEnd(context As CodeInjection.Framework.IInvocationContext) _
           Implements CodeInjection.Framework.IMethodHandler.MethodEnd
        Console.WriteLine(String.Format("End of method {0}", context.Name))
    End Sub
End Class

使用 CodeInjection.App 将代码注入目标方法

为了方便代码注入,我创建了一个名为 CodeInjection.App 的小型 Windows 窗体应用程序,它使用 CodeInjection.Framework 将代码注入磁盘上程序集的目标方法中。

如果我们从命令提示符执行现有的 DummyConsoleApp,我们将得到如下输出:

请注意,Dummy 类代码没有发生任何特殊变化。

现在,让我们使用 CodeInjection.App 来注入目标方法的代码!

  1. 打开 Windows Forms 应用程序 CodeInjection.App
  2. 单击“浏览”,然后导航到刚刚执行的 DummyConsoleApp 程序集所在的文件夹。
  3. 单击“Inject”按钮。

现在再次从命令提示符尝试执行 DummyConsoleApp 控制台应用程序,并查看输出!

通过查看控制台输出,我们可以看到用 CodeInjectionAttribute 属性标记的方法现在正在执行额外的操作,这意味着代码注入已经完成。

如果我们用 .NET Reflector 打开这个程序集并与原始代码进行比较,我们可以看到其中的差异:

幕后

我们已经知道类和方法(将成为代码注入的目标)必须添加 CodeInjectionAttribute,但是什么代码有效地将外部方法调用注入到目标方法中,它是如何工作的?

为了方便实现代码注入,我使用了 Jean Baptiste Evain 创建的 Mono.Cecil,您可以从 这里 下载。

在我的实现中,方法调用的注入发生在 CodeInjection.Framework.CodeInector 类中,注入到目标方法中的外部方法调用位于 CodeInjection.Framework.Helpers 类中。

CodeInjection.App 通过在 CodeInjection.Framework.CodeInjector 类的构造函数中提供要修改的程序集的磁盘路径来执行代码注入。

此类中存在一些事件,但是,只有当您需要通知调用者代码注入的进度时,它们才是有必要的,您可以在 CodeInjection.App 项目的 frmProgress 窗体中看到。

CodeInjector.InjectBeginAndEndMethodCalls() 方法用于获取以下框架方法的引用:

  • Helpers.OnMethodBegin()
  • Helpers.OnMethodEnd()
  • Helpers.GetMethodContext()

这些方法是目标程序集外部的方法,其中目标方法的注入将发生。

Private Sub InjectBeginAndEndMethodCalls(ByVal assembly As Mono.Cecil.AssemblyDefinition, _
                                        ByVal targetMethod As Mono.Cecil.MethodDefinition)
    Dim worker = targetMethod.Body.GetILProcessor
    Dim getMethodContext As Mono.Cecil.MethodReference = a_
        ssembly.MainModule.Import(_MethodGetMethodContext)
    Dim beforeExecuteMethodCallStatic As Mono.Cecil.MethodReference = _
        assembly.MainModule.Import(_MethodBeforeExecuteStatic)
    Dim afterExecuteMethodCallStatic As Mono.Cecil.MethodReference = _
        assembly.MainModule.Import(_MethodAfterExecuteStatic)

    InjectCodeOnMethod(beforeExecuteMethodCallStatic, 0, targetMethod, getMethodContext)
    InjectCodeOnMethod(afterExecuteMethodCallStatic, _
        targetMethod.Body.Instructions.Count - 1, targetMethod, getMethodContext)
End Sub

一旦获取了框架方法的引用,就会调用 InjectCodeOnMethod() 两次。

首先,在目标方法体开始处注入 Helpers.OnMethodBegin() 方法调用,最后在目标方法体结束处注入 Helpers.OnMethodEnd()

Private Shared Sub InjectCodeOnMethod(ByVal BeginOrEndMethodRef As Mono.Cecil.MethodReference,
                       ByVal positionOnTargetMethod As Integer,
                       ByVal targetmethod As Mono.Cecil.MethodDefinition,
                       ByVal getContextMethodRef As Mono.Cecil.MethodReference)
    Dim IL = targetmethod.Body.GetILProcessor
    Dim GetContextmethodInjection As Instruction = IL.Create(OpCodes.Call, getContextMethodRef)
    Dim BeginOrEndMethodInjection As Instruction = IL.Create(OpCodes.Call, BeginOrEndMethodRef)

    Dim targetMethodBodyInstruction As Instruction = _
                        targetmethod.Body.Instructions(positionOnTargetMethod)
    IL.InsertBefore(targetMethodBodyInstruction, GetContextmethodInjection)
    IL.InsertAfter(GetContextmethodInjection, BeginOrEndMethodInjection)
End Sub

术语表

  • 框架:一组类,其目的是促进对目标程序集的代码注入。
  • 目标程序集目标程序集:其用 CodeInjectionAttribute 属性标记的方法将被修改的目标程序集。
  • 目标方法目标方法:用 CodeInjectionAttribute 属性标记的方法,框架将修改这些方法,以便在方法体开始和结束时添加一个框架方法调用。
© . All rights reserved.