使用 AspectDN 在 .NET 框架上进行面向切面编程





5.00/5 (4投票s)
如何将 AspectDN 的切面直接织入 .NET 二进制文件
引言
AspectDN
是一个用于将通知织入二进制 .NET 程序集的库。切面、通知和切点用接近 C#5 的语言描述。
AspectDN
可用于织入
- 代码片段
- 新类、结构、接口、枚举和委托
- 新成员,如字段、属性、事件、构造函数
根据通知的类型,切点可以是
- Assemblies
- Types
- 字段
- 属性
- 方法
- 异常
- 事件
以下示例摘自以下文章:面向切面编程 - 使用 RealProxy 类进行面向切面编程 | Microsoft,该文章说明了如何使用动态代理添加新功能,如日志记录或身份验证方法。
我们将做同样的示例,但这次使用 AspectDN
直接处理二进制文件(且无需重新编译初始应用程序的源代码)。
必备组件
请下载 zip 文件并解压缩。它包含:
- 一个VS Application 目录,其中包含我们将用于创建初始应用程序二进制文件的 Visual Studio 应用程序。
- 一个AspectRepository 01 目录,其中包含第一个修改的织入项目。
- 一个AspectRepository 02 目录,其中包含第二个修改的织入项目。
AspectDN
可在此处下载:此处。
由于我们处理的是二进制文件,因此您有时需要查看它们的变化。我建议使用 ILSPY,这是一个强大的工具。
目标应用程序
描述
假设您开发了一个用于管理客户存储库的应用程序。该应用程序被广泛使用并安装了许多次。该应用程序包含几个类:
- 一个
customer
类public class Customer { public int Id { get; set; } public string Name { get; set; } public string Address { get; set; } }
- 一个
interface
public interface IRepository<T> { void Add(T entity); void Delete(T entity); void Update(T entity); IEnumerable<T> GetAll(); T GetById(int id); }
- 一个实现上述
interface
的repository
类。public class Repository<T> : IRepository<T> { public void Add(T entity) { Console.WriteLine("Adding {0}", entity); } public void Delete(T entity) { Console.WriteLine("Deleting {0}", entity); } public void Update(T entity) { Console.WriteLine("Updating {0}", entity); } public IEnumerable<T> GetAll() { Console.WriteLine("Getting entities"); return null; } public T GetById(int id) { Console.WriteLine("Getting entity {0}", id); return default(T); } }
- 最后,一个
static
main
方法为该目录提供数据。static void Main(string[] args) { Console.WriteLine("***\r\n Begin program \r\n"); IRepository<Customer> customerRepository = new Repository<Customer>(); var customer = new Customer { Id = 1, Name = "Customer 1", Address = "Address 1" }; customerRepository.Add(customer); customerRepository.Update(customer); customerRepository.Delete(customer); Console.WriteLine("\r\nEnd program \r\n***"); Console.ReadLine(); }
由于 AspectDN
处理二进制文件,并且在我们深入研究切面之前,您需要使用 VSSourceApplication
中的 Visual Studio 项目生成 repository.exe 应用程序。然后打开命令提示符并运行 Repository.exe,如下所示:
...\VS SourceApplication\bin\Debug\repository.exe
您将获得以下结果:
操作步骤
最初,您将仅被要求为新部署添加新功能。
- 应用程序必须具有身份验证系统,在任何查询或更新之前使用。
- 应用程序必须对敏感操作进行审计和日志记录。
可能的解决方案
您有多种选择:
- 复制源代码并在该副本中进行代码更改。
- 更改原始应用程序源代码,并在config 文件中添加新设置,例如,以检查是否需要使用日志,具体取决于安装。
- 直接将更改添加到部署的二进制文件中,而不更改原始应用程序源代码。
第一个解决方案将耗费大量时间,因为需要维护多个应用程序。
第二个解决方案将使代码更加复杂,并且需要重新安装所有客户端,存在引入错误的风险。
第三个解决方案仍然是最简单的,并且没有前两种解决方案的缺点。此解决方案可以使用 AspectDN
实现。
第一次更改(AspectDN 项目 01)
在第一部分中,我们将仅添加日志要求。
织入您的第一个切面
在详细介绍切面声明之前,我们将首先运行 AspectDN
项目并执行织入。
在您之前下载的AspectDN 目录下的命令提示符中执行以下命令。
xxx\AspectDN\ApsectDb.exe -window
出现一个窗口,允许您加载项目。AspectDN
项目是一个 XML 文件,包含多个标签和属性,使我们能够加载织入上下文。
aspectdn 项目文件由以下部分组成:
<?xml version="1.0" encoding="utf-8" ?>
<AspectDN>
<Project name="AspectRepository"
language="CS5"
projectDirectoryPath="XXXX\AspectProject01"
logPath="..\"
sourceTargetPath="XXXX\VS SourceApplication\bin\debug"
outputTargetPath="..\target" >
<!-- c# aspect sources -->
<AspectSourceFiles>
<AspectSourceFile filename ="AspectRepository.CS5_ASP"/>
</AspectSourceFiles>
</Project>
</AspectDN>
- 属性
name
定义项目名称(AspectRepository
) - 属性
language
定义项目中使用的语言(目前仅支持 C#5) - 属性
projectDirectoryPath
定义包含文件源(aspects、advices、pointcuts)的目录的根目录。 - 属性
logPath
定义包含日志的目录路径,其中保存织入错误。 - 属性
sourceTargetPath
定义包含原始可执行应用程序(二进制文件而非源代码)的目录路径。 - 属性
outputTargetPath
定义织入切面后包含应用程序的目录路径。 AspectSourceFiles
定义包含要织入的切面、切点和通知的所有源。所有源文件必须包含在根目录中。
请注意,当路径以 ../ 开头时,表示该路径是根目录的子目录。
更改为您的目录后,加载项目配置文件,然后单击织入按钮。
织入操作的顺序显示在窗口的右侧区域。如果一切顺利,您将看到以下事件:
转到 'AspectDNProject 01' 目录的target 子目录并运行 repository.exe。结果如下:
如果我们使用 ILSPY 或 ILDASM 查看新文件,我们可以看到以下差异:
让我们看看我们编写的切面以实现此结果。
切面声明
打开项目目录中的源文件 AspectRepository.CS5_Asp。
首先,我们将描述用于将新成员织入 repository 类的切面。
在 AspectDN
中,所有通知、切点和切面都封装在一个或多个包(等同于 C# 中的命名空间)中,并如此声明。
using System;
using System.Threading;
package RepositoryAspects
{
// place aspects, pointcuts and advices
}
一个切面总是由以下几部分组成:
- 一个切点,可能还有一个连接点
- 一个通知
- 一个原型成员的映射
以下类型成员切面用于织入我们的新方法 Log
:
/* add a new member */
addLogMethod => // (1) name of the aspect
extend classes : classes.Name=="Repository`1" // (2) pointcut
with type members // aspect kind
{
// (3) method Log
private void Log(string msg, object arg = null)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(msg, arg);
Console.ResetColor();
}
}
addLogMethod
是切面的名称。- 切点表明我们正在扩展目标应用程序中包含的一个类,该类是一个名为
Repository
的泛型类(一个参数)。 - 通知描述了一个
Log
方法,该方法声明得就像在 C# 类中声明一样。
然后,我们声明将用于跟踪 Repository
类中 CRUD 方法的代码切面。此处仅描述 Add
方法,因为其他方法声明方式相同,除了少数细节。
// add log in add method
codeAspectAdd => // (1) name of the aspect
extend around body methods : methods.Name == "Add" &&
methods.DeclaringType.Name == "Repository`1" // (2) pointcut
with
{
// (3) members of the target we need
prototype members
{
<#T>; // type parameter
#T #Entity; // field
void #Log(string msg, object arg); // method
}
// (4) chunk of code
#Log("Before Adding {0}", #Entity);
[around anchor];
#Log("After Adding {0}", #Entity);
} where #Log = Log, <#T> : T, #Entity= entity; // (5) prototype member mappings
- 切面
codeAspectAdd
的名称。 - 切点表明我们要将代码块织入
Repository~1
类Add
方法的主体内。 - 原型成员声明用于描述当前通知中使用的目标方法内部上下文。建议尽量实现与目标的松耦合,以确保切面的重用性。这些成员始终以
#
开头,以区别于切面声明变量。<#T>
是一个泛型参数原型,它必须在目标(类或方法)中有对应的项。#Entity
是目标中存在的字段、属性、变量或方法参数原型。void #Log(string msg, object msg)
是目标中现有方法的原型。在这种情况下,它由前一个切面织入。
- 要织入的代码块由 C#5 语句组成。[around anchor] 指令指示指令将被拆分为两部分:连接点之前和之后的部分。
- 原型成员的名称与目标的实际成员名称进行映射。
通过使用 System.Diagnostic
的 StackFame
来获取方法名称(add
、update
、delete
),可以只创建一个代码切面来处理所有 CRUD 方法。
更改的第二部分(Aspectdn 项目 02)
现在,我们为添加、修改和删除客户添加基本的身份验证检查。这些权限将仅授予具有 ADMIN 角色的用户。
执行织入
在解释第二部分中使用的切面之前,我们将执行与第一部分相同的操作,即织入切面并查看结果。我们现在处理 'AspectDNProject 02' 目录中定义的项目。更改配置文件中的目录为您的目录后,运行 aspectdn -window
命令,加载项目并织入。
在目标目录中运行织入的应用程序。使用下面的命令,不带任何参数。
要使用具有相应权限的用户进行检查,请使用以下命令运行 EXE 及其任意参数:
我们应用了哪些更改?
例如,如果您使用 ILSpy,您将在织入的应用程序中看到与原始应用程序不同的更改。
而不是在 repository~1
类中定义 LOG
方法,而是创建了一个新的 Logger
类。
另一个新类负责身份验证。它使用运行进程的用户或服务帐户的身份。日志记录器将用于跟踪身份验证。
在 Repository~1
的 CRUD 方法中,已织入新代码以检查身份验证。日志记录器将用于跟踪身份验证。
最后,在 main
方法中,为了能够模拟具有或不具有相应权限的身份验证,如果传递了参数,则会更改身份。
切面声明
打开项目目录中的源文件 AspectRepository.CS5_Asp。
我们声明了一个类型切面,用于将两个新类织入 Repository.Exe 程序集。
/* add Lgger and Authentification class */
typeAspect =>
extend assemblies : assemblies.Name == "Repository.exe" // (1) pointcut
with types
{
// (2) Logger class declaration
public static class Logger
{
public static void Log(string msg, object arg)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(msg, arg);
Console.ResetColor();
}
}
// (2) Authentification class declaration
public static class Authentication
{
public static bool Check(string msg)
{
if (Thread.CurrentPrincipal.IsInRole("ADMIN"))
{
Logger.Log("User authenticated - You can execute '{0}' ", msg);
return true;
}
Logger.Log("User not authenticated - You can't execute '{0}' ", msg);
return false;
}
}
} namespace target Repository; // (3) target namespace
- 这次,切点设置在 Repository.exe 程序集上。
- 我们在此类型切面中声明了两个类,就像在 C# 项目中声明类一样。请注意,
Authentication
类使用Logger
。 - 对于类型切面,必须在目标中指明类将被织入的命名空间。
对于 CRUD 方法,我们声明了以下代码切面,该切面使用前两个类。
// add the authentifcation for the add method
codeAspectAdd =>
extend around body methods : methods.Name == "Add" &&
methods.DeclaringType.Name == "Repository`1" // pointcut
with
{
// target members
prototype members
{
<#T>;
#T #Entity;
}
if (!typeAspect.Authentication.Check("Add"))
return;
typeAspect.Logger.Log("Before Adding {0}", #Entity);
[around anchor];
typeAspect.Logger.Log("After Adding {0}", #Entity);
} where typeAspect.Authentication from Repository,
typeAspect.Logger from Repository, <#T> : T, #Entity= entity;
要使用类型切面中的类型,必须在类型名称前加上类型通知(在本例中为与切面相同的名称),并且您需要指定它们属于目标的哪个命名空间。
让我们看看用于更改 main
方法中用户身份的最后一个代码切面。
codeAspectTest =>
extend before body methods : methods.Name == "Main"
with
{
prototype members
{
string[] #args;
}
if (#args!=null && #args.Length>0)
{
Thread.CurrentPrincipal =
new GenericPrincipal(new GenericIdentity("Administrator"),
new[] { "ADMIN" });
}
} where #args = args;
如您所见,我们声明了一个 string
数组作为原型成员,它映射了方法参数。所有指令均使用 C#5 语言。
结论
与需要代码更改才能工作的 RealProxy
类不同,AspectDN
以极大的简便性和极少的努力将切面直接织入可执行文件中。
使用的语言接近 C#5,因此开发人员可以轻松快速地编写切面。
有很多教程供您探索和发现,例如如何使用 AspectDN
定制分层应用程序(包括持久化),方法是添加新的功能要求。
历史
- 2023 年 7 月 15 日:初始版本