[Abp 源碼分析]十4、DTO 自動驗證

0.簡介

在平時開發 API 接口的時候須要對前端傳入的參數進行校驗以後才能進入業務邏輯進行處理,不然一旦前端傳入一些非法/無效數據到 API 當中,輕則致使程序報錯,重則致使整個業務流程出現問題。html

用過傳統 ASP.NET MVC 數據註解的同窗應該知道,咱們能夠經過在 Model 上面指定各類數據特性,而後在前端調用 API 的時候就會根據這些註解來校驗 Model 內部的字段是否合法。前端

1.啓動流程

Abp 針對於數據校驗分爲兩個地方進行,第一個是 MVC 的過濾器,也是咱們最常使用的。第二個則是藉助於 Castle 的攔截器實現的 DTO 數據校驗功能,前者只能用於控制器方法,然後者則支持普通方法。mvc

1.1 過濾器注入

在注入 Abp 的時候,經過 AddAbp() 方法內部的 ConfigureAspNetCore() 配置了諸多過濾器。app

private static void ConfigureAspNetCore(IServiceCollection services, IIocResolver iocResolver)
{
    // ... 其餘代碼
    
    //Configure MVC
    services.Configure<MvcOptions>(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(AbpValidationActionFilter));
        
        // ... 其餘過濾器注入
    }
    
    // ... 其餘代碼
}

1.2 攔截器注入

Abp 針對於驗證攔截器的註冊始於 AbpBootstrapper 類,該基類在以前曾經屢次出現過,也就是在用戶調用 IServiceCollection.AddAbp<TStartupModule>() 方法的時候會初始化該類的一個實例對象。在該類的構造函數當中,會調用一個 AddInterceptorRegistrars() 方法用於添加各類攔截器的註冊類實例。代碼以下:async

public class AbpBootstrapper : IDisposable
{
    private AbpBootstrapper([NotNull] Type startupModule, [CanBeNull] Action<AbpBootstrapperOptions> optionsAction = null)
    {
        // ... 其餘代碼

        if (!options.DisableAllInterceptors)
        {
            AddInterceptorRegistrars();
        }
    }

    // ... 其餘代碼

    // 添加各類攔截器
    private void AddInterceptorRegistrars()
    {
        ValidationInterceptorRegistrar.Initialize(IocManager);
        AuditingInterceptorRegistrar.Initialize(IocManager);
        EntityHistoryInterceptorRegistrar.Initialize(IocManager);
        UnitOfWorkRegistrar.Initialize(IocManager);
        AuthorizationInterceptorRegistrar.Initialize(IocManager);
    }

    // ... 其餘代碼\
}

來到 ValidationInterceptorRegistrar 類型定義當中能夠看到,其內部就是經過 Castle 的 IocContainer 來針對每次注入的應用服務應用上參數驗證攔截器。ide

internal static class ValidationInterceptorRegistrar
{
    public static void Initialize(IIocManager iocManager)
    {
        iocManager.IocContainer.Kernel.ComponentRegistered += Kernel_ComponentRegistered;
    }

    private static void Kernel_ComponentRegistered(string key, IHandler handler)
    {
        // 判斷是否實現了 IApplicationService 接口,若是實現了,則爲該對象添加攔截器
        if (typeof(IApplicationService).GetTypeInfo().IsAssignableFrom(handler.ComponentModel.Implementation))
        {
            handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(ValidationInterceptor)));
        }
    }
}

2.代碼分析

從 Abp 庫代碼當中咱們能夠知道其攔截器與過濾器是在什麼時候被注入的,下面咱們就來具體分析一下他們的處理邏輯。函數

2.1 過濾器代碼分析

Abp 在框架初始化的時候就將 AbpValidationActionFilter 添加到 MVC 的配置當中,其自定義實現的攔截器實現了 IAsyncActionFilter 接口,也就是說當每次接口被調用的時候都會進入該攔截器的內部。工具

public class AbpValidationActionFilter : IAsyncActionFilter, ITransientDependency
{
    // Ioc 解析器,用於解析各類注入的組件
    private readonly IIocResolver _iocResolver;
    // Abp 針對與 ASP.NET Core 的配置項,主要做用是判斷用戶是否須要檢測控制器方法
    private readonly IAbpAspNetCoreConfiguration _configuration;

    public AbpValidationActionFilter(IIocResolver iocResolver, IAbpAspNetCoreConfiguration configuration)
    {
        _iocResolver = iocResolver;
        _configuration = configuration;
    }

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // ... 處理邏輯
    }
}

在內部首先是結合配置項判斷用戶是否禁用了 MVC Controller 的參數驗證功能,禁用了則不進行任何操做。this

public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
    // 判斷是否禁用了控制器檢測
    if (!_configuration.IsValidationEnabledForControllers || !context.ActionDescriptor.IsControllerAction())
    {
        await next();
        return;
    }

    // 針對應用服務增長一個驗證完成標識
    using (AbpCrossCuttingConcerns.Applying(context.Controller, AbpCrossCuttingConcerns.Validation))
    {
        // 解析出方法驗證器,傳入請求上下文,而且調用這些驗證器具體的驗證方法
        using (var validator = _iocResolver.ResolveAsDisposable<MvcActionInvocationValidator>())
        {
            validator.Object.Initialize(context);
            validator.Object.Validate();
        }

        await next();
    }
}

其實咱們這裏看到有一個 AbpCrossCuttingConcerns.Applying() 方法,那麼該方法的做用是什麼呢?

在這裏我先大致講述一下該方法的做用,該方法主要是嚮應用服務對象 (也就是繼承了 ApplicationService 類的對象) 內部的 AppliedCrossCuttingConcerns 屬性增長一個常量值,在這裏也就是 AbpCrossCuttingConcerns.Validation 的值,也就是一個字符串。

那麼其做用是什麼呢,就是防止重複驗證。從啓動流程一節咱們就已經知道 Abp 框架在啓動的時候除了注入過濾器以外,還會注入攔截器進行接口參數驗證,當過濾器驗證過以後,其實不必再使用攔截器進行二次驗證。

因此在攔截器的 Intercept() 方法內部會有這樣一句代碼:

public void Intercept(IInvocation invocation)
{
    // 判斷是否擁有處理過的標識
    if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Validation))
    {
        invocation.Proceed();
        return;
    }

    // ... 其餘代碼
}

解釋完 AbpCrossCuttingConcerns.Applying() 以後,咱們繼續往下看代碼。

// 解析出方法驗證器,傳入請求上下文,而且調用這些驗證器具體的驗證方法
using (var validator = _iocResolver.ResolveAsDisposable<MvcActionInvocationValidator>())
{
    validator.Object.Initialize(context);
    validator.Object.Validate();
}

await next();

這裏就比較簡單了,過濾器經過 IocResolver 解析出來了一個 MvcActionInvocationValidator 對象,使用該對象來校驗具體的參數內容。

2.2 攔截器代碼分析

看完過濾器代碼以後,其實攔截器代碼更加簡單。總體邏輯上面與過濾器差很少,只不過針對於攔截器,它是經過一個 MethodInvocationValidator 對象來校驗傳入的參數內容。

public class ValidationInterceptor : IInterceptor
{
    // Ioc 解析器,用於解析各類注入的組件
    private readonly IIocResolver _iocResolver;

    public ValidationInterceptor(IIocResolver iocResolver)
    {
        _iocResolver = iocResolver;
    }

    public void Intercept(IInvocation invocation)
    {
        // 判斷過濾器是否已經處理過
        if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Validation))
        {
            // 處理過則直接進入具體方法內部,執行業務邏輯
            invocation.Proceed();
            return;
        }

        // 解析出方法驗證器,傳入請求上下文,而且調用這些驗證器具體的驗證方法
        using (var validator = _iocResolver.ResolveAsDisposable<MethodInvocationValidator>())
        {
            validator.Object.Initialize(invocation.MethodInvocationTarget, invocation.Arguments);
            validator.Object.Validate();
        }

        invocation.Proceed();
    }
}

能夠看到兩個過濾器與攔截器業務邏輯類似,但都是經過驗證器來進行處理的,那麼驗證器又是個什麼鬼東西呢?

2.3 參數驗證器

驗證器便是用來具體執行驗證邏輯的工具,從上述代碼裏面咱們能夠看到過濾器和攔截器都是經過解析出 MethodInvocationValidator/MvcActionInvocationValidator 以後調用其驗證方法進行驗證的。

首先咱們來看一下 MVC 的驗證器是如何進行處理的,看方法類型的定義,能夠看到其繼承了一個基類,叫 ActionInvocationValidatorBase,而這個基類呢,又繼承自 MethodInvocationValidator

public class MvcActionInvocationValidator : ActionInvocationValidatorBase
{
    // ... 其餘代碼
}
public abstract class ActionInvocationValidatorBase : MethodInvocationValidator
{
    // ... 其餘代碼
}

因此咱們分析代碼的順序調整一下,先看一下 MethodInvocationValidator 的內部是如何作處理的吧,這個類型內部仍是比較簡單的,可能除了有一個遞歸有點繞以外。

其主要功能就是拿着傳遞進來的參數值,經過在 Abp 框架啓動的時候注入的具體驗證器(用戶自定義驗證器)來遞歸校驗每一個參數的值。

/// <summary>
/// 本類用於須要參數驗證的方法.
/// </summary>
public class MethodInvocationValidator : ITransientDependency
{
    // 最大迭代驗證次數
    private const int MaxRecursiveParameterValidationDepth = 8;

    // 待驗證的方法信息
    protected MethodInfo Method { get; private set; }
    // 傳入的參數值
    protected object[] ParameterValues { get; private set; }
    // 方法參數信息
    protected ParameterInfo[] Parameters { get; private set; }
    protected List<ValidationResult> ValidationErrors { get; }
    protected List<IShouldNormalize> ObjectsToBeNormalized { get; }

    private readonly IValidationConfiguration _configuration;
    private readonly IIocResolver _iocResolver;

    public MethodInvocationValidator(IValidationConfiguration configuration, IIocResolver iocResolver)
    {
        _configuration = configuration;
        _iocResolver = iocResolver;

        ValidationErrors = new List<ValidationResult>();
        ObjectsToBeNormalized = new List<IShouldNormalize>();
    }

    // 初始化攔截器參數
    public virtual void Initialize(MethodInfo method, object[] parameterValues)
    {
        Check.NotNull(method, nameof(method));
        Check.NotNull(parameterValues, nameof(parameterValues));

        Method = method;
        ParameterValues = parameterValues;
        Parameters = method.GetParameters();
    }
    
    // 開始驗證參數的有效性
    public void Validate()
    {
        // 檢測是否初始化,沒有初始化則拋出系統級異常
        CheckInitialized();

        // 檢測方法是否有參數
        if (Parameters.IsNullOrEmpty())
        {
            return;
        }

        // 檢測方法是否爲公開方法
        if (!Method.IsPublic)
        {
            return;
        }

        // 若是沒有開啓方法參數檢測,則直接返回
        if (IsValidationDisabled())
        {
            return;                
        }

        // 若是方法所定義的參數數量與傳入的參數值數量匹配不上,則拋出系統級異常
        if (Parameters.Length != ParameterValues.Length)
        {
            throw new Exception("Method parameter count does not match with argument count!");
        }

        // 遍歷方法的參數列表,使用傳入的參數值進行校驗
        for (var i = 0; i < Parameters.Length; i++)
        {
            ValidateMethodParameter(Parameters[i], ParameterValues[i]);
        }

        // 若是校驗的錯誤結果集合有任意一條數據,則拋出用戶異常,返回給前端展現
        if (ValidationErrors.Any())
        {
            ThrowValidationError();
        }

        foreach (var objectToBeNormalized in ObjectsToBeNormalized)
        {
            objectToBeNormalized.Normalize();
        }
    }

    // ... 忽略的代碼
    
    // 校驗調用方法時傳遞的參數與參數值
    protected virtual void ValidateMethodParameter(ParameterInfo parameterInfo, object parameterValue)
    {
        // 若是參數值爲空的狀況下,作一系列特殊判斷
        if (parameterValue == null)
        {
            if (!parameterInfo.IsOptional && 
                !parameterInfo.IsOut && 
                !TypeHelper.IsPrimitiveExtendedIncludingNullable(parameterInfo.ParameterType, includeEnums: true))
            {
                ValidationErrors.Add(new ValidationResult(parameterInfo.Name + " is null!", new[] { parameterInfo.Name }));
            }

            return;
        }

        // 遞歸校驗參數
        ValidateObjectRecursively(parameterValue, 1);
    }

    protected virtual void ValidateObjectRecursively(object validatingObject, int currentDepth)
    {
        // 驗證層級是否超過了最大層級(8)
        if (currentDepth > MaxRecursiveParameterValidationDepth)
        {
            return;
        }

        // 值是否爲空,爲空則不繼續進行校驗
        if (validatingObject == null)
        {
            return;
        }

        // 判斷其類型是不是用戶配置的忽略類型,忽略則不進行校驗
        if (_configuration.IgnoredTypes.Any(t => t.IsInstanceOfType(validatingObject)))
        {
            return;
        }

        // 判斷參數類型是否爲基本類型
        if (TypeHelper.IsPrimitiveExtendedIncludingNullable(validatingObject.GetType()))
        {
            return;
        }

        SetValidationErrors(validatingObject);

        // 斷定參數類型是否實現了 IEnumerabe 接口,若是實現了,則遞歸遍歷校驗其內部的元素
        if (IsEnumerable(validatingObject))
        {
            foreach (var item in (IEnumerable) validatingObject)
            {
                ValidateObjectRecursively(item, currentDepth + 1);
            }
        }

        // 若是實現了標準化接口,則進行標準化操做
        if (validatingObject is IShouldNormalize)
        {
            ObjectsToBeNormalized.Add(validatingObject as IShouldNormalize);
        }

        // 是否還須要繼續遞歸校驗
        if (ShouldMakeDeepValidation(validatingObject))
        {
            var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();
            foreach (var property in properties)
            {
                // 若是有禁止校驗的特性則忽略
                if (property.Attributes.OfType<DisableValidationAttribute>().Any())
                {
                    continue;
                }

                ValidateObjectRecursively(property.GetValue(validatingObject), currentDepth + 1);
            }
        }
    }
    
    // ... 其餘代碼

    protected virtual bool ShouldValidateUsingValidator(object validatingObject, Type validatorType)
    {
        return true;
    }

    // 是否進行深度驗證
    protected virtual bool ShouldMakeDeepValidation(object validatingObject)
    {
        // 不須要遞歸集合對象
        if (validatingObject is IEnumerable)
        {
            return false;
        }

        var validatingObjectType = validatingObject.GetType();

        // 不須要遞歸基礎類型的對象
        if (TypeHelper.IsPrimitiveExtendedIncludingNullable(validatingObjectType))
        {
            return false;
        }

        return true;
    }
    
    // ... 其餘代碼
}

有朋友可能會奇怪,在方法內部不是經過 IEnumerable 判斷以後來進行遞歸校驗麼,爲何在最後面還有一個深度驗證呢?

這是由於當前對象除了是一個集合的狀況以外,還有可能其內部某個對象是另一個用戶所自定義的複雜對象,這個時候就必需要經過深度驗證來校驗各個參數的值。不過這個遞歸也是有限度的,經過 MaxRecursiveParameterValidationDepth 來控制這個迭代層數爲 8 層。若是不加以限制的話,那麼頗有可能出現循環引用而產生死循環的狀況,或者是層級過深致使接口相應緩慢。

那麼在這裏執行具體校驗操做的則是那些實現了 IMethodParameterValidator 接口的對象,這些對象在 Abp 核心模塊(AbpKernelModule)的預加載的時候被添加到了 Configuration.Validation.Validators 屬性當中。

固然用戶也能夠在本身的模塊預加載方法當中增長本身的參數驗證器,只要實現該接口便可。

public sealed class AbpKernelModule : AbpModule
{
    public override void PreInitialize()
    {
        // ... 其餘代碼
        // 增長鬚要忽略的類型
        AddIgnoredTypes();
        // 增長參數校驗器
        AddMethodParameterValidators();
    }

    private void AddMethodParameterValidators()
    {
        Configuration.Validation.Validators.Add<DataAnnotationsValidator>();
        Configuration.Validation.Validators.Add<ValidatableObjectValidator>();
        Configuration.Validation.Validators.Add<CustomValidator>();
    }

    // Abp 默認須要忽略的對象
    private void AddIgnoredTypes()
    {
        var commonIgnoredTypes = new[]
        {
            typeof(Stream),
            typeof(Expression)
        };

        foreach (var ignoredType in commonIgnoredTypes)
        {
            Configuration.Auditing.IgnoredTypes.AddIfNotContains(ignoredType);
            Configuration.Validation.IgnoredTypes.AddIfNotContains(ignoredType);
        }

        var validationIgnoredTypes = new[] { typeof(Type) };
        foreach (var ignoredType in validationIgnoredTypes)
        {
            Configuration.Validation.IgnoredTypes.AddIfNotContains(ignoredType);
        }
    }
}

以後呢,回到以前的校驗方法,能夠看到在 SetValidationErrors(object validatingObject) 方法裏面遍歷了以前被注入的驗證器集合,而後調用其 Validate() 方法來進行具體的參數校驗。

protected virtual void SetValidationErrors(object validatingObject)
{
    foreach (var validatorType in _configuration.Validators)
    {
        if (ShouldValidateUsingValidator(validatingObject, validatorType))
        {
            using (var validator = _iocResolver.ResolveAsDisposable<IMethodParameterValidator>(validatorType))
            {
                var validationResults = validator.Object.Validate(validatingObject);
                ValidationErrors.AddRange(validationResults);
            }
        }
    }
}

2.4 具體的參數驗證器

這裏以 Abp 默認實現的 DataAnnotationValidator 類型爲例,能夠看看他是怎麼來根據參數的數據註解來驗證參數是否正確的。

public class DataAnnotationsValidator : IMethodParameterValidator
{
    public virtual IReadOnlyList<ValidationResult> Validate(object validatingObject)
    {
        return GetDataAnnotationAttributeErrors(validatingObject);
    }
    
    protected virtual List<ValidationResult> GetDataAnnotationAttributeErrors(object validatingObject)
    {
        var validationErrors = new List<ValidationResult>();

        var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();
        // 得到參數值的全部屬性,若是傳入的是一個 DTO 對象的話,他內部確定會有不少屬性的
        foreach (var property in properties)
        {
            var validationAttributes = property.Attributes.OfType<ValidationAttribute>().ToArray();
            // 沒有數據註解特性,跳過當前屬性處理
            if (validationAttributes.IsNullOrEmpty())
            {
                continue;
            }

            // 建立一個錯誤信息上下文,用戶數據註解工具進行校驗
            var validationContext = new ValidationContext(validatingObject)
            {
                DisplayName = property.DisplayName,
                MemberName = property.Name
            };

            // 根據特性來校驗參數結果
            foreach (var attribute in validationAttributes)
            {
                var result = attribute.GetValidationResult(property.GetValue(validatingObject), validationContext);
                if (result != null)
                {
                    validationErrors.Add(result);
                }
            }
        }

        return validationErrors;
    }
}

3. 後記

最近工做較忙,可能更新速度不會像原來那麼快,不過我儘量在國慶結束後完成剩餘文章,謝謝你們的支持。

4.點此跳轉到總目錄

相關文章
相關標籤/搜索