MIT 是一种使用 C# 编排生成 IL 代码的技术,IL 是 .NET 平台的中间语言,由于 IL 的高性能的特点,很多框架都使用 EMIT 技术动态生成代码,最广泛的使用是编写 AOP 框架。在本节中,笔者将会介绍 AOP 的实现原理,以及使用 EMIT 编写一个简单的 AOP 程序。

创建控制台项目,引入 CZGL.AOP 包,示例代码请参考 Demo.CZGLAOP 项目。

有以下接口和类型:

public interface ITest
{
    void MyMethod();
}
public class Test : ITest
{
    public virtual string A { get; set; }
    public Test()
    {
        Console.WriteLine("构造函数没问题");
    }
    public virtual void MyMethod()
    {
        Console.WriteLine("运行中");
    }
}

我们希望,在执行 MyMethod 方法时,能够在执行前后打印出日志,这时可以先编写一个特性类,继承 ActionAttribute ,实现 Before 和 After 接口。

public class LogAttribute : ActionAttribute
{
    public override void Before(AspectContext context)
    {
        Console.WriteLine("--执行前--");
    }

    public override object After(AspectContext context)
    {
        Console.WriteLine("--执行后--");
        if (context.IsMethod)
            return context.MethodResult;
        else if (context.IsProperty)
            return context.PropertyValue;
        return null;
    }
}

然后改造 Test 类型。

[Interceptor]
public class Test : ITest
{
    [Log]
    public virtual string A { get; set; }
    public Test()
    {
        Console.WriteLine("构造函数");
    }
    [Log]
    public virtual void MyMethod()
    {
        Console.WriteLine("运行中");
    }
}

然后创建 AOP 类型:

ITest test1 = AopInterceptor.CreateProxyOfInterface<ITest, Test>();
test1.MyMethod();
Test test2 = AopInterceptor.CreateProxyOfClass<Test>();
test2.MyMethod();

运行项目,会输出:

构造函数
--执行前--
运行中
--执行后--
构造函数
--执行前--
运行中

AOP 实现原理

AOP(Aspect-oriented Programming) 即面向切片编程,在 C# 中有动态 AOP 和静态 AOP 两种,如果是在程序启动后生成的,为动态 AOP,这类框架有 Castle 、AspectCore 等,它们都使用了 EMIT 技术,在代码编译时即生成的,为静态 AOP,这类框架有 Fody 等。

实现 AOP 的前提

请看如下所示的代码,当调用 Voice() 方法时,请思考控制台会打印什么内容。

public class Program
{
    static void Main()
    {
        Animal c = new Cat();
        Console.WriteLine(c.Voice());
    }

	public abstract class Animal
	{
		public string Voice() => "null";
	}

    public class Cat: Animal
    {
        public new string Voice() => "喵";
    }
}

如果你有运行代码,会发现打印结果是 null,虽然 c 是 Cat 类型,但是这里我们使用的是 Animal 类型,CLR 首先判断 Voice 方法是否为抽象方法或虚方法,如果不是则直接调用,不会往子类中查找。

我们使用工具查看Animal 中 Voice 方法的 IL 代码:

// Methods
.method public hidebysig 
        instance string Voice () cil managed 

那么,同样的代码,在 java 中,又会发生什么呢?

public class Main {
    public static void main(String[] args) {
       Animal animal = new Cat();
       System.out.println(animal.Voice());
    }
}


class Animal{
    public String Voice(){
    return "null";
    }
}

class Cat extends Animal{
    public String Voice(){
    return "喵";
    }
}

运行这段代码后会发现,打印出来的是 。因为 java 中的方法默认是虚方法,而 C# 中的 方法需要加上关键字 virtual 才是虚方法。

为了能够在使用父类方法时,执行的是子类的代码,我们需要将代码改成:

public abstract class Animal
{
	public virtual string Voice() => "null";
}

public class Cat : Animal
{
	public override string Voice() => "喵";
}
// Methods
   .method public hidebysig newslot virtual 
           instance string Voice () cil managed 

那么,虚方法跟实现 AOP 有啥关系呢?其实,使用 EMIT 技术编写 AOP 框架的思路很简单,那就是继承,比如我们要给 A 类型的 A 方法实现 AOP,那么 A 方法就必须得是抽象方法或虚方法,然后我们通过 EMIT 技术生成一个类型 B 继承 A,然后创建 A 类型时实际上创建的是 B 类型。此时,调用 A 中的 A 方法,CLR 会执行 B 中的 A 方法。

A a = new B();

在使用 EMIT 技术实现 AOP 之前,我们可以通过容器依赖注入来领会 AOP 是如何通过继承实现的。

有个 UserService 服务,实现了登录过程。

public class UserService
{
	public virtual bool Login(string name, string passeword)
	{
		return true;
	}
}

然后我们使用一个新的类型重写 Login 方法,在执行方法之前和之后打印日志。

public class UserServiceAop : UserService
{
	public override bool Login(string name, string passeword)
	{
		Console.WriteLine($"用户开始登录:{name}");
		var result = base.Login(name, passeword);
		Console.WriteLine($"用户 {name} 登录结果 {result}");
		return result;
	}
}

然后通过注入服务实现 AOP:

IServiceCollection ioc = new ServiceCollection();
ioc.AddScoped<UserService, UserServiceAop>();
var services = ioc.BuildServiceProvider();

var userService = services.GetRequiredService<UserService>();
userService.Login("工良", "123456");

如果需要 AOP 的是接口,那就更加简单,我们只需要生成一个继承接口的类型即可,完全不需要继承父类,而父类也不需要标记为虚方法或抽象方法。

public interface IUserService
{
	bool Login(string name, string passeword);
}

public class UserService : IUserService
{
	public bool Login(string name, string passeword)
	{
		return true;
	}
}

public class UserServiceAop : IUserService
{
	private readonly UserService _service;
	public UserServiceAop()
	{
		_service = new UserService();
	}
	public bool Login(string name, string passeword)
	{
		Console.WriteLine($"用户开始登录:{name}");
		var result = _service.Login(name, passeword);
		Console.WriteLine($"用户 {name} 登录结果 {result}");
		return result;
	}
}

IServiceCollection ioc = new ServiceCollection();
ioc.AddScoped<IUserService, UserServiceAop>();
var services = ioc.BuildServiceProvider();

var userService = services.GetRequiredService<IUserService>();
userService.Login("工良", "123456");

实际上,接口方法本身就是抽象方法,所以因此无需处理接口即可直接使用 AOP 生成代理类型。

// Methods
   .method public hidebysig newslot abstract virtual 
           instance bool Login (
                string name,
                string passeword
            ) cil managed 
        {
        } // end of method IUserService::Login

通过这个例子你应该可以领会到使用 EMIT 技术实现 AOP ,最基础最本质的是实现一个新的类型基础父类或接口。由于 AOP 的类型是动态生成的,我们在开发是无法创建,所以 AOP 一般需要结合 IOC 使用,我们可以拦截容器中的 <IUserService, UserService>,替换成 <IUserService, UserServiceAop>。或者提供一个工厂服务,用于获取 AOP 后的对象。

EMIT 实现 AOP

本节代码可以在 Demo8.Console、Demo8.FxConsole 中查看。

ILSpy 是一个反编译工具,能够帮助我们查看代码生成的 IL,这样一来,即使我们对 IL 不熟悉,也可以通过编译好的 IL 代码中抄过来。

在 Visual Studio 中 安装扩展 ILSpy ,安装或手动下载(GitHub - icsharpcode/ILSpy: .NET Decompiler with support for PDB generation, ReadyToRun, Metadata (&more) - cross-platform! · GitHub)。

image-20230817193711194

创建一个项目,然后创建接口和类型。

public interface IUserService
{
	bool Login(string name, string passeword);
}

public class UserService : IUserService
{
	public bool Login(string name, string passeword)
	{
		return true;
	}
}
public class UserServiceAop : IUserService
{
	private readonly UserService _service;
	public UserServiceAop()
	{
		_service = new UserService();
	}
	public bool Login(string name, string passeword)
	{
		Console.WriteLine($"用户开始登录:{name}");
		var result = _service.Login(name, passeword);
		Console.WriteLine($"用户 {name} 登录结果 {result}");
		return result;
	}
}

然后编译项目生成 dll 文件,将 Demo8.Console.dll 拖动放到 ILSpy 中,你可以查看到 UserServiceAop 的全部 IL 代码,我们只需要抄即可!

image-20230817194216915

接下来,我们开始使用 EMIT 技术,动态生成一个 UserServiceAop 类型。

首先是动态构建程序集并创建一个新的类型。

public static Assembly Build()
{
	// 构建运行时程序集
	AssemblyName assemblyName = new AssemblyName("AopTmp");
	assemblyName.SetPublicKeyToken(new Guid().ToByteArray());
	AssemblyBuilder assBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndCollect);

	// 构建模块
	ModuleBuilder moduleBuilder = assBuilder.DefineDynamicModule(assemblyName.Name);
	/// 构建类型,命名空间+类名
	TypeBuilder typeBuilder = moduleBuilder.DefineType("Aop.UserServiceAop",
		TypeAttributes.Public, parent: null, interfaces: typeof(UserService).GetInterfaces());
	// 构建字段
	// field private initonly class Program/UserService _service
	var fieldBuilder = typeBuilder.DefineField("_service", typeof(UserService), FieldAttributes.Private | FieldAttributes.InitOnly);

	BuildCtor(typeBuilder, fieldBuilder);
	BuildMethod(typeBuilder, fieldBuilder);
	var type = typeBuilder.CreateType();
	return assBuilder;
}

创建类型后,首先给类型添加构造函数。

private static void BuildCtor(TypeBuilder typeBuilder, FieldBuilder fieldBuilder)
{
	// 构造函数

更多推荐