#一、简要说明
ABP vNext 当中的审计模块早在 **依赖注入与拦截器**一文中有所提及,但没有详细的对其进行分析。
审计模块是 ABP vNext 框架的一个基本组件,它能够提供一些实用日志记录。不过这里的日志不是说系统日志,而是说接口每次调用之后的执行情况(执行时间、传入参数、异常信息、请求 IP)。
除了常规的日志功能以外,关于 实体 和 聚合 的审计字段接口也是存放在审计模块当中的。(创建人、创建时间、修改人、修改时间、删除人、删除时间)
#二、源码分析
#2.1. 审计日志拦截器
#2.1.1 审计日志拦截器的注册
Volo.Abp.Auditing 的模块定义十分简单,主要是提供了 审计日志拦截器 的注册功能。下面代码即在组件注册的时候,会调用 AuditingInterceptorRegistrar.RegisterIfNeeded 方法来判定是否为实现类型(ImplementationType) 注入审计日志拦截器。
public class AbpAuditingModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistred(AuditingInterceptorRegistrar.RegisterIfNeeded);
}
}跳转到具体的实现,可以看到内部会结合三种类型进行判断。分别是 AuditedAttribute 、IAuditingEnabled、DisableAuditingAttribute 。
前两个作用是,只要类型标注了 AuditedAttribute 特性,或者是实现了 IAuditingEnable 接口,都会为该类型注入审计日志拦截器。
而 DisableAuditingAttribute 类型则相反,只要类型上标注了该特性,就不会启用审计日志拦截器。某些接口需要 提升性能 的话,可以尝试使用该特性禁用掉审计日志功能。
public static class AuditingInterceptorRegistrar
{
public static void RegisterIfNeeded(IOnServiceRegistredContext context)
{
// 满足条件时,将会为该类型注入审计日志拦截器。
if (ShouldIntercept(context.ImplementationType))
{
context.Interceptors.TryAdd<AuditingInterceptor>();
}
}
private static bool ShouldIntercept(Type type)
{
// 首先判断类型上面是否使用了辅助类型。
if (ShouldAuditTypeByDefault(type))
{
return true;
}
// 如果任意方法上面标注了 AuditedAttribute 特性,则仍然为该类型注入拦截器。
if (type.GetMethods().Any(m => m.IsDefined(typeof(AuditedAttribute), true)))
{
return true;
}
return false;
}
//TODO: Move to a better place
public static bool ShouldAuditTypeByDefault(Type type)
{
// 下面就是根据三种辅助类型进行判断,是否为当前 type 注入审计日志拦截器。
if (type.IsDefined(typeof(AuditedAttribute), true))
{
return true;
}
if (type.IsDefined(typeof(DisableAuditingAttribute), true))
{
return false;
}
if (typeof(IAuditingEnabled).IsAssignableFrom(type))
{
return true;
}
return false;
}
}#2.1.2 审计日志拦截器的实现
审计日志拦截器的内部实现,主要使用了三个类型进行协同工作。它们分别是负责管理审计日志信息的 IAuditingManager,负责创建审计日志信息的 IAuditingHelper,还有统计接口执行时常的 Stopwatch 。
整个审计日志拦截器的大体流程如下:
- 首先是判定 MVC 审计日志过滤器是否进行处理。
- 再次根据特性,和类型进行二次验证是否应该创建审计日志信息。
- 根据调用信息,创建
AuditLogInfo和AuditLogActionInfo审计日志信息。 - 调用
StopWatch的计时方法,如果出现了异常则将异常信息添加到刚才构建的AuditLogInfo对象中。 - 无论是否出现异常,都会进入
finally语句块,这个时候会调用StopWatch实例的停止方法,并统计完成执行时间。
public override async Task InterceptAsync(IAbpMethodInvocation invocation)
{
if (!ShouldIntercept(invocation, out var auditLog, out var auditLogAction))
{
await invocation.ProceedAsync();
return;
}
// 开始进行计时操作。
var stopwatch = Stopwatch.StartNew();
try
{
await invocation.ProceedAsync();
}
catch (Exception ex)
{
// 如果出现了异常,一样的将异常信息添加到审计日志结果中。
auditLog.Exceptions.Add(ex);
throw;
}
finally
{
// 统计完成,并将信息加入到审计日志结果中。
stopwatch.Stop();
auditLogAction.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds);
auditLog.Actions.Add(auditLogAction);
}
}可以看到,只有当 ShouldIntercept() 方法返回 true 的时候,下面的统计等操作才会被执行。
protected virtual bool ShouldIntercept(
IAbpMethodInvocation invocation,
out AuditLogInfo auditLog,
out AuditLogActionInfo auditLogAction)
{
auditLog = null;
auditLogAction = null;
if (AbpCrossCuttingConcerns.IsApplied(invocation.TargetObject, AbpCrossCuttingConcerns.Auditing))
{
return false;
}
// 如果没有获取到 Scop,则返回 false。
var auditLogScope = _auditingManager.Current;
if (auditLogScope == null)
{
return false;
}
// 进行二次判断是否需要存储审计日志。
if (!_auditingHelper.ShouldSaveAudit(invocation.Method))
{
return false;
}
// 构建审计日志信息。
auditLog = auditLogScope.Log;
auditLogAction = _auditingHelper.CreateAuditLogAction(
auditLog,
invocation.TargetObject.GetType(),
invocation.Method,
invocation.Arguments
);
return true;
}#2.2 审计日志的持久化
大体流程和我们上面说的一样,不过好像缺少了重要的一步,那就是 持久化操作。你可以在 Volo.Abp.Auditing 模块发现有 IAuditingStore 接口的定义,但是它的 SaveAsync() 方法却没有在拦截器内部被调用。同样在 MVC 的审计日志过滤器实现,你也会发现没有调用持久化方法。
那么我们的审计日志是在什么时候被持久化的呢?找到 SaveAsync() 被调用的地方,发现 ABP vNext 实现了一个审计日志的 ASP.NET Core 中间件。

在这个中间件内部的实现比较简单,首先通过一个判定方法,决定是否为本次请求执行 IAuditingManager.BeginScope() 方法。如果判定通过,则执行,否则不仅行任何操作。
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (!ShouldWriteAuditLog(context))
{
await next(context);
return;
}
using (var scope = _auditingManager.BeginScope())
{
try
{
await next(context);
}
finally
{
await scope.SaveAsync();
}
}
}可以看到,在这里 ABP vNext 使用 IAuditingManager 构建,调用其 BeginScope() 构建了一个 IAuditLogSaveHandle 对象,并使用其提供的 SaveAsync() 方法进行持久化操作。
#2.2.1 嵌套的持久化操作
在构造出来的 IAuditLogSaveHandle 对象里面,还是使用的 IAuditingManager 的默认实现 AuditingManager 所提供的 SaveAsync() 方法进行持久化。
阅读源码之后,发现了下面两个问题:
IAuditingManager没有将持久化方法公开 出来,而是作为一个protected级别的方法。- 为什么还要借助
IAuditLogSaveHandle间接地调用 管理器的持久化方法。
这就要从中间件的代码说起了,可以看到它是构造出了一个可以被释放的 IAuditLogSaveHandle 对象。ABP vNext 这样做的目的,就是可以嵌套多个 Scope,即 只在某个范围内 才将审计日志记录下来。这种特性类似于 工作单元 的用法,其底层实现是 之前文章 讲过的 IAmbientScopeProvider 对象。
例如在某个应用服务内部,我可以这样写代码:
using (var scope = _auditingManager.BeginScope())
{
await myAuditedObject1.DoItAsync(new InputObject { Value1 = "我是内部嵌套测试方法1。", Value2 = 5000 });
using (var scope2 = _auditingManager.BeginScope())
{
await myAuditedObject1.DoItAsync(new InputObject {Value1 = "我是内部嵌套测试方法2。", Value2 = 10000});
await scope2.SaveAsync();
}
await scope.SaveAsync();
}想一下之前的代码,在拦截器内部,我们是通过 IAuditingManager.Current 拿到当前可用的 IAuditLogScope ,而这个 Scope 就是在调用 IAuditingManager.BeginScope() 之后生成的。
#2.2.3 最终的持久化代码
通过上述的流程,我们得知最后的审计日志信息会通过 IAuditingStore 进行持久化。ABP vNext 为我们提供了一个默认的 SimpleLogAuditingStore 实现,其内部就是调用 ILogger 将信息输出。如果需要将审计日志持久化到数据库,你可以实现 IAUditingStore 接口,覆盖原有实现 ,或者使用 ABP vNext 提供的 Volo.Abp.AuditLogging 模块。
#2.3 审计日志的序列化
审计日志的序列化处理是在 IAuditingHelper 的默认实现内部被使用,可以看到构建审计日志的方法内部,通过自定义的序列化器来将 Action 的参数进行序列化处理,方便存储。
public virtual AuditLogActionInfo CreateAuditLogAction(
AuditLogInfo auditLog,
Type type,
MethodInfo method,
IDictionary<string, object> arguments)
{
var actionInfo = new AuditLogActionInfo
{
ServiceName = type != null
? type.FullName
: "",
MethodName = method.Name,
// 序列化参数信息。
Parameters = SerializeConvertArguments(arguments),
ExecutionTime = Clock.Now
};
//TODO Execute contributors
return actionInfo;
}
protected virtual string SerializeConvertArguments(IDictionary<string, object> arguments)
{
try
{
if (arguments.IsNullOrEmpty())
{
return "{}";
}
var dictionary = new Dictionary<string, object>();
foreach (var argument in arguments)
{
// 忽略的代码,主要作用是构建参数字典。
}
// 调用序列化器,序列化 Action 的调用参数。
return AuditSerializer.Serialize(dictionary);
}
catch (Exception ex)
{
Logger.LogException(ex, LogLevel.Warning);
return "{}";
}
}下面就是具体序列化器的代码:
public class JsonNetAuditSerializer : IAuditSerializer, ITransientDependency
{
protected AbpAuditingOptions Options;
public JsonNetAuditSerializer(IOptions<AbpAuditingOptions> options)
{
Options = options.Value;
}
public string Serialize(object obj)
{
// 使用 JSON.NET 进行序列化操作。
return JsonConvert.SerializeObject(obj, GetSharedJsonSerializerSettings());
}
// ... 省略的代码。
}#2.4 审计日志的配置参数
针对审计日志相关的配置参数的定义,都存放在 AbpAuditingOptions 当中。下面我会针对各个参数的用途,对其进行详细的说明。
public class AbpAuditingOptions
{
//TODO: Consider to add an option to disable auditing for application service methods?
// 该参数目前版本暂未使用,为保留参数。
public bool HideErrors { get; set; }
// 是否启用审计日志功能,默认值为 true。
public bool IsEnabled { get; set; }
// 审计日志的应用程序名称,默认值为 null,主要在构建 AuditingInfo 被使用。
public string ApplicationName { get; set; }
// 是否为匿名请求记录审计日志默认值 true。
public bool IsEnabledForAnonymousUsers { get; set; }
// 审计日志功能的协作者集合,默认添加了 AspNetCoreAuditLogContributor 实现。
public List<AuditLogContributor> Contributors { get; }
// 默认的忽略类型,主要在序列化时使用。
public List<Type> IgnoredTypes { get; }
// 实体类型选择器。
public IEntityHistorySelectorList EntityHistorySelectors { get; }
//TODO: Move this to asp.net core layer or convert it to a more dynamic strategy?
// 是否为 Get 请求记录审计日志,默认值 false。
public bool IsEnabledForGetRequests { get; set; }
public AbpAuditingOptions()
{
IsEnabled = true;
IsEnabledForAnonymousUsers = true;
HideErrors = true;
Contributors = new List<AuditLogContributor>();
IgnoredTypes = new List<Type>
{
typeof(Stream),
typeof(Expression)
};
EntityHistorySelectors = new EntityHistorySelectorList();
}
}#2.4 实体相关的审计信息
在文章开始就谈到,除了对 HTTP 请求有审计日志记录以外,ABP vNext 还提供了实体审计信息的记录功能。所谓的实体的审计信息,指的就是实体继承了 ABP vNext 提供的接口之后,ABP vNext 会自动维护实现的接口字段,不需要开发人员自己再进行处理。
这些接口包括创建实体操作的相关信息 IHasCreationTime、IMayHaveCreator、ICreationAuditedObject 以及删除实体时,需要记录的相关信息接口 IHasDeletionTime、IDeletionAuditedObject 等。除了审计日志模块定义的类型以外,在 Volo.Abp.Ddd.Domain 模块的 Auditing 里面也有很多审计实体的默认实现。
我在这里就不再一一列举,下面仅快速讲解一下 ABP vNext 是如何通过这些接口,实现对审计字段的自动维护的。
在审计日志模块的内部,我们看到一个接口名字叫做 IAuditPropertySetter,它提供了三个方法,分别是:
public interface IAuditPropertySetter
{
void SetCreationProperties(object targetObject);
void SetModificationProperties(object targetObject);
void SetDeletionProperties(object targetObject);
}所以,这几个方法就是用于设置创建信息、修改信息、删除信息的。现在跳转到默认实现 AuditPropertySetter,随便找一个 SetCreationTime() 方法。该方法内部首先是判断传入的 object 是否实现了 IHasCreationTime 接口,如果实现了对其进行强制类型转换,然后赋值即可。
private void SetCreationTime(object targetObject)
{
if (!(targetObject is IHasCreationTime objectWithCreationTime))
{
return;
}
if (objectWithCreationTime.CreationTime == default)
{
objectWithCreationTime.CreationTime = Clock.Now;
}
}其他几个 Set 方法大同小异,那我们看一下有哪些地方使用到了上述三个方法。

可以看到使用者就包含有 EF Core 模块和 MongoDB 模块,这里我以 EF Core 模块为例,猜测应该是传入了实体对象过来。

果不其然…查看这个方法的调用链,发现是 DbContext 每次进行 SaveChanges/SaveChangesAsync 的时候,就会对实体进行审计字段自动赋值操作。

#三、总结
审计日志是 ABP vNext 为我们提供的一个可选组件,当开启审计日志功能后,我们可以根据审计日志信息快速定位问题。但审计日志的开启,也会较大的影响性能,因为每次请求都会创建审计日志信息,之后再进行持久化。因此在使用审计日志功能时,可以结合 DisableAuditingAttribute 特性和 IAuditingManager.BeginScope(),按需开启审计日志功能。