Util应用框架基础(二)- 面向切面编程(AOP)
moboyou 2025-08-11 01:28 4 浏览
面向切面编程(AOP)
Util应用框架横切关注点处理
本节介绍Util应用框架对横切关注点的处理.
文章分为多个小节,如果对设计原理不感兴趣,只需阅读基础用法部分即可.
概述
有些问题需要在系统中全局处理,比如记录异常错误日志.
如果在每个出现问题的地方进行处理,不仅费力,还可能产生大量冗余代码,并打断业务逻辑的编写.
这类跨多个业务模块的非功能需求,被称为横切关注点.
我们需要把横切关注点集中管理起来.
Asp.Net Core 提供的过滤器可以处理这类需求.
过滤器有异常过滤器和操作过滤器等类型.
异常过滤器可以全局处理异常.
操作过滤器可以拦截控制器操作,在操作前和操作后执行特定代码.
过滤器很易用,但它必须配合控制器使用,所以只能解决部分问题.
你不能将过滤器特性打在应用服务的方法上,那不会产生作用.
我们需要引入一种类似 Asp.Net Core 过滤器的机制,在控制器范围外处理横切关注点.
AOP框架
AOP 是 Aspect Oriented Programming 的缩写,即面向切面编程.
AOP 框架提供了类似 Asp.Net Core 过滤器的功能,能够拦截方法,在方法执行前后插入自定义代码.
.Net AOP框架有动态代理和静态织入两种实现方式.
动态代理 AOP 框架
动态代理 AOP 框架在运行时动态创建代理类,从而为方法提供自定义代码插入点.
动态代理 AOP 框架有一些限制.
- 要拦截的方法必须在接口中定义,或是虚方法.
- 代理类过多,特别是启用了参数拦截,会导致启动性能下降.
.Net 动态代理 AOP 框架有Castle 和 AspectCore 等.
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 方法有两个参数 AspectContext 和 AspectDelegate.
- 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 {
}
- 上一篇:QQ假红包生成器(恶搞qq红包假红包生成)
- 已经是最后一篇了
相关推荐
- Util应用框架基础(二)- 面向切面编程(AOP)
-
面向切面编程(AOP)Util应用框架横切关注点处理本节介绍Util应用框架对横切关注点的处理.文章分为多个小节,如果对设计原理不感兴趣,只需阅读基础用法部分即可.概述有些问题需要在系统中全局处理,比...
- QQ假红包生成器(恶搞qq红包假红包生成)
-
软件名称:QQ假红包生成器软件大小:892.37KB运行平台:安卓软件说明:本软件由iapp编写,所以全被安全软件误报。作者也把软件免杀了,但免杀版无法在5.0系统的手机上安装,安装非免杀版即可...
- 如何用云虚拟主机搭建一个影视网站
-
大家好,今天教大家使用云虚拟主机搭建一个影视网站的教程,本教程使用的是苹果CMS系统搭建。清空虚拟主机在开始安装网站之前,先要清空下虚拟主机。因为虚拟主机开通成功后,一般都会有一个默认网页文件,直接...
- 小白快速掌握!目前最有效玩客云刷机法之玩客云刷魔改版iStoreOS
-
本内容来源于@什么值得买APP,观点仅代表作者本人|作者:最佳男煮角来来咯,我又来咯,为方便小白快速掌握刷机流程,我在每一篇文章里都是尽可能一步一图一解读的,而最最基础的准备直接点击我的文章《什么,...
- AI七个月突破数学家“围剿”反超人类,14位数学家深挖原始推理token:不靠死记硬背靠直觉
-
从只能答对2%的题目,到在超难数学题集中刷下22%得分,甚至超过人类团队平均水平,大模型需要多长时间?现在,令数学家们都惊讶的结果已经尘埃落定:7个月。发生在大名鼎鼎的“专为为难大模型而生的”Fron...
- 1.5B刷新数学代码SOTA!快手清华精细Token管理,LLM推理能力飙升
-
Archer团队投稿量子位|公众号QbitAI当大模型在数学题和代码任务里“卷”参数规模时,一支来自快手和清华的团队给出了不同答案——他们用1.5B参数的小模型,在多个推理基准上干过了同量级S...
- 抢票软件刷走200张,黄牛代码篡改系统,看球还敢信“代抢”吗?
-
7月4日晚无锡警方抓了两个人,他们用电脑程序抢了200多张苏超比赛门票,赚了1万5。现在苏超比赛场场爆满,抢票软件、黄牛倒票成了大问题。网上有人晒高价票,有人说自己被“代抢”骗了钱。黄牛倒票分三类:有...
- 大模型转行土木工程,首个「打灰人」评估基准:检验读、改工程图纸能力
-
首个工程自动化任务评估基准DrafterBench,可用于测试大语言模型在土木工程图纸修改任务中的表现。通过模拟真实工程命令,全面考察模型的结构化数据理解、工具调用、指令跟随和批判性推理能力,研究结...
- 仅用一行代码实现全网站暗黑模式(仅用一行代码实现全网站暗黑模式怎么办)
-
开源项目推荐:uViewPro正式开源!70+Vue3组件重构完成,uni-app组件库新选择推荐文章:element-plus同款主题换肤动画如何实现?-附完整源码经历了PMP和软...
- “少写一行代码,5分钟狂刷一次下载,开发者8000美元就这么烧没了!”
-
只因为漏写了一行代码,macOS录屏工具ScreenStudio的开发者AdamPietrasiak意外烧掉了8000美元的流量费,连带着还让部分用户断了网。一个看起来微不足道的自动...
- IE法提取网页数据(如何提取网页中的表格)
-
【分享成果,随喜正能量】我们不良的行为、不善的念头、不好的言语,都属于我们内心的暴力,要想获得平和,我们需要自我修习,学会控制情绪,学会内观反省并接近善良。。《VBA信息获取与处理》教程是我推出第六套...
- 从abc起步学做网站(4)(abc是哪几个网站)
-
上一次我们做了一个大多数网站使用的模板,带有顶部、左侧导航、右侧主内容、底部等板块。现在我们把它逐步扩展成一个论坛。一个论坛的基本功能有注册,登陆,发帖,回帖,看帖,删帖等,我们一步步来做。首先大多数...
- 黑客命令第16集:47种最常见的**网站方法2/2
-
31.工具1:网站猎手2:大马一个关键字:切勿关闭Cookies功能,否则您将不能登录插入diy.asp32.关键字:Team5StudioAllrightsreserved默认数据库:dat...
- 安卓最有名的网页编辑器(安卓网页设计工具)
-
安卓平台上有几款较为知名的网页编辑器,如Quoda、DroidEdit等,它们凭借丰富的功能和良好的用户体验受到广泛关注,以下是具体介绍:-Quoda:是一款强大的免费多语言代码编辑器,支持HTM...
- 918国际导航免费分享一款简洁模板+整站程序+数据打包-yungui
-
918国际导航免费分享一款简洁模板+整站程序+数据打包国际导航模板说明:搜索栏背景是动态型的,为了简洁本站没有搜索框以及文章页,只有首页一个,这样可以满足大部分人的需求了,毕竟大家只是大家导航而已...
- 一周热门
- 最近发表
-
- Util应用框架基础(二)- 面向切面编程(AOP)
- QQ假红包生成器(恶搞qq红包假红包生成)
- 如何用云虚拟主机搭建一个影视网站
- 小白快速掌握!目前最有效玩客云刷机法之玩客云刷魔改版iStoreOS
- AI七个月突破数学家“围剿”反超人类,14位数学家深挖原始推理token:不靠死记硬背靠直觉
- 1.5B刷新数学代码SOTA!快手清华精细Token管理,LLM推理能力飙升
- 抢票软件刷走200张,黄牛代码篡改系统,看球还敢信“代抢”吗?
- 大模型转行土木工程,首个「打灰人」评估基准:检验读、改工程图纸能力
- 仅用一行代码实现全网站暗黑模式(仅用一行代码实现全网站暗黑模式怎么办)
- “少写一行代码,5分钟狂刷一次下载,开发者8000美元就这么烧没了!”
- 标签列表
-
- 外键约束 oracle (36)
- oracle的row number (32)
- 唯一索引 oracle (34)
- oracle in 表变量 (28)
- oracle导出dmp导出 (28)
- oracle两个表 (20)
- oracle 数据库 字符集 (20)
- matlab化简多项式 (20)
- 多线程的创建方式 (29)
- 多线程 python (30)
- java多线程并发处理 (32)
- 宏程序代码一览表 (35)
- c++需要学多久 (25)
- css class选择器用法 (25)
- css样式引入 (30)
- css教程文字移动 (33)
- php简单源码 (36)
- php个人中心源码 (25)
- php小说爬取源码 (23)
- 云电脑app源码 (22)
- html画折线图 (24)
- docker好玩的应用 (28)
- linux有没有pe工具 (34)
- mysql数据库源码 (21)
- php开源万能表单系统源码 (21)