Util應用框架核心(二) - 啟動器

何鎮汐發表於2023-10-30

本節介紹 Util 專案啟動初始化過程.

文章分為多個小節,如果對設計原理不感興趣,只需閱讀基礎用法部分即可.

基礎用法

檢視 Util 服務配置,範例:

var builder = WebApplication.CreateBuilder( args );
builder.AsBuild()
  .AddAop()
  .AddSerilog()
  .AddUtil();

注意其中呼叫了 AddUtil 方法.

AddUtil 方法呼叫啟動器進行初始化.

設計動機

有些服務需要配置,但並不需要傳遞配置引數.

對於這類服務,我們希望自動完成配置,而不是手工呼叫 AddXXX() 方法.

Util專案需要一種自動執行特定初始化程式碼的方法.

Util啟動時掃描全部程式集,找出特定程式碼塊,並執行它們.

這些被自動執行的程式碼塊,稱為服務註冊器.

Util 啟動器的設計和程式碼主要從 NopCommerce 吸收而來,並在專案實戰中不斷改進.

採用程式集掃描,是一種簡單輕量的啟動方式,不需要進行任何配置.

原始碼解析

AddUtil 擴充套件方法

IHostBuilderIAppBuilder 介面上擴充套件了 AddUtil 方法.

AddUtil 方法呼叫 Bootstrapper 啟動器的 Start 方法,掃描程式集執行服務註冊器.

通常你不需要呼叫 Bootstrapper 類啟動,使用 AddUtil 擴充套件方法會更簡單.

/// <summary>
/// 主機生成器服務擴充套件
/// </summary>
public static class IHostBuilderExtensions {
    /// <summary>
    /// 啟動Util服務 
    /// </summary>
    /// <param name="hostBuilder">主機生成器</param>
    public static IHostBuilder AddUtil( this IHostBuilder hostBuilder ) {
        hostBuilder.CheckNull( nameof( hostBuilder ) );
        var bootstrapper = new Bootstrapper( hostBuilder );
        bootstrapper.Start();
        return hostBuilder;
    }

    /// <summary>
    /// 啟動Util服務 
    /// </summary>
    /// <param name="appBuilder">應用生成器</param>
    public static IAppBuilder AddUtil( this IAppBuilder appBuilder ) {
        appBuilder.CheckNull( nameof( appBuilder ) );
        var bootstrapper = new Bootstrapper( appBuilder.Host );
        bootstrapper.Start();
        return appBuilder;
    }
}

Bootstrapper 啟動器

啟動器使用型別查詢器 ITypeFinder 找出所有啟用的服務註冊器 IServiceRegistrar,並根據 OrderId 屬性排序.

使用反射建立服務註冊器例項,並將主機生成器 IHostBuilder 例項傳遞給它.

執行服務註冊器例項的 Register 方法,完成服務初始化工作.

/// <summary>
/// 啟動器
/// </summary>
public class Bootstrapper {
    /// <summary>
    /// 主機生成器
    /// </summary>
    private readonly IHostBuilder _hostBuilder;
    /// <summary>
    /// 程式集查詢器
    /// </summary>
    private readonly IAssemblyFinder _assemblyFinder;
    /// <summary>
    /// 型別查詢器
    /// </summary>
    private readonly ITypeFinder _typeFinder;
    /// <summary>
    /// 服務配置操作列表
    /// </summary>
    private readonly List<Action> _serviceActions;

    /// <summary>
    /// 初始化啟動器
    /// </summary>
    /// <param name="hostBuilder">主機生成器</param>
    public Bootstrapper( IHostBuilder hostBuilder ) {
        _hostBuilder = hostBuilder ?? throw new ArgumentNullException( nameof( hostBuilder ) );
        _assemblyFinder = new AppDomainAssemblyFinder { AssemblySkipPattern = BootstrapperConfig.AssemblySkipPattern };
        _typeFinder = new AppDomainTypeFinder( _assemblyFinder );
        _serviceActions = new List<Action>();
    }

    /// <summary>
    /// 啟動
    /// </summary>
    public virtual void Start() {
        ConfigureServices();
        ResolveServiceRegistrar();
        ExecuteServiceActions();
    }

    /// <summary>
    /// 配置服務
    /// </summary>
    protected virtual void ConfigureServices() {
        _hostBuilder.ConfigureServices( ( context, services ) => {
            Util.Helpers.Config.SetConfiguration( context.Configuration );
            services.TryAddSingleton( _assemblyFinder );
            services.TryAddSingleton( _typeFinder );
        } );
    }

    /// <summary>
    /// 解析服務註冊器
    /// </summary>
    protected virtual void ResolveServiceRegistrar() {
        var types = _typeFinder.Find<IServiceRegistrar>();
        var instances = types.Select( type => Reflection.CreateInstance<IServiceRegistrar>( type ) ).Where( t => t.Enabled ).OrderBy( t => t.OrderId ).ToList();
        var context = new ServiceContext( _hostBuilder, _assemblyFinder, _typeFinder );
        instances.ForEach( t => _serviceActions.Add( t.Register( context ) ) );
    }

    /// <summary>
    /// 執行延遲服務註冊操作
    /// </summary>
    protected virtual void ExecuteServiceActions() {
        _serviceActions.ForEach( action => action?.Invoke() );
    }
}

ITypeFinder 型別查詢器

應用程式域型別查詢器 AppDomainTypeFinder 使用程式集查詢器 IAssemblyFinder 獲取程式集列表.

並從程式集中查詢指定介面的實現型別.

/// <summary>
/// 型別查詢器
/// </summary>
public interface ITypeFinder {
    /// <summary>
    /// 查詢型別列表
    /// </summary>
    /// <typeparam name="T">查詢型別</typeparam>
    List<Type> Find<T>();
    /// <summary>
    /// 查詢型別列表
    /// </summary>
    /// <param name="findType">查詢型別</param>
    List<Type> Find( Type findType );
}

/// <summary>
/// 應用程式域型別查詢器
/// </summary>
public class AppDomainTypeFinder : ITypeFinder {
    /// <summary>
    /// 程式集查詢器
    /// </summary>
    private readonly IAssemblyFinder _assemblyFinder;

    /// <summary>
    /// 初始化應用程式域型別查詢器
    /// </summary>
    /// <param name="assemblyFinder">程式集查詢器</param>
    public AppDomainTypeFinder( IAssemblyFinder assemblyFinder ) {
        _assemblyFinder = assemblyFinder ?? throw new ArgumentNullException( nameof( assemblyFinder ) );
    }

    /// <summary>
    /// 查詢型別列表
    /// </summary>
    /// <typeparam name="T">查詢型別</typeparam>
    public List<Type> Find<T>() {
        return Find( typeof( T ) );
    }

    /// <summary>
    /// 獲取程式集列表
    /// </summary>
    public List<Assembly> GetAssemblies() {
        return _assemblyFinder.Find();
    }

    /// <summary>
    /// 查詢型別列表
    /// </summary>
    /// <param name="findType">查詢型別</param>
    public List<Type> Find( Type findType ) {
        return Reflection.FindImplementTypes( findType, GetAssemblies()?.ToArray() );
    }
}

IAssemblyFinder 程式集查詢器

應用程式域程式集查詢器 AppDomainAssemblyFinder 掃描當前應用程式域,獲取全部程式集.

值得注意的是,如果在應用程式域所有程式集中進行查詢,必定效率十分低下,啟動將異常緩慢.

我們掃描程式集的目的,是希望從中獲得服務註冊器.

只有Util應用框架和你的專案相關的程式集中,才有可能包含服務註冊器.

所以排除掉 .Net 和第三方類庫程式集,將能大大提升掃描查詢效率.

/// <summary>
/// 程式集查詢器
/// </summary>
public interface IAssemblyFinder {
    /// <summary>
    /// 程式集過濾模式
    /// </summary>
    public string AssemblySkipPattern { get; set; }
    /// <summary>
    /// 查詢程式集列表
    /// </summary>
    List<Assembly> Find();
}

/// <summary>
/// 應用程式域程式集查詢器
/// </summary>
public class AppDomainAssemblyFinder : IAssemblyFinder {
    /// <summary>
    /// 程式集過濾模式
    /// </summary>
    public string AssemblySkipPattern { get; set; }
    /// <summary>
    /// 程式集列表
    /// </summary>
    private List<Assembly> _assemblies;

    /// <summary>
    /// 獲取程式集列表
    /// </summary>
    public List<Assembly> Find() {
        if ( _assemblies != null )
            return _assemblies;
        _assemblies = new List<Assembly>();
        LoadAssemblies();
        foreach( var assembly in AppDomain.CurrentDomain.GetAssemblies() ) {
            if( IsSkip( assembly ) )
                continue;
            _assemblies.Add( assembly );
        }
        return _assemblies;
    }

    /// <summary>
    /// 載入引用但尚未呼叫的程式集列表到當前應用程式域
    /// </summary>
    protected virtual void LoadAssemblies() {
        var currentDomainAssemblies = AppDomain.CurrentDomain.GetAssemblies();
        foreach( string file in GetLoadAssemblyFiles() )
            LoadAssembly( file, currentDomainAssemblies );
    }

    /// <summary>
    /// 獲取需要載入的程式集檔案列表
    /// </summary>
    protected virtual string[] GetLoadAssemblyFiles() {
        return Directory.GetFiles( AppContext.BaseDirectory, "*.dll" );
    }

    /// <summary>
    /// 載入程式集到當前應用程式域
    /// </summary>
    protected void LoadAssembly( string file, Assembly[] currentDomainAssemblies ) {
        try {
            var assemblyName = AssemblyName.GetAssemblyName( file );
            if( IsSkip( assemblyName.Name ) )
                return;
            if( currentDomainAssemblies.Any( t => t.FullName == assemblyName.FullName ) )
                return;
            AppDomain.CurrentDomain.Load( assemblyName );
        }
        catch( BadImageFormatException ) {
        }
    }

    /// <summary>
    /// 是否過濾程式集
    /// </summary>
    protected bool IsSkip( string assemblyName ) {
        var applicationName = Assembly.GetEntryAssembly()?.GetName().Name;
        if ( assemblyName.StartsWith( $"{applicationName}.Views" ) )
            return true;
        if( assemblyName.StartsWith( $"{applicationName}.PrecompiledViews" ) )
            return true;
        if ( string.IsNullOrWhiteSpace( AssemblySkipPattern ) )
            return false;
        return Regex.IsMatch( assemblyName, AssemblySkipPattern, RegexOptions.IgnoreCase | RegexOptions.Compiled );
    }

    /// <summary>
    /// 是否過濾程式集
    /// </summary>
    private bool IsSkip( Assembly assembly ) {
        return IsSkip( assembly.FullName );
    }
}

配置程式集過濾列表

Util應用框架已經排除了引用的所有依賴庫程式集.

但你的專案可能引用其它第三方類庫,如果只引用了少量類庫,影響非常小,但引用大量類庫,則必須配置程式集過濾列表.

如果你不想在每個專案配置程式集過濾,可以讓Util應用框架更新過濾列表,請把要過濾的程式集名稱告訴我們.

Util.Infrastructure.BootstrapperConfig 是啟動器配置, AssemblySkipPattern 屬性提供了程式集過濾列表.

程式集過濾列表是一個正規表示式,使用 | 分隔程式集,使用 ^ 匹配起始名稱過濾.

範例1

如果你想排除名為 Demo 的程式集.

BootstrapperConfig.AssemblySkipPattern += "|Demo";

builder.AsBuild().AddUtil();

必須在 AddUtil 之前設定 BootstrapperConfig.AssemblySkipPattern 屬性.

範例2

排除 Demo 開頭的程式集,比如 Demo.A,Demo.B .

BootstrapperConfig.AssemblySkipPattern += "|^Demo";

相關文章