拦截器


1、基本原理

  所谓拦截,实际上就是代码的注入,它是基于 Emit 技术(见 动态编译),动态将代码注入到一个类型所需要进行拦截的方法或属性的前后进行执行。

  IInterceptor 接口是 AOP 中的拦截器定义,它有以下两个方法:

  • Initialize 方法

  拦截器初始化时触发,其接收一个 InterceptContext 类型的参数,它包含所拦截的对象和成员。

  • Intercept 方法

  拦截器触发所拦截的事件,其接收一个 InterceptCallInfo 类型的参数,它包含所拦截的对象、拦截的成员、事件类型、返回值、异常信息等等,后面会作详细的说明。该方法会在被拦截的对象的方法或属性执行前后进行调用。


  下面举例说明一个方法注入代码后,其内部结构发生的变化,并以此加深对 AOP 的理解。如下所示:

public override double Calculate(double x, double y)
{
    var callInfo = new InterceptCallInfo();
    info.Target = this;
    info.Arguments = new object[] { x, y };

    //通知拦截器调用 BeforeMethodCall

    try
    {
        if (!info.Cancel)
        {
            info.ReturnValue = base.Calculate(x, y);
        }

        //通知拦截器调用 AfterMethodCall
    }
    catch (Exception exp)
    {
        info.Exception = exp;

        //通知拦截器调用 Catching
    }
    finally
    {
        //通知拦截器调用 Finally
    }
}

2、基本拦截

  首先定义一个类型,注意该类型不能是密封(sealed)的,且需要注入的方法或属性必须是可重写(virtual)的,并使用 InterceptAttribute 特性标记它所使用的拦截器。如下所示:

public class AopTester
{
    [Intercept(typeof(SampleInterceptor))]
    public virtual void HelloWorld()
    {
        Console.WriteLine("hello world");
    }
}

  定义拦截器 SampleInterceptor 类,编写方法拦截之前及之后的代码。如下所示:

public class SampleInterceptor : IInterceptor
{
    public virtual void Initialize(InterceptContext context)
    {
    }

    public virtual void Intercept(InterceptCallInfo info)
    {
        switch (info.InterceptType)
        {
            case InterceptType.BeforeMethodCall:
                Console.WriteLine("调用 {0} 方法之前", info.Member.Name);
                break;
            case InterceptType.AfterMethodCall:
                Console.WriteLine("调用 {0} 方法之后", info.Member.Name);
                break;
            case InterceptType.BeforeGetValue:
                Console.WriteLine("获取 {0} 属性之前", info.Member.Name);
                break;
            case InterceptType.AfterGetValue:
                Console.WriteLine("获取 {0} 属性之后", info.Member.Name);
                break;
            case InterceptType.BeforeSetValue:
                Console.WriteLine("设置 {0} 属性之前", info.Member.Name);
                break;
            case InterceptType.AfterSetValue:
                Console.WriteLine("设置 {0} 属性之后", info.Member.Name);
                break;
            case InterceptType.Catching:
                Console.WriteLine("{0} 发生了异常", info.Member.Name);
                break;
            case InterceptType.Finally:
                Console.WriteLine("{0} Finally", info.Member.Name);
                break;
        }
    }
}

3、处理更多事情

  拦截器可以拦截异常信息、改变拦截器的参数,以及返回值,甚至终止代码的执行。如果 InterceptAttribute 特性的 allowThrowException 参数指定为 false,则不会抛出异常。如下所示:

public class AopTester
{
    [Intercept(typeof(SampleInterceptor), false)]
    public virtual double Calculate(double x, double y)
    {
        if (x < 0 || y < 0)
        {
            throw new ArgumentException("x 或 y 超出范围");
        }
    }
}

public class SampleInterceptor : IInterceptor
{
    public void Initialize(InterceptContext context)
    {
    }

    public virtual void Intercept(InterceptCallInfo info)
    {
        var target = info.Target as AopTester;

        if (info.InterceptType == InterceptType.Catching) //拦截异常
        {
            info.ReturnValue = -1d; //指定返回值
        }
        else if (info.InterceptType == InterceptType。BeforeMethodCall)
        {
            if ((double)info.Arguments[0] < 90) //参数0 即 x
            {
                info.Cancel = true; //不会调用父类方法
                info.Break = true; //后续拦截器不会再调用
            }
        }
    }
}

  当指定 Cancel = true 后,被拦截的方法或属性不会调用父类的方法。同一方法可以指定多个拦截器,它们在同一事件通知时按顺序调用,当指定 Break = true 后,后续的拦截器将不会被调用执行。


4、异步方法

  异步方法被拦截时,为了方便对 InterceptCallInfo.ReturnValue 的处理,此值为 Task<T> 中的 Result,但是 InterceptCallInfo.ReturnType 仍然是 Task<T>,在使用时应注意。如下所示,在使用缓存管理器的 TryGet 方法时,不能误使用 InterceptCallInfo.ReturnType 作为泛型参数:

public class CacheInterceptor : IInterceptor
{
    private static MethodInfo MthCacheTryGet = typeof(ICacheManager).GetMethods().FirstOrDefault(s => s.Name == "TryGet" && s.GetParameters().Length == 3);

    public void Intercept(InterceptCallInfo info)
    {
        //执行方法前
        if (info.InterceptType == InterceptType.BeforeMethodCall)
        {
            CheckDataCache(info);
        }
        //执行方法后
        else if (info.InterceptType == InterceptType.AfterMethodCall)
        {
            UpdateDataCache(info);
        }
    }

    /// <summary>
    /// 检查数据缓存。
    /// </summary>
    /// <param name="info"></param>
    private void CheckDataCache(InterceptCallInfo info)
    {
        //判断是否加了 CacheSupportAttribute 特性
        if (info.Member.IsDefined<CacheSupportAttribute>())
        {
            var cacheMgr = CacheManagerFactory.CreateManager();
            var cacheKey = GetCacheKey(info);

            //检查缓存管理器里有没有对应的缓存项,如果有的话直接取出来赋给 ReturnValue,然后设置 Cancel 忽略方法调用
            if (cacheMgr.Contains(cacheKey))
            {
                //如果是返回类型是 Task<T> 则拿到 T
                var returnType = info.ReturnType.GetTaskResultType() : info.ReturnType;
                var method = MthCacheTryGet.MakeGenericMethod(returnType);
                info.ReturnValue = method.FastInvoke(cacheMgr, new object[] { cacheKey, null, null });

                if (info.ReturnValue != null)
                {
                    info.Cancel = true;
                }
            }
        }
    }

    /// <summary>
    /// 更新数据缓存。
    /// </summary>
    /// <param name="info"></param>
    private void UpdateDataCache(InterceptCallInfo info)
    {
        //判断是否加了 CacheSupportAttribute 特性
        if (info.Member.IsDefined<CacheSupportAttribute>())
        {
            var cacheMgr = CacheManagerFactory.CreateManager();
            var cacheKey = GetCacheKey(info);

            if (cacheMgr.Contains(cacheKey))
            {
                return;
            }

            var attr = info.Member.GetCustomAttribute<CacheSupportAttribute>();
            var relationTypes = info.Member.GetCustomAttributes<CacheRelationAttribute>().Select(s => s.RelationType).ToList();

            //如果是返回类型是 Task<T> 则拿到 T
            var returnType = info.ReturnType.GetTaskReturnType() ?? info.ReturnType;
            CacheKeyManager.AddKeys(returnType, cacheKey);

            if (relationTypes.Count > 0)
            {
                CacheKeyManager.AddKeys(relationTypes, cacheKey);
            }

            cacheMgr.Add(cacheKey, info.ReturnValue, TimeSpan.FromSeconds(attr.Expired));
        }
    }
}