本节介绍Util应用框架对AspectCore AOP的使用.

概述

有些问题需要在系统中全局处理,比如记录异常错误日志.

如果在每个出现问题的地方进行处理,不仅费力,还可能产生大量冗余代码,并打断业务逻辑的编写.

这类跨多个业务模块的非功能需求,被称为横切关注点.

我们需要把横切关注点集中管理起来.

Asp.Net Core 提供的过滤器可以处理这类需求.

过滤器有异常过滤器和操作过滤器等类型.

异常过滤器可以全局处理异常.

操作过滤器可以拦截控制器操作,在操作前和操作后执行特定代码.

过滤器很易用,但它必须配合控制器使用,所以只能解决部分问题.

你不能将过滤器特性打在应用服务的方法上,那不会产生作用.

我们需要引入一种类似 Asp.Net Core 过滤器的机制,在控制器范围外处理横切关注点.

AOP框架

AOP 是 Aspect Oriented Programming 的缩写,即面向切面编程.

AOP 框架提供了类似 Asp.Net Core 过滤器的功能,能够拦截方法,在方法执行前后插入自定义代码.

.Net AOP框架有动态代理静态织入两种实现方式.

动态代理 AOP 框架

动态代理 AOP 框架在运行时动态创建代理类,从而为方法提供自定义代码插入点.

动态代理 AOP 框架有一些限制.

  • 要拦截的方法必须在接口中定义,或是虚方法.

  • 代理类过多,特别是启用了参数拦截,会导致启动性能下降.

.Net 动态代理 AOP 框架有CastleAspectCore 等.

Util应用框架使用 AspectCore ,选择 AspectCore 是因为它更加易用.

Util 对 AspectCore 仅简单包装.

静态织入 AOP 框架

静态织入 AOP 框架在编译时修改.Net IL中间代码.

与动态代理AOP相比,静态织入AOP框架有一些优势.

  • 不必是虚方法.

  • 支持静态方法.

  • 更高的启动性能.

但是成熟的 .Net 静态织入 AOP 框架大多是收费的.

Rougamo.Fody 是一个免费的静态织入 AOP 框架,可以关注.

基础用法

引用Nuget包

Nuget包名: Util.Aop.AspectCore

启用Aop

需要明确调用 AddAop 扩展方法启用 AOP 服务.

var builder = WebApplication.CreateBuilder( args );
builder.AsBuild().AddAop();

使用要点

  • 定义服务接口

    如果使用抽象基类,应将需要拦截的方法设置为虚方法.

  • 配置服务接口的依赖注入关系

    AspectCore AOP依赖Ioc对象容器,只有在对象容器中注册的服务接口才能创建服务代理.

  • 将方法拦截器放在接口方法上.

    AspectCore AOP拦截器是一种.Net特性 Attribute,遵循 Attribute 使用约定.

    下面的例子将 CacheAttribute 方法拦截器添加到 ITestService 接口的 Test 方法上.

    注意: 应将拦截器放在接口方法上,而不是实现类上.

    按照约定, CacheAttribute 需要去掉 Attribute 后缀,并放到 [] 中.

    public interface ITestService : ISingletonDependency {
    [Cache]
    List<string> Test( string value );
    }
  • 将参数拦截器放在接口方法参数上.

    AspectCore AOP 支持拦截特定参数.

    下面的例子在参数 value 上施加了 NotNullAttribute 参数拦截器.

      public interface ITestService : ISingletonDependency {
    void Test( [NotNull] string value );
    }

Util内置拦截器

Util应用框架使用 Asp.Net Core 过滤器处理全局异常,全局错误日志,授权等需求,仅定义少量 AOP 拦截器.

Util应用框架定义了几个参数拦截器,用于验证.

  • NotNullAttribute

    • 验证是否为 null,如果为 null 抛出 ArgumentNullException 异常.

    • 使用范例:

      public interface ITestService : ISingletonDependency {
    void Test( [NotNull] string value );
    }
  • NotEmptyAttribute

    • 使用 string.IsNullOrWhiteSpace 验证是否为空字符串,如果为空则抛出 ArgumentNullException 异常.

    • 使用范例:

      public interface ITestService : ISingletonDependency {
    void Test( [NotEmpty] string value );
    }
  • ValidAttribute

    • 如果对象实现了 IValidation 验证接口,则自动调用对象的 Validate 方法进行验证.

      Util应用框架实体,值对象,DTO等基础对象均已实现 IValidation 接口.

    • 使用范例:

      验证单个对象.

      public interface ITestService : ISingletonDependency {
    void Test( [Valid] CustomerDto dto );
    }

    验证对象集合.

      public interface ITestService : ISingletonDependency {
    void Test( [Valid] List<CustomerDto> dto );
    }

Util应用框架为缓存定义了方法拦截器.

  • CacheAttribute

    • 使用范例:
      public interface ITestService : ISingletonDependency {
    [Cache]
    List<string> Test( string value );
    }

禁止创建服务代理

有些时候,你不希望为某些接口创建代理类.

使用 Util.Aop.IgnoreAttribute 特性标记接口即可.

下面演示了从 AspectCore AOP 排除工作单元接口.

[Util.Aop.Ignore]
public interface IUnitOfWork {
Task<int> CommitAsync();
}

创建自定义拦截器

除了内置的拦截器外,你可以根据需要创建自定义拦截器.

创建方法拦截器

继承 Util.Aop.InterceptorBase 基类,重写 Invoke 方法.

下面以缓存拦截器为例讲解创建方法拦截器的要点.

  • 缓存拦截器获取 ICache 依赖服务并创建缓存键.

  • 通过缓存键和返回类型查找缓存是否存在.

  • 如果缓存已经存在,则设置返回值,不需要执行拦截的方法.

  • 如果缓存不存在,执行方法获取返回值并设置缓存.

Invoke 方法有两个参数 AspectContextAspectDelegate.

  • AspectContext上下文提供了方法元数据信息和服务提供程序.

    • 使用 AspectContext 上下文获取方法元数据.

      AspectContext 上下文提供了拦截方法相关的大量元数据信息.

      本例使用 context.ServiceMethod.ReturnType 获取返回类型.

    • 使用 AspectContext 上下文获取依赖的服务.

      AspectContext上下文提供了 ServiceProvider 服务提供器,可以使用它获取依赖服务.

      本例需要获取缓存操作接口 ICache ,使用 context.ServiceProvider.GetService<ICache>() 获取依赖.

  • AspectDelegate表示拦截的方法.

    await next( context ); 执行拦截方法.

    如果需要在方法执行前插入自定义代码,只需将代码放在 await next( context ); 之前即可.

/// <summary>
/// 缓存拦截器
/// </summary>
public class CacheAttribute : InterceptorBase {
/// <summary>
/// 缓存键前缀
/// </summary>
public string CacheKeyPrefix { get; set; }
/// <summary>
/// 缓存过期间隔,单位:秒,默认值:36000
/// </summary>
public int Expiration { get; set; } = 36000; /// <summary>
/// 执行
/// </summary>
public override async Task Invoke( AspectContext context, AspectDelegate next ) {
var cache = GetCache( context );
var returnType = GetReturnType( context );
var key = CreateCacheKey( context );
var value = await GetCacheValue( cache, returnType, key );
if ( value != null ) {
SetReturnValue( context, returnType, value );
return;
}
await next( context );
await SetCache( context, cache, key );
} /// <summary>
/// 获取缓存服务
/// </summary>
protected virtual ICache GetCache( AspectContext context ) {
return context.ServiceProvider.GetService<ICache>();
} /// <summary>
/// 获取返回类型
/// </summary>
private Type GetReturnType( AspectContext context ) {
return context.IsAsync() ? context.ServiceMethod.ReturnType.GetGenericArguments().First() : context.ServiceMethod.ReturnType;
} /// <summary>
/// 创建缓存键
/// </summary>
private string CreateCacheKey( AspectContext context ) {
var keyGenerator = context.ServiceProvider.GetService<ICacheKeyGenerator>();
return keyGenerator.CreateCacheKey( context.ServiceMethod, context.Parameters, CacheKeyPrefix );
} /// <summary>
/// 获取缓存值
/// </summary>
private async Task<object> GetCacheValue( ICache cache, Type returnType, string key ) {
return await cache.GetAsync( key, returnType );
} /// <summary>
/// 设置返回值
/// </summary>
private void SetReturnValue( AspectContext context, Type returnType, object value ) {
if ( context.IsAsync() ) {
context.ReturnValue = typeof( Task ).GetMethods()
.First( p => p.Name == "FromResult" && p.ContainsGenericParameters )
.MakeGenericMethod( returnType ).Invoke( null, new[] { value } );
return;
}
context.ReturnValue = value;
} /// <summary>
/// 设置缓存
/// </summary>
private async Task SetCache( AspectContext context, ICache cache, string key ) {
var options = new CacheOptions { Expiration = TimeSpan.FromSeconds( Expiration ) };
var returnValue = context.IsAsync() ? await context.UnwrapAsyncReturnValue() : context.ReturnValue;
await cache.SetAsync( key, returnValue, options );
}
}

创建参数拦截器

继承 Util.Aop.ParameterInterceptorBase 基类,重写 Invoke 方法.

与方法拦截器类似, Invoke 也提供了两个参数 ParameterAspectContext 和 ParameterAspectDelegate.

ParameterAspectContext 上下文提供方法元数据.

ParameterAspectDelegate 表示拦截的方法.

下面演示了 [NotNull] 参数拦截器.

在方法执行前判断参数是否为 null,如果为 null 抛出异常,不会执行拦截方法.

/// <summary>
/// 验证参数不能为null
/// </summary>
public class NotNullAttribute : ParameterInterceptorBase {
/// <summary>
/// 执行
/// </summary>
public override Task Invoke( ParameterAspectContext context, ParameterAspectDelegate next ) {
if( context.Parameter.Value == null )
throw new ArgumentNullException( context.Parameter.Name );
return next( context );
}
}

性能优化

AddAop 配置方法默认不带参数,所有添加到 Ioc 容器的服务都会创建代理类,并启用参数拦截器.

AspectCore AOP 参数拦截器对启动性能有很大的影响.

默认配置适合规模较小的项目.

当你在Ioc容器注册了上千个甚至更多的服务时,启动时间将显著增长,因为启动时需要创建大量的代理类.

有几个方法可以优化 AspectCore AOP 启动性能.

  • 拆分项目

    对于微服务架构,单个项目包含的接口应该不会特别多.

    如果发现由于创建代理类导致启动时间过长,可以拆分项目.

    但对于单体架构,不能通过拆分项目的方式解决.

  • 减少创建的代理类.

    Util定义了一个AOP标记接口 IAopProxy ,只有继承了 IAopProxy 的接口才会创建代理类.

    要启用 IAopProxy 标记接口,只需向 AddAop 传递 true .

      var builder = WebApplication.CreateBuilder( args );
    builder.AsBuild().AddAop( true );

    现在只有明确继承自 IAopProxy 的接口才会创建代理类,代理类的数量将大幅减少.

    应用服务和领域服务接口默认继承了 IAopProxy.

    如果你在其它构造块使用了拦截器,比如仓储,需要让你的仓储接口继承 IAopProxy.

  • 禁用参数拦截器.

    如果启用了 IAopProxy 标记接口,启动性能依然未达到你的要求,可以禁用参数拦截器.

    AddAop 扩展方法支持传入 Action<IAspectConfiguration> 参数,可以覆盖默认设置.

    下面的例子禁用了参数拦截器,并为所有继承了 IAopProxy 的接口创建代理.

      var builder = WebApplication.CreateBuilder( args );
    builder.AsBuild().AddAop( options => options.NonAspectPredicates.Add( t => !IsProxy( t.DeclaringType ) ) ); /// <summary>
    /// 是否创建代理
    /// </summary>
    private static bool IsProxy( Type type ) {
    if ( type == null )
    return false;
    var interfaces = type.GetInterfaces();
    if ( interfaces == null || interfaces.Length == 0 )
    return false;
    foreach ( var item in interfaces ) {
    if ( item == typeof( IAopProxy ) )
    return true;
    }
    return false;
    }

源码解析

AppBuilderExtensions

扩展了 AddAop 配置方法.

isEnableIAopProxy 参数用于启用 IAopProxy 标记接口.

Action<IAspectConfiguration> 参数用于覆盖默认配置.

/// <summary>
/// Aop配置扩展
/// </summary>
public static class AppBuilderExtensions {
/// <summary>
/// 启用AspectCore拦截器
/// </summary>
/// <param name="builder">应用生成器</param>
public static IAppBuilder AddAop( this IAppBuilder builder ) {
return builder.AddAop( false );
} /// <summary>
/// 启用AspectCore拦截器
/// </summary>
/// <param name="builder">应用生成器</param>
/// <param name="isEnableIAopProxy">是否启用IAopProxy接口标记</param>
public static IAppBuilder AddAop( this IAppBuilder builder,bool isEnableIAopProxy ) {
return builder.AddAop( null, isEnableIAopProxy );
} /// <summary>
/// 启用AspectCore拦截器
/// </summary>
/// <param name="builder">应用生成器</param>
/// <param name="setupAction">AspectCore拦截器配置操作</param>
public static IAppBuilder AddAop( this IAppBuilder builder, Action<IAspectConfiguration> setupAction ) {
return builder.AddAop( setupAction, false );
} /// <summary>
/// 启用AspectCore拦截器
/// </summary>
/// <param name="builder">应用生成器</param>
/// <param name="setupAction">AspectCore拦截器配置操作</param>
/// <param name="isEnableIAopProxy">是否启用IAopProxy接口标记</param>
private static IAppBuilder AddAop( this IAppBuilder builder, Action<IAspectConfiguration> setupAction, bool isEnableIAopProxy ) {
builder.CheckNull( nameof( builder ) );
builder.Host.UseServiceProviderFactory( new DynamicProxyServiceProviderFactory() );
builder.Host.ConfigureServices( ( context, services ) => {
ConfigureDynamicProxy( services, setupAction, isEnableIAopProxy );
RegisterAspectScoped( services );
} );
return builder;
} /// <summary>
/// 配置拦截器
/// </summary>
private static void ConfigureDynamicProxy( IServiceCollection services, Action<IAspectConfiguration> setupAction, bool isEnableIAopProxy ) {
services.ConfigureDynamicProxy( config => {
if ( setupAction == null ) {
config.NonAspectPredicates.Add( t => !IsProxy( t.DeclaringType, isEnableIAopProxy ) );
config.EnableParameterAspect();
return;
}
setupAction.Invoke( config );
} );
} /// <summary>
/// 是否创建代理
/// </summary>
private static bool IsProxy( Type type, bool isEnableIAopProxy ) {
if ( type == null )
return false;
if ( isEnableIAopProxy == false ) {
if ( type.SafeString().Contains( "Xunit.DependencyInjection.ITestOutputHelperAccessor" ) )
return false;
return true;
}
var interfaces = type.GetInterfaces();
if ( interfaces == null || interfaces.Length == 0 )
return false;
foreach ( var item in interfaces ) {
if ( item == typeof( IAopProxy ) )
return true;
}
return false;
} /// <summary>
/// 注册拦截器服务
/// </summary>
private static void RegisterAspectScoped( IServiceCollection services ) {
services.AddScoped<IAspectScheduler, ScopeAspectScheduler>();
services.AddScoped<IAspectBuilderFactory, ScopeAspectBuilderFactory>();
services.AddScoped<IAspectContextFactory, ScopeAspectContextFactory>();
}
}

Util.Aop.IAopProxy

IAopProxy 是一个标记接口,继承了它的接口才会创建代理类.

/// <summary>
/// Aop代理标记
/// </summary>
public interface IAopProxy {
}

Util.Aop.InterceptorBase

InterceptorBase 是方法拦截器基类.

它是一个简单抽象层, 未来可能提供一些共享方法.

/// <summary>
/// 拦截器基类
/// </summary>
public abstract class InterceptorBase : AbstractInterceptorAttribute {
}

Util.Aop.ParameterInterceptorBase

ParameterInterceptorBase 是参数拦截器基类.

/// <summary>
/// 参数拦截器基类
/// </summary>
public abstract class ParameterInterceptorBase : ParameterInterceptorAttribute {
}

Util.Aop.IgnoreAttribute

[Util.Aop.Ignore] 用于禁止创建代理类.

/// <summary>
/// 忽略拦截
/// </summary>
public class IgnoreAttribute : NonAspectAttribute {
}

Util应用框架基础(三) - 面向切面编程(AspectCore AOP)的更多相关文章

  1. Spring(三)面向切面编程(AOP)

    在直系学长曾经的指导下,参考了直系学长的博客(https://www.cnblogs.com/WellHold/p/6655769.html)学习Spring的另一个核心概念--面向切片编程,即AOP ...

  2. 依赖注入(DI)有助于应用对象之间的解耦,而面向切面编程(AOP)有助于横切关注点与所影响的对象之间的解耦(转good)

    依赖注入(DI)有助于应用对象之间的解耦,而面向切面编程(AOP)有助于横切关注点与所影响的对象之间的解耦.所谓横切关注点,即影响应用多处的功能,这些功能各个应用模块都需要,但又不是其主要关注点,常见 ...

  3. C# 中使用面向切面编程(AOP)中实践代码整洁

    1. 前言 最近在看<架构整洁之道>一书,书中反复提到了面向对象编程的 SOLID 原则(在作者的前一本书<代码整洁之道>也是被大力阐释),而面向切面编程(Aop)作为面向对象 ...

  4. C# 中使用面向切面编程(AOP)中实践代码整洁(转)

    出处:https://www.cnblogs.com/chenug/p/9848852.html 1. 前言 最近在看<架构整洁之道>一书,书中反复提到了面向对象编程的 SOLID 原则( ...

  5. Spring——面向切面编程(AOP)详解

    声明:本博客仅仅是一个初学者的学习记录.心得总结,其中肯定有许多错误,不具有参考价值,欢迎大佬指正,谢谢!想和我交流.一起学习.一起进步的朋友可以加我微信Liu__66666666 这是简单学习一遍之 ...

  6. Java中的面向切面编程(AOP)

    一.什么是AOP? Aspect Oriented Programming ,即面向切面编程. AOP是对面向对象编程的一个补充. 它的目的是将复杂的需求分解为不同的切面,将散布在系统中的公共功能集中 ...

  7. 面向切面编程(AOP)及其作用

    在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用. 1.面向切面编程(AOP) 面向切面编程(AOP)就是对软件系统不同关注点的分离,开发者通过拦截方法调用并在方法调用前后添加辅助代码. ...

  8. 面向切面编程 (AOP )

    什么是面向切面编程? 面向切面编程就是(AOP --- aspect-oriented programming), 在百科上说: 面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一 ...

  9. Spring 面向切面编程(AOP)

    Spring 系列教程 Spring 框架介绍 Spring 框架模块 Spring开发环境搭建(Eclipse) 创建一个简单的Spring应用 Spring 控制反转容器(Inversion of ...

  10. (转存)面向切面编程(AOP)的理解

    面向切面编程(AOP)的理解 标签: aop编程 2010-06-14 20:17 45894人阅读 评论(11) 收藏 举报  分类: Spring(9)  在传统的编写业务逻辑处理代码时,我们通常 ...

随机推荐

  1. ch-manager.sh

    [root@dev-clickhouse1 ~]# cat ch-manager.sh #!/bin/bash ch_arr=(ch1-shard1-main ch1-shard2-sub ch2-s ...

  2. Qt 生成应用程序(二)软件多图标与文件操作

    目录 关联某种文件的默认打开方式 assoc ftype 解决方案 设置文件默认图标 应用软件添加多个图标 综合方法 嘿,各位Qt桌面应用开发的同学们(应该Qt大部分应用场景就是这个吧),上一篇文章中 ...

  3. git:gitignore常用配置

    配置 在项目文件中添加.gitignore文件 .DS_Store node_modules /dist

  4. tensorflow.js 多分类,机器学习区分企鹅种类

    前言: 在规则编码中,我们常常会遇到需要通过多种区间判断某种物品分类.比如二手物品的定价,尽管不是新品没有 SKU 但是基本的参数是少不了.想通过成色来区分某种物品,其实主要是确定一些参数.然后根据参 ...

  5. 用 Python 自动创建 Markdown 表格 - 每天5分钟玩转 GPT 编程系列(4)

    目录 1. 他们居然问我要 Prompts 2. 让 GPT-4 来写代码 2.1 我对 DevChat 说 2.2 DevChat 回答 2.3 我又对 DevChat 说 2.4 DevChat ...

  6. Android13冻结进程分析:如何提高设备性能和用户体验

    Android13冻结进程分析:如何提高设备性能和用户体验 本文介绍了Android13中的冻结进程功能,它是一种重要的资源管理策略,可以提高系统性能和稳定性,同时最大限度地节省设备的资源和电池消耗. ...

  7. 概率dp_C++详解

    引入 概率 DP 用于解决概率问题与期望问题,建议先对概率和期望的内容有一定了解.一般情况下,解决概率问题需要顺序循环,而解决期望问题使用逆序循环,如果定义的状态转移方程存在后效性问题,还需要用到 高 ...

  8. Web通用漏洞--sql注入

    SQL注入 mysql注入目的:获取当前web权限 mysql注入--常规查询&union联合查询 MYSQL--Web组成架构 服务器搭建web服务可能存在多个站点搭建在一台服务器中,数据集 ...

  9. Azure Storage 系列(八)存储类型细化分类说明

    一,引言 Azure 存储账户功能经过官方改进迭代后,在创建的时候,存储账户的类型被分为两大类: 1)general-purpose v2 account(标准常规用途v2) Blob 存储,队列存储 ...

  10. 纯前端导出word手写复杂表格,并还原成word。百分百还原表格。一文搞定前端表格导出为word

    本次的需求是手写一个养老院老人生活能力评定表,并且要能够录入信息,最终导出 表格因为有七页所以代码很多,可以不用看表格模板的详细代码. 先贴上最终效果图 填写完导出之后 基本上实现了样式的百分百还原导 ...