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





5.00/5 (21投票s)
这是关于如何通过向现有程序集添加方法调用来使用 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 解决方案的源代码分为三个项目
CodeInjection.Framework
:负责将方法调用注入到程序集目标方法的框架。CodeInjection.App
:Windows Forms 应用程序,它引用CodeInjection.Framework
类,以便将方法调用注入到磁盘上存在的目标程序集的目标方法中。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
接口:
顾名思义,该接口的每个方法都在目标方法开始和完成执行时被调用,这使得实现您需要的任何逻辑成为可能,例如将信息记录到数据库或触发其他方法。
为了找到实现 IMethodHandler
的 System.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
来注入目标方法的代码!
- 打开 Windows Forms 应用程序
CodeInjection.App
。 - 单击“浏览”,然后导航到刚刚执行的
DummyConsoleApp
程序集所在的文件夹。 - 单击“
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
属性标记的方法,框架将修改这些方法,以便在方法体开始和结束时添加一个框架方法调用。