[Abp 源码分析]十五、自动审计记录

目录 0.简介 1.启动流程 2.1 过滤器注入 2.2 拦截器注入 2.代码分析 2.1 过滤器代码分析 2.2 拦截器代码分析 2.3 核心的 IAuditingHelper 2.4 审计信息持久化 3. 后记 4.点此跳转到总目录 正文 回到顶部 0.简介 Abp 框架为我们自带了审计日志功能,审计日志可以方便地查看每次请求接口所耗的时间,能够帮助我们快速定位到某些性能有问题的接口。除此之外,审计日志信息还包含有每次调用接口时客户端请求的参数信息,客户端的 IP 与客户端使用的浏览器。有了这些数据之后,我们就可以很方便地复现接口产生 BUG 时的一些环境信息。 当然如果你脑洞更大的话,可以根据这些数据来开发一个可视化的图形界面,方便开发与测试人员来快速定位问题。 PS: 如果使用了 Abp.Zero 模块则自带的审计记录实现是存储到数据库当中的,但是在使用 EF Core + MySQL(EF Provider 为 Pomelo.EntityFrameworkCore.MySql) 在高并发的情况下会有数据库连接超时的问题,这块推荐是重写实现,自己采用 Redis 或者其他存储方式。 如果需要禁用审计日志功能,则需要在任意模块的预加载方法(PreInitialize()) 当中增加如下代码关闭审计日志功能。 public class XXXStartupModule { public override PreInitialize() { // 禁用审计日志 Configuration.Auditing.IsEnabled = false; } } 回到顶部 1.启动流程 审计组件与参数校验组件一样,都是通过 MVC 过滤器与 Castle 拦截器来实现记录的。也就是说,在每次调用接口/方法时都会进入 过滤器/拦截器 并将其写入到数据库表 AbpAuditLogs 当中。 其核心思想十分简单,就是在执行具体接口方法的时候,先使用 StopWatch 对象来记录执行完一个方法所需要的时间,并且还能够通过 HttpContext 来获取到一些客户端的关键信息。 2.1 过滤器注入 同上一篇文章所讲的一样,过滤器是在 AddAbp() 方法内部的 ConfigureAspNetCore() 方法注入的。 private static void ConfigureAspNetCore(IServiceCollection services, IIocResolver iocResolver) { // ... 其他代码 //Configure MVC services.Configure(mvcOptions => { mvcOptions.AddAbp(services); }); // ... 其他代码 } 而下面就是过滤器的注入方法: internal static class AbpMvcOptionsExtensions { public static void AddAbp(this MvcOptions options, IServiceCollection services) { // ... 其他代码 AddFilters(options); // ... 其他代码 } // ... 其他代码 private static void AddFilters(MvcOptions options) { // ... 其他过滤器注入 // 注入审计日志过滤器 options.Filters.AddService(typeof(AbpAuditActionFilter)); // ... 其他过滤器注入 } // ... 其他代码 } 2.2 拦截器注入 注入拦截器的地方与 DTO 自动验证的拦截器的位置一样,都是在 AbpBootstrapper 对象被构造的时候进行注册。 public class AbpBootstrapper : IDisposable { private AbpBootstrapper([NotNull] Type startupModule, [CanBeNull] Action optionsAction = null) { // ... 其他代码 if (!options.DisableAllInterceptors) { AddInterceptorRegistrars(); } } // ... 其他代码 // 添加各种拦截器 private void AddInterceptorRegistrars() { ValidationInterceptorRegistrar.Initialize(IocManager); AuditingInterceptorRegistrar.Initialize(IocManager); EntityHistoryInterceptorRegistrar.Initialize(IocManager); UnitOfWorkRegistrar.Initialize(IocManager); AuthorizationInterceptorRegistrar.Initialize(IocManager); } // ... 其他代码 } 转到 AuditingInterceptorRegistrar 的具体实现可以发现,他在内部针对于审计日志拦截器的注入是区分了类型的。 internal static class AuditingInterceptorRegistrar { public static void Initialize(IIocManager iocManager) { iocManager.IocContainer.Kernel.ComponentRegistered += (key, handler) => { // 如果审计日志配置类没有被注入,则直接跳过 if (!iocManager.IsRegistered()) { return; } var auditingConfiguration = iocManager.Resolve(); // 判断当前 DI 所注入的类型是否应该为其绑定审计日志拦截器 if (ShouldIntercept(auditingConfiguration, handler.ComponentModel.Implementation)) { handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(AuditingInterceptor))); } }; } // 本方法主要用于判断当前类型是否符合绑定拦截器的条件 private static bool ShouldIntercept(IAuditingConfiguration auditingConfiguration, Type type) { // 首先判断当前类型是否在配置类的注册类型之中,如果是,则进行拦截器绑定 if (auditingConfiguration.Selectors.Any(selector => selector.Predicate(type))) { return true; } // 当前类型如果拥有 Audited 特性,则进行拦截器绑定 if (type.GetTypeInfo().IsDefined(typeof(AuditedAttribute), true)) { return true; } // 如果当前类型内部的所有方法当中有一个方法拥有 Audited 特性,则进行拦截器绑定 if (type.GetMethods().Any(m => m.IsDefined(typeof(AuditedAttribute), true))) { return true; } // 都不满足则返回 false,不对当前类型进行绑定 return false; } } 可以看到在判断是否绑定拦截器的时候,Abp 使用了 auditingConfiguration.Selectors 的属性来进行判断,那么默认 Abp 为我们添加了哪些类型是必定有审计日志的呢? 通过代码追踪,我们来到了 AbpKernalModule 类的内部,在其预加载方法里面有一个 AddAuditingSelectors() 的方法,该方法的作用就是添加了一个针对于应用服务类型的一个选择器对象。 public sealed class AbpKernelModule : AbpModule { public override void PreInitialize() { // ... 其他代码 AddAuditingSelectors(); // ... 其他代码 } // ... 其他代码 private void AddAuditingSelectors() { Configuration.Auditing.Selectors.Add( new NamedTypeSelector( "Abp.ApplicationServices", type => typeof(IApplicationService).IsAssignableFrom(type) ) ); } // ... 其他代码 } 我们先看一下 NamedTypeSelector 的一个作用是什么,其基本类型定义由一个 string 和 Func 组成,十分简单,重点就出在这个断言委托上面。 public class NamedTypeSelector { // 选择器名称 public string Name { get; set; } // 断言委托 public Func Predicate { get; set; } public NamedTypeSelector(string name, Func predicate) { Name = name; Predicate = predicate; } } 回到最开始的地方,当 Abp 为 Selectors 添加了一个名字为 "Abp.ApplicationServices" 的类型选择器。其断言委托的大体意思就是传入的 type 参数是继承自 IApplicationService 接口的话,则返回 true,否则返回 false。 这样在程序启动的时候,首先注入类型的时候,会首先进入上文所述的拦截器绑定类当中,这个时候会使用 Selectors 内部的类型选择器来调用这个集合内部的断言委托,只要这些选择器对象有一个返回 true,那么就直接与当前注入的 type 绑定拦截器。 回到顶部 2.代码分析 2.1 过滤器代码分析 首先查看这个过滤器的整体类型结构,一个标准的过滤器,肯定要实现 IAsyncActionFilter 接口。从下面的代码我们可以看到其注入了 IAbpAspNetCoreConfiguration 和一个 IAuditingHelper 对象。这两个对象的作用分别是判断是否记录日志,另一个则是用来真正写入日志所使用的。 public class AbpAuditActionFilter : IAsyncActionFilter, ITransientDependency { // 审计日志组件配置对象 private readonly IAbpAspNetCoreConfiguration _configuration; // 真正用来写入审计日志的工具类 private readonly IAuditingHelper _auditingHelper; public AbpAuditActionFilter(IAbpAspNetCoreConfiguration configuration, IAuditingHelper auditingHelper) { _configuration = configuration; _auditingHelper = auditingHelper; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // ... 代码实现 } // ... 其他代码 } 接着看 AbpAuditActionFilter() 方法内部的实现,进入这个过滤器的时候,通过 ShouldSaveAudit() 方法来判断是否要写审计日志。 之后呢与 DTO 自动验证的过滤器一样,通过 AbpCrossCuttingConcerns.Applying() 方法为当前的对象增加了一个标识,用来告诉拦截器说我已经处理过了,你就不要再重复处理了。 再往下就是创建审计信息,执行具体接口方法,并且如果产生了异常的话,也会存放到审计信息当中。 最后接口无论是否执行成功,还是说出现了异常信息,都会将其性能计数信息同审计信息一起,通过 IAuditingHelper 存储起来。 public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // 判断是否写日志 if (!ShouldSaveAudit(context)) { await next(); return; } // 为当前类型打上标识 using (AbpCrossCuttingConcerns.Applying(context.Controller, AbpCrossCuttingConcerns.Auditing)) { // 构造审计信息(AuditInfo) var auditInfo = _auditingHelper.CreateAuditInfo( context.ActionDescriptor.AsControllerActionDescriptor().ControllerTypeInfo.AsType(), context.ActionDescriptor.AsControllerActionDescriptor().MethodInfo, context.ActionArguments ); // 开始性能计数 var stopwatch = Stopwatch.StartNew(); try { // 尝试调用接口方法 var result = await next(); // 产生异常之后,将其异常信息存放在审计信息之中 if (result.Exception != null && !result.ExceptionHandled) { auditInfo.Exception = result.Exception; } } catch (Exception ex) { // 产生异常之后,将其异常信息存放在审计信息之中 auditInfo.Exception = ex; throw; } finally { // 停止计数,并且存储审计信息 stopwatch.Stop(); auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds); await _auditingHelper.SaveAsync(auditInfo); } } } 2.2 拦截器代码分析 拦截器处理时的总体思路与过滤器类似,其核心都是通过 IAuditingHelper 来创建审计信息和持久化审计信息的。只不过呢由于拦截器不仅仅是处理 MVC 接口,也会处理内部的一些类型的方法,所以针对同步方法与异步方法的处理肯定会复杂一点。 拦截器呢,我们关心一下他的核心方法 Intercept() 就行了。 public void Intercept(IInvocation invocation) { // 判断过滤器是否已经处理了过了 if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Auditing)) { invocation.Proceed(); return; } // 通过 IAuditingHelper 来判断当前方法是否需要记录审计日志信息 if (!_auditingHelper.ShouldSaveAudit(invocation.MethodInvocationTarget)) { invocation.Proceed(); return; } // 构造审计信息 var auditInfo = _auditingHelper.CreateAuditInfo(invocation.TargetType, invocation.MethodInvocationTarget, invocation.Arguments); // 判断方法的类型,同步方法与异步方法的处理逻辑不一样 if (invocation.Method.IsAsync()) { PerformAsyncAuditing(invocation, auditInfo); } else { PerformSyncAuditing(invocation, auditInfo); } } // 同步方法的处理逻辑与 MVC 过滤器逻辑相似 private void PerformSyncAuditing(IInvocation invocation, AuditInfo auditInfo) { var stopwatch = Stopwatch.StartNew(); try { invocation.Proceed(); } catch (Exception ex) { auditInfo.Exception = ex; throw; } finally { stopwatch.Stop(); auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds); _auditingHelper.Save(auditInfo); } } // 异步方法处理 private void PerformAsyncAuditing(IInvocation invocation, AuditInfo auditInfo) { var stopwatch = Stopwatch.StartNew(); invocation.Proceed(); if (invocation.Method.ReturnType == typeof(Task)) { invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithFinally( (Task) invocation.ReturnValue, exception => SaveAuditInfo(auditInfo, stopwatch, exception) ); } else //Task { invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithFinallyAndGetResult( invocation.Method.ReturnType.GenericTypeArguments[0], invocation.ReturnValue, exception => SaveAuditInfo(auditInfo, stopwatch, exception) ); } } private void SaveAuditInfo(AuditInfo auditInfo, Stopwatch stopwatch, Exception exception) { stopwatch.Stop(); auditInfo.Exception = exception; auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds); _auditingHelper.Save(auditInfo); } 这里异步方法的处理在很早之前的工作单元拦截器就有过讲述,这里就不再重复说明了。 2.3 核心的 IAuditingHelper 从代码上我们就可以看到,不论是拦截器还是过滤器都是最终都是通过 IAuditingHelper 对象来储存审计日志的。Abp 依旧为我们实现了一个默认的 AuditingHelper ,实现了其接口的所有方法。我们先查看一下这个接口的定义: public interface IAuditingHelper { // 判断当前方法是否需要存储审计日志信息 bool ShouldSaveAudit(MethodInfo methodInfo, bool defaultValue = false); // 根据参数集合创建一个审计信息,一般用于拦截器 AuditInfo CreateAuditInfo(Type type, MethodInfo method, object[] arguments); // 根据一个参数字典类来创建一个审计信息,一般用于 MVC 过滤器 AuditInfo CreateAuditInfo(Type type, MethodInfo method, IDictionary arguments); // 同步保存审计信息 void Save(AuditInfo auditInfo); // 异步保存审计信息 Task SaveAsync(AuditInfo auditInfo); } 我们来到其默认实现 AuditingHelper 类型,先看一下其内部注入了哪些接口。 public class AuditingHelper : IAuditingHelper, ITransientDependency { // 日志记录器,用于记录日志 public ILogger Logger { get; set; } // 用于获取当前登录用户的信息 public IAbpSession AbpSession { get; set; } // 用于持久话审计日志信息 public IAuditingStore AuditingStore { get; set; } // 主要作用是填充审计信息的客户端调用信息 private readonly IAuditInfoProvider _auditInfoProvider; // 审计日志组件的配置相关 private readonly IAuditingConfiguration _configuration; // 在调用 AuditingStore 进行持久化的时候使用,创建一个工作单元 private readonly IUnitOfWorkManager _unitOfWorkManager; // 用于序列化参数信息为 JSON 字符串 private readonly IAuditSerializer _auditSerializer; public AuditingHelper( IAuditInfoProvider auditInfoProvider, IAuditingConfiguration configuration, IUnitOfWorkManager unitOfWorkManager, IAuditSerializer auditSerializer) { _auditInfoProvider = auditInfoProvider; _configuration = configuration; _unitOfWorkManager = unitOfWorkManager; _auditSerializer = auditSerializer; AbpSession = NullAbpSession.Instance; Logger = NullLogger.Instance; AuditingStore = SimpleLogAuditingStore.Instance; } // ... 其他实现的接口 } 2.3.1 判断是否创建审计信息 首先分析一下其内部的 ShouldSaveAudit() 方法,整个方法的核心作用就是根据传入的方法类型来判定是否为其创建审计信息。 其实在这一串 if 当中,你可以发现有一句代码对方法是否标注了 DisableAuditingAttribute 特性进行了判断,如果标注了该特性,则不为该方法创建审计信息。所以我们就可以通过该特性来控制自己应用服务类,控制里面的的接口是否要创建审计信息。同理,我们也可以通过显式标注 AuditedAttribute 特性来让拦截器为这个方法创建审计信息。 public bool ShouldSaveAudit(MethodInfo methodInfo, bool defaultValue = false) { if (!_configuration.IsEnabled) { return false; } if (!_configuration.IsEnabledForAnonymousUsers && (AbpSession?.UserId == null)) { return false; } if (methodInfo == null) { return false; } if (!methodInfo.IsPublic) { return false; } if (methodInfo.IsDefined(typeof(AuditedAttribute), true)) { return true; } if (methodInfo.IsDefined(typeof(DisableAuditingAttribute), true)) { return false; } var classType = methodInfo.DeclaringType; if (classType != null) { if (classType.GetTypeInfo().IsDefined(typeof(AuditedAttribute), true)) { return true; } if (classType.GetType
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信