使用ABP框架踩过的坑系列3
从架构角度来讲,ApplicationService究竟应该如何定位,一种说法是直接对应用例UseCase, 也就是直接对应UI, 这个UI是广义的,不仅仅是浏览器的页面,也包括API调用。还是从我曾经踩过的一个坑说起吧:
public class ProductImportService : AdvancedAsyncCrudAppService<Product, ProductDto, PagedResultRequestDto> , IProductImportService { ......public ProductImportService(...... ) : base(productRepository) { ...... } //[MyIgnoreApiAttribute] //[DisableValidation] //[DisableAuditing] private void SaveProductAndTestSizeHead( RawData rawData ) { ...... } //[DisableValidation] //[DisableAuditing] private void SaveStandardSizeValue( RawData rawData) { ...... } //[DisableValidation] //[DisableAuditing] private void SaveTestSizeInfo( RawData rawData) { ...... } private void SaveTestSizeValue( RawData rawData) { List<TestSizeValue> newTestSizeValues = rawData.TestSizeValues; var firstEntity = newTestSizeValues.FirstOrDefault(); if (firstEntity == null) { return; } var dbExistEntities = _testSizeValueValueRepository.GetAllIncluding( os => os.TestSizeInfo , os => os.TestSizeInfo.TestSizeHead , os => os.StandardSizeValue ) .Where(os => os.TestSizeInfo.TestSizeHead.Id == rawData.TestSizeHead.Id && os.IsAim == firstEntity.IsAim ).ToList(); FilterValues(newTestSizeValues, dbExistEntities, out List<TestSizeValue> newEnities, out List<TestSizeValue> updateEnities); foreach( var entity in newEnities) { entity.TestSizeInfo = rawData.TestSizeInfos.FirstOrDefault(tsi => tsi.IsSame(entity.TestSizeInfo)); entity.StandardSizeValue = rawData.StandardSizeValues.FirstOrDefault(ssv => ssv.IsSame(entity.StandardSizeValue)); } _testSizeValueValueRepository.BulkInsert(newEnities); // 批量插入新的 foreach( var updateEntity in updateEnities) { _testSizeValueValueRepository.Update(updateEntity); // 修改已存在的 } rawData.TestSizeValues = newTestSizeValues; } private void FilterValues(List<TestSizeValue> testSizeValues, List<TestSizeValue> dbExistEntities, out List<TestSizeValue> newEnities, out List<TestSizeValue> updateEnities) { ...... } public RawData Save(RawData rawData) { SaveProductAndTestSizeHead( rawData); SaveStandardSizeValue( rawData); SaveTestSizeInfo( rawData); SaveTestSizeValue( rawData); return rawData; }
这是一个从Excel文件中导入数据的场景,每个文件的数据是个矩阵,有50多列,有30多行,数据有50x30=1500个左右,导入场景性能是个关键因素,因为它决定了单位时间内能处理多少个Excel文件,调试时发现每个文件的处理时间是90秒左右,首先想到的方案是改用批量插入,改善到10秒左右,再也没法改善了。于是在各个地方加了时间计算,终于发现问题出在哪里了,其实瓶颈并不在数据库操作,而是在方法执行前,也就是ABP拦截器里消耗的时间,这个拦截器就是Audit Logging : User, browser, IP address, calling service, method, parameters, calling time, execution duration and some other informations are automatically saved for each request based on conventions and configurations. 审计全记录,最耗时的是记录 parameters,每次记录都要序列化(用的是Json),如果是大数据库的化,这块是非常非常耗时的!后来仔细研究ABP源码,其实很简单
public sealed class AbpKernelModule : AbpModule { public override void PreInitialize() { IocManager.AddConventionalRegistrar(new BasicConventionalRegistrar()); IocManager.Register<IScopedIocResolver, ScopedIocResolver>(DependencyLifeStyle.Transient); ValidationInterceptorRegistrar.Initialize(IocManager); AuditingInterceptorRegistrar.Initialize(IocManager); UnitOfWorkRegistrar.Initialize(IocManager); AuthorizationInterceptorRegistrar.Initialize(IocManager); Configuration.Auditing.Selectors.Add( new NamedTypeSelector( "Abp.ApplicationServices", type => typeof(IApplicationService).IsAssignableFrom(type) ) ); ...... } ...... }
ABP是通过拦截器的方式,注入了代码(功能),ValidationInterceptor 验证拦截器、AuditingInterceptor 审计拦截器、AuthorizationInterceptor 认证拦截器,AuditingInterceptor 审计拦截器会拦截所有ApplicationServices
Configuration.Auditing.Selectors.Add( new NamedTypeSelector( "Abp.ApplicationServices", type => typeof(IApplicationService).IsAssignableFrom(type) ) );
AuditingInterceptor 审计拦截器,有与其配套的Attribute,来实现申明式Enable/Disable
namespace Abp.Auditing { internal class AuditingInterceptor : IInterceptor { ......public void Intercept(IInvocation invocation) { if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Auditing)) { invocation.Proceed(); return; } if (!_auditingHelper.ShouldSaveAudit(invocation.MethodInvocationTarget)) { invocation.Proceed(); return; } var auditInfo = _auditingHelper.CreateAuditInfo(invocation.MethodInvocationTarget, invocation.Arguments); if (AsyncHelper.IsAsyncMethod(invocation.Method)) { PerformAsyncAuditing(invocation, auditInfo); } else { PerformSyncAuditing(invocation, auditInfo); } } ...... } }
public class AuditingHelper : IAuditingHelper, ITransientDependency { ......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.IsDefined(typeof(AuditedAttribute), true)) { return true; } if (classType.IsDefined(typeof(DisableAuditingAttribute), true)) { return false; } if (_configuration.Selectors.Any(selector => selector.Predicate(classType))) { return true; } } return defaultValue; }
所以我的,第一个解决方案是:
[DisableValidation] [DisableAuditing]
但经过仔细分析,其实在导入这个场景中,Save保存数据到DB, 其实不是UseCase用例,而Import才是UseCase, Save只是Import的一个步骤; Import 的第一步是Parse解析Excel文件,第二步才是Save; 因此Save不应该作为ApplicationService(比较重的服务,ABP会自动注入很多关切),上策应该把Save作为DomainServie(轻量级服务,ABP不会自动注入很多东西),以下是Excel导入Save的最终解决方案:
public class ProductImportService :DomainService , IProductImportService { ...... public ProductImportService(...... ) : base(productRepository) { ...... } //[MyIgnoreApiAttribute] //[DisableValidation] //[DisableAuditing] private void SaveProductAndTestSizeHead( RawData rawData ) { ...... } //[DisableValidation] //[DisableAuditing] private void SaveStandardSizeValue( RawData rawData) { ...... } //[DisableValidation] //[DisableAuditing] private void SaveTestSizeInfo( RawData rawData) { ...... } private void SaveTestSizeValue( RawData rawData) { ...... } private void FilterValues(List<TestSizeValue> testSizeValues, List<TestSizeValue> dbExistEntities, out List<TestSizeValue> newEnities, out List<TestSizeValue> updateEnities) { ...... } public RawData Save(RawData rawData) { SaveProductAndTestSizeHead( rawData); SaveStandardSizeValue( rawData); SaveTestSizeInfo( rawData); SaveTestSizeValue( rawData); return rawData; }
总结,ApplicationService 很强大,但也要合适的使用,分清ApplicationService和DomainServie的适合场景,也许是ABP或DDD的一个重要的架构选择!