[.NET Core + AOP + Attribute]实现横切面对象方法的额外操作,简化开发,提高效率!

介绍

AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,用于将横切关注点(cross-cutting concerns)从业务逻辑中分离出来。

核心思想: 在不修改原有业务代码的前提下,通过”切入”的方式为程序动态添加额外行为。

常见的横切关注点:

  • 日志记录
  • 性能监控
  • 事务管理
  • 权限校验
  • 缓存处理
  • 异常处理
  • 重试机制

核心概念:

概念说明
Aspect(切面)横切关注点的模块化,如”日志切面”
Join Point(连接点)程序执行过程中可以插入切面的点(如方法调用)
Advice(通知/增强)切面在连接点执行的具体行为(前置、后置、环绕等)
Pointcut(切入点)匹配连接点的表达式,定义哪些方法会被增强
Weaving(织入)将切面应用到目标对象的过程

DispatchProxy 方案

DispatchProxy 是 .NET 内置的动态代理基类,位于 System.Reflection.DispatchProxy,无需任何第三方库。

定义接口:

public interface ICalculator
{
    int Add(int a, int b);
    int Divide(int a, int b);
}

创建实现类:

public class Calculator : ICalculator
{
    public int Add(int a, int b) => a + b;
    public int Divide(int a, int b) => a / b;
}

创建接口代理:

public class AopProxy<T> : DispatchProxy where T : class
{
    private T _target;

    // 工厂方法:创建代理
    public static T Create(T target)
    {
        // Create 返回的是 DispatchProxy 的子类,需要强转
        var proxy = Create<T, AopProxy<T>>() as AopProxy<T>;
        proxy._target = target;
        return proxy as T;
    }

    // 所有接口方法调用都会路由到这里
    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        Console.WriteLine($"[Before] 方法: {targetMethod.Name},参数: [{string.Join(", ", args)}]");

        object result = null;
        try
        {
            // 调用真实方法
            result = targetMethod.Invoke(_target, args);

            Console.WriteLine($"[After]  方法: {targetMethod.Name},返回: {result}");
        }
        catch (TargetInvocationException ex)
        {
            Console.WriteLine($"[Error]  方法: {targetMethod.Name},异常: {ex.InnerException?.Message}");
            throw ex.InnerException; // 把原始异常抛出去
        }

        return result;
    }
}

使用

var real = new Calculator();

// 通过代理对象调用,所有方法自动被拦截
ICalculator calc = AopProxy<ICalculator>.Create(real);

Console.WriteLine(calc.Add(3, 5));
Console.WriteLine(calc.Divide(10, 2));
// Console.WriteLine(calc.Divide(10, 0)); // 会被拦截到异常处理

Castle.DynamicProxy 方案

Castle.DynamicProxy 是 Castle Project 中的一个核心组件,是一个运行时动态生成代理类(Proxy)的 .NET 库。它可以在不修改原始代码的情况下,拦截方法调用并注入自定义逻辑——这是 AOP(面向切面编程) 在 .NET 生态中最经典的实现方式之一。

安装包:

Install-Package Castle.Core

# 若配合 Autofac 使用
Install-Package Autofac.Extras.DynamicProxy

这里我们使用 特性 + 拦截器 实现!

定义特性基类:

/// <summary>
/// 拦截器基类特性
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public abstract class InterceptBaseAttribute : Attribute
{
    /// <summary>
    /// 切面优先级,数值越小优先级越高
    /// </summary>
    public int Order { get; set; } = 0;

    /// <summary>
    /// 是否忽略拦截(用于方法级覆盖类级配置)
    /// </summary>
    public bool Ignore { get; set; } = false;

    /// <summary>
    /// 执行拦截前逻辑(由子类实现)
    /// </summary>
    /// <param name="invocation">拦截的调用信息</param>
    public abstract void OnBefore(IInvocation invocation);

    /// <summary>
    /// 执行拦截后逻辑(由子类实现)
    /// </summary>
    /// <param name="invocation">拦截的调用信息</param>
    public abstract void OnAfter(IInvocation invocation);

    /// <summary>
    /// 拦截器异常处理逻辑(由子类实现)
    /// </summary>
    /// <param name="invocation">拦截的调用信息</param>
    /// <param name="ex">发生的异常</param>
    public abstract void OnException(IInvocation invocation, Exception ex);
}
/// <summary>
/// 日志拦截器
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class LogAttribute : InterceptBaseAttribute
{
    public override void OnAfter(IInvocation invocation)
    {
    }

    public override void OnBefore(IInvocation invocation)
    {

    }

    public override void OnException(IInvocation invocation, Exception ex)
    {

    }
}

忽略拦截特性:如果我们在标记了整个类,但是类中的某个方法不想被拦截,就可以使用下面这个特性进行标记。

/// <summary>
/// 免拦截特性
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class NoInterceptAttribute : Attribute
{
}

拦截器基类:(抽象类)

/// <summary>
/// 拦截器基类
/// </summary>
public abstract class BaseInterceptor : IInterceptor
{
    /// <summary>
    /// 拦截方法执行的核心逻辑
    /// </summary>
    /// <param name="invocation">拦截的调用信息</param>
    public virtual void Intercept(IInvocation invocation)
    {
        // 检查是否全局忽略
        if (HasNoIntercept(invocation))
        {
            invocation.Proceed();//执行目标方法
            return;
        }

        // 收集并排序所有 InterceptAttribute
        var attributes = GetInterceptAttributes(invocation).OrderBy(a => a.Order).ToList();

        // 执行 Before
        foreach (var attr in attributes)
        {
            if (!attr.Ignore) attr.OnBefore(invocation);
        }

        try
        {
            // 执行目标方法
            invocation.Proceed();

            // 处理异步方法
            if (invocation.ReturnValue is Task task)
            {
                task.GetAwaiter().GetResult(); // 同步等待,确保 After 在完成后执行
            }
        }
        catch (Exception ex)
        {
            // 6. 执行异常处理
            foreach (var attr in attributes)
            {
                if (!attr.Ignore) attr.OnException(invocation, ex);
            }
            throw;
        }

        // 7. 执行 After
        foreach (var attr in attributes.AsEnumerable().Reverse()) // 反向执行,类似栈
        {
            if (!attr.Ignore) attr.OnAfter(invocation);
        }

        // 8. 子类扩展点
        OnIntercepted(invocation);
    }

    /// <summary>
    /// 子类扩展:拦截完成后的额外操作
    /// </summary>
    protected virtual void OnIntercepted(IInvocation invocation) { }

    /// <summary>
    /// 检查方法或类是否有 [NoIntercept]
    /// </summary>
    protected bool HasNoIntercept(IInvocation invocation)
    {
        return invocation.Method.GetCustomAttribute<NoInterceptAttribute>() != null || invocation.TargetType!.GetCustomAttribute<NoInterceptAttribute>() != null;
    }

    /// <summary>
    /// 收集类级 + 方法级的 InterceptAttribute
    /// </summary>
    /// <param name="invocation">调用信息</param>
    /// <returns>所有相关的 InterceptBaseAttribute</returns>
    protected IEnumerable<InterceptBaseAttribute> GetInterceptAttributes(IInvocation invocation)
    {
        var methodAttrs = invocation.Method.GetCustomAttributes<InterceptBaseAttribute>();//方法级特性
        var classAttrs = invocation.TargetType!.GetCustomAttributes<InterceptBaseAttribute>();//类级特性
        return methodAttrs.Concat(classAttrs);
    }
}

服务层接口:

public interface IUserService
{
    Task<User> GetUserAsync(int id);
    void DeleteUser(int id);
    void InternalMethod(); // 不想被拦截
}

服务层标记:

// 类级:所有方法默认被 Log 和 Performance 拦截
[Log(Order = 1)]
[Performance(Order = 2)]
public class UserService : IUserService
{
    public async Task<User> GetUserAsync(int id)
    {
        // 自动触发 Log + Performance
        return await _db.Users.FindAsync(id);
    }

    // 方法级:额外添加缓存,或覆盖类级配置
    [Log(Ignore = true)]  // 此类方法不记录日志
    public void DeleteUser(int id)
    {
        _db.Users.Remove(id);
    }

    [NoIntercept]  // 完全跳过拦截
    public void InternalMethod()
    {
        // 纯内部逻辑,不触发任何切面
    }
}

使用

我们最后需要应用到对应的对象方法中,这里我们推荐使用 Autofac 来将其对应的拦截器注入到系统服务中,这样每次就可以直接使用!

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()).ConfigureContainer<ContainerBuilder>(container =>
{
    //注册 IHttpContextAccessor(必须在 TokenService 之前)
    container.RegisterType<HttpContextAccessor>()
        .As<IHttpContextAccessor>()
        .SingleInstance();

    // 注册拦截器
    container.RegisterType<LogInterceptor>();

    // 注册服务并启用拦截
    //container.RegisterType<UserOperated>().As<IUserOperated>().EnableInterfaceInterceptors().InterceptedBy(typeof(LogInterceptor));
});

异步方法拦截方案

如果我们直接使用上面的方法进行拦截,会出现返回值不匹配的问题,故我们使用下面的方案进行拦截!

安装包:

Install-Package Castle.Core.AsyncInterceptor

定义一个拦截器基类(异步):

/// <summary>
/// 拦截器基类(异步版本)
/// </summary>
public abstract class BaseInterceptorAsync : IAsyncInterceptor
{
    /// <summary>
    /// 异步方法拦截(有返回值 Task<TResult>)
    /// </summary>
    /// <typeparam name="TResult">返回值类型</typeparam>
    /// <param name="invocation">被调用方法信息</param>
    public abstract void InterceptAsynchronous<TResult>(IInvocation invocation);

    /// <summary>
    /// 异步方法拦截(无返回值 Task)
    /// </summary>
    /// <param name="invocation">被调用方法信息</param>
    public abstract void InterceptAsynchronous(IInvocation invocation);

    /// <summary>
    /// 同步方法拦截
    /// </summary>
    /// <param name="invocation">被调用方法信息</param>
    public abstract void InterceptSynchronous(IInvocation invocation);
}
/// <summary>
/// 日志拦截器
/// </summary>
public class LogInterceptor : BaseInterceptorAsync
{
    public override void InterceptAsynchronous<TResult>(IInvocation invocation)
    {
        invocation.Proceed();
    }

    public override void InterceptAsynchronous(IInvocation invocation)
    {
        invocation.Proceed();
    }

    public override void InterceptSynchronous(IInvocation invocation)
    {
        invocation.Proceed();
    }
}

这样我们的异步方法拦截器就写法好了!

我们定义一个异步方法拦截器适配器:

/// <summary>
/// 异步方法拦截器适配器
/// </summary>
/// <typeparam name="T"></typeparam>
public class InterceptorAsyncAdapter<T> : IInterceptor where T : IAsyncInterceptor
{
    /// <summary>
    /// 目标对象拦截适配器
    /// </summary>
    private readonly T _asyncInterceptor;

    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="asyncInterceptor">目标对象拦截适配器</param>
    public InterceptorAsyncAdapter(T asyncInterceptor)
    {
        _asyncInterceptor = asyncInterceptor;
    }

    /// <summary>
    /// 拦截器适配器
    /// </summary>
    /// <param name="invocation">被拦截方法的信息</param>
    public void Intercept(IInvocation invocation)
    {
        var returnType = invocation.Method.ReturnType;

        // 异步方法
        if (typeof(Task).IsAssignableFrom(returnType))
        {
            if (returnType.IsGenericType)
            {
                // Task<TResult>
                var resultType = returnType.GetGenericArguments()[0];

                // 精确获取泛型方法定义
                var method = typeof(IAsyncInterceptor).GetMethods().First(m => m.Name == nameof(IAsyncInterceptor.InterceptAsynchronous) && m.IsGenericMethodDefinition);

                // 构造具体泛型方法并调用
                var genericMethod = method.MakeGenericMethod(resultType);
                genericMethod.Invoke(_asyncInterceptor, new object[] { invocation });
            }
            else
            {
                // Task(无返回值)
                _asyncInterceptor.InterceptAsynchronous(invocation);
            }
        }
        else
        {
            // 同步方法
            _asyncInterceptor.InterceptSynchronous(invocation);
        }
    }
}

然后我们在 系统 DI 注入 中写入该代码:

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()).ConfigureContainer<ContainerBuilder>(container =>
{
    //注册 IHttpContextAccessor(必须在 TokenService 之前)
    container.RegisterType<HttpContextAccessor>()
        .As<IHttpContextAccessor>()
        .SingleInstance();

    container.RegisterType<LogInterceptor>();

    // 注册适配器
    container.RegisterType<InterceptorAsyncAdapter<LogInterceptor>>();

    // 注册服务并启用拦截
    container.RegisterType<UserOperated>()
        .As<IUserOperated>()
        .EnableInterfaceInterceptors()
        .InterceptedBy(typeof(InterceptorAsyncAdapter<LogInterceptor>));
});

扩展:

这里将会补充一些额外的知识!

IInvocation 参数介绍

我们在拦截器和特性中会发现,方法的参数列表中有一个 IInvocation 类型的参数,下面将会进行解释:

属性类型说明
ReturnValueobject关键:目标方法执行后的返回值。你可以读取它,也可以替换/包装它。
MethodMethodInfo被拦截方法的反射信息(接口上的方法声明)。
MethodInvocationTargetMethodInfo目标实现类上的实际方法(当代理接口时,可能与 Method 不同)。
TargetTypeType被代理的实现类类型(如 UserService)。
InvocationTargetobject被代理的原始实例(非代理对象)。
Proxyobject代理对象本身(即外部调用者持有的那个对象)。
Argumentsobject[]方法调用时的参数数组。在 OnAfter 中通常只读,用于日志记录。
GenericArgumentsType[]泛型方法的类型参数(如 GetUser<T>() 中的 T)。
核心属性说明
方法说明
Proceed()调用链中的下一个拦截器或目标方法。在 OnAfter 中不应再调用,否则会导致方法重复执行。
GetArgumentValue(int)获取指定索引的参数值。
SetArgumentValue(int, object)修改参数值。在 OnAfter 中修改无意义(方法已执行完毕)。
核心方法

拦截器 / 特性中获取某些数据

当我们在 .NET Core Web API 中进行编程时,难免需要获取到 系统注入 DI 的对象或是在项目中该拦截器代码在类库A中,而我们某个必要的类对象在类库B中,但是他们都共同引用了类库C,这时候我们就可以使用类似于依赖倒置的思想!

首先我们需要在类库C中定义一个跟类库B中这个类对象的接口文件:

public interface ICalculator
{
    int Add(int a, int b);
    int Divide(int a, int b);
}

类库B中这个类对象的需要继承该接口:

public class Calculator : ICalculator
{
    public int Add(int a, int b) => a + b;
    public int Divide(int a, int b) => a / b;
}

定义类库B中这个类对象的相关上下文对象:

public static class CalculatorContext
{
    public static readonly AsyncLocal<ICalculator?> Current = new();
}

在程序的主入口设置上下文:

//...
CalculatorContext.Current.Value = calculator;
//...

//或者
var app = builder.Build();

// 从容器中获取并设置上下文
var calculator= app.Services.GetRequiredService<Calculator>();//...
CalculatorContext.Current.Value = calculator;

然后,我们可以定义一个扩展方法来获取他的对象数据:

/// <summary>
/// 获取系统注入服务对象
/// </summary>
/// <param name="invocation">调用函数的信息</param>
/// <param name="propertyName">对象属性</param>
/// <param name="_propertyName">对象属性(备用)</param>
/// <returns>目标对象</returns>
public static T? GetSystemDIServicesContext<T>(this IInvocation invocation, string propertyName, string _propertyName = "")
{
      // 优先从目标对象属性获取
      var engineProp = invocation.TargetType!.GetProperty(propertyName) ?? invocation.TargetType.GetProperty(_propertyName);

      if (engineProp != null)
      {
            var val = engineProp.GetValue(invocation.InvocationTarget);
            if (val is T obj) return obj;
      }

      return default(T);
}

使用:

var calculator = invocation.GetSystemDIServicesContext<IConfiguration>("CalculatorContext", "Calculator");

if (calculator == null) _engine = CalculatorContext.Current.Value;

https://wp.itdka.cn/1391.html
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容