Abp 源码分析:四、模块配置

0.简要介绍

在 Abp 框架当中通过各种 Configuration 来实现模块的配置,Abp 本身提供的很多基础设施功能的一些在运行时的行为是通过很多不同的 Configuration 来开放给用户进行一些自定义配置的。

比如说缓存模块,我要配置缓存的过期时间,Abp 默认是 1 个小时,但是我也可以自己来定义,直接赋值或者从配置项来读取都是由具体使用者来控制的,所以 Abp 通过各种 Configuration 类来控制一些运行时参数。

这些 Abp 本身基础设施的配置类都是存放在 \Abp\src\Abp\Configuration\Startup\ 这个文件夹内部的,我们来看一下他们的依赖关系。

1530004535999

1.启动流程

从上图可以看到在 IAbpStartupConfiguration 内部拥有诸多引用(可能没有列举完成,可以在其定义看到),基本上 Abp 自己的基础设施配置都在这里面。

那么 IAbpStartupConfiguration 自己内部的这些属性是在哪儿初始化的呢,其实就是在之前讲过的 AbpBootstrapperInitialize() 内部初始化的。再看下代码:

public virtual void Initialize()
{
    try
    {
        // 其他代码
        IocManager.IocContainer.Install(new AbpCoreInstaller());
        IocManager.Resolve<AbpStartupConfiguration>().Initialize();
		// 其他代码
    }
    catch (Exception ex)
    {
        _logger.Fatal(ex.ToString(), ex);
        throw;
    }
}

AbpCoreInstaller 类内部之前也说过,在这里面统一注入了这些 Configuration 的单例,同时解析出 AbpStartupConfiguration ,调用其 Initialzie() 方法来对自己的那些 xxxConfiguration 接口赋值,代码如下:

public void Initialize()
{
    Localization = IocManager.Resolve<ILocalizationConfiguration>();
    Modules = IocManager.Resolve<IModuleConfigurations>();
    Features = IocManager.Resolve<IFeatureConfiguration>();
    Navigation = IocManager.Resolve<INavigationConfiguration>();
    Authorization = IocManager.Resolve<IAuthorizationConfiguration>();
    Validation = IocManager.Resolve<IValidationConfiguration>();
    Settings = IocManager.Resolve<ISettingsConfiguration>();
    UnitOfWork = IocManager.Resolve<IUnitOfWorkDefaultOptions>();
    EventBus = IocManager.Resolve<IEventBusConfiguration>();
    MultiTenancy = IocManager.Resolve<IMultiTenancyConfig>();
    Auditing = IocManager.Resolve<IAuditingConfiguration>();
    Caching = IocManager.Resolve<ICachingConfiguration>();
    BackgroundJobs = IocManager.Resolve<IBackgroundJobConfiguration>();
    Notifications = IocManager.Resolve<INotificationConfiguration>();
    EmbeddedResources = IocManager.Resolve<IEmbeddedResourcesConfiguration>();
    EntityHistory = IocManager.Resolve<IEntityHistoryConfiguration>();

    CustomConfigProviders = new List<ICustomConfigProvider>();
    ServiceReplaceActions = new Dictionary<Type, Action>();
}

所以,在模块定义的基类 AbpModule 当中,早就注入了 IAbpStartupConfiguration 接口,让你很方便的就可以在模块的预加载的时候配置各种基础设施的参数。举个栗子:

public override void PreInitialize()
{
    Configuration.Caching.ConfigureAll(z=>z.DefaultSlidingExpireTime = TimeSpan.FromHours(1));
}

1530003198685

可以看到这里我们的 Configuration 属性其实就是 IAbpStartupConfiguration 接口。

2.代码分析

2.1自定义模块配置

我们可以看到 IAbpStartupConfiguration 除了自己拥有大量基础设施的配置类,同时他还继承一个基类叫做 DictionaryBasedConfig ,那么 Abp 框架为什么要这么写呢?

其实这个基类的作用就是存放用户自定义的 Configuration 类型的,细心观察的话会发现在 AbpStartupConfiguration 的内部有一个 Get 方法,该方法就是用来获取存储的配置类型。

public T Get<T>()
{
    // 调用基类的 GetOrCreate 方法,不存在的话直接从 IocContainer 中解析
    return GetOrCreate(typeof(T).FullName, () => IocManager.Resolve<T>());
}

DictionaryBasedConfig 中维护了一个字典 CustomSettings ,其 Key/Value 类型为 string/object ,因为在 Abp 框架当中是不知道你自定义模块配置类的类型的,所以存了一个 object 对象。

然后就有以下用法,首先在模块 PreInitialize() 方法当中注入你需要注入的配置类:

public override void PreInitialize()
{
    // 注入配置类
    IocManager.Register<IAbpAspNetCoreConfiguration, AbpAspNetCoreConfiguration>();

    // 替换服务,后面讲解
    Configuration.ReplaceService<IPrincipalAccessor, AspNetCorePrincipalAccessor>(DependencyLifeStyle.Transient);
    Configuration.ReplaceService<IAbpAntiForgeryManager, AbpAspNetCoreAntiForgeryManager>(DependencyLifeStyle.Transient);
    Configuration.ReplaceService<IClientInfoProvider, HttpContextClientInfoProvider>(DependencyLifeStyle.Transient);
}

然后针对 IModuleConfigurations 写一个扩展方法,因为在 IModuleConfigurations 内部就有一个 IAbpAspNetCoreConfiguration 的实例,IModuleConfigurations 的注释就说该接口是用于配置模块的,模块可以通过编写扩展方法来添加自己的 Configuration 类:

public static class AbpAspNetCoreConfigurationExtensions
{
    /// <summary>
    /// Used to configure ABP ASP.NET Core module.
    /// </summary>
    public static IAbpAspNetCoreConfiguration AbpAspNetCore(this IModuleConfigurations configurations)
    {
        // 两种写法都差不多
        return configurations.AbpConfiguration.GetOrCreate("AbpModule", () => IocManager.Resolve<IAbpAspNetCoreConfiguration>());
        return configurations.AbpConfiguration.Get<IAbpAspNetCoreConfiguration>();
    }
}

2.2 服务实现替换

在 Abp 当中允许我们替换一些他本身的一些实现,只要你是在模块进行预加载的时候替换的话,都是可以的。而 Abp 他本身在 IAbpStartupConfiguration 当中提供了一个方法叫做 ReplaceService() 方法专门来让你替换服务。

我们来看一下他的定义:

void ReplaceService(Type type, Action replaceAction);

emmmm,传入一个 TypeAction,咋跟我看到的不一样呢,Ctrl + N 搜索了一下,发现在模块里面使用的 ReplaceService() 方法是存放在 AbpStartupConfigurationExtensions 里面编写的一个静态方法,其定义如下:

public static void ReplaceService<TType, TImpl>(this IAbpStartupConfiguration configuration, DependencyLifeStyle lifeStyle = DependencyLifeStyle.Singleton)
	where TType : class
	where TImpl : class, TType
{
	configuration.ReplaceService(typeof(TType), () =>
	{
		configuration.IocManager.Register<TType, TImpl>(lifeStyle);
	});
}

我来看看,传入一个 Type 和 一个 ActionType 用来调用 IAbpStartupConfiguration 的同名方法,Action 则是用来注册组件的。

原来如此,我们再来到 IAbpStartupConfiguration.ReplaceService(Type type, Action replaceAction) 的具体实现:

public Dictionary<Type, Action> ServiceReplaceActions { get; private set; }

public void ReplaceService(Type type, Action replaceAction)
{
    ServiceReplaceActions[type] = replaceAction;
}

唔,就是一个字典嘛,我们来看看在什么地方用到过它。

public override void Initialize()
{
	foreach (var replaceAction in ((AbpStartupConfiguration)Configuration).ServiceReplaceActions.Values)
	{
		replaceAction();
	}
    
    // 其他代码
}

最后我们看到在 AbpKernelModuleInitialize() 方法里面就会遍历这个字典,来调用之前存入的 Action

因为 Abp 所有组件的注册都是在模块 Initialize() 内部来进行注册的,而这串代码刚好又放在 AbpKernelModule 的初始化方法的第一行就开始执行,所以确保你替换的组件能够在 Abp 内部组件注册前执行。

所以当你要替换 Abp 内置组件服务的时候一定要记住在模块的 PreInitialize() 里面执行哦~

3. 扩展:Abp 支持多数据库

如果你的 Abp 项目有多个数据库上下文实体的时候怎么办呢?

在 Abp 官方 Demo 当中就有说明,你可以通过替换默认的 IConnectionStringResolver 来实现不同数据库的解析哦~,我们继承 DefaultConnectionStringResolver 实现一个 MulitDbContextConnectionStringResolver

public class MulitDbContextConnectionStringResolver : DefaultConnectionStringResolver
{
	public HKERPConnectionStringResolver(IAbpStartupConfiguration configuration)
		: base(configuration)
	{
	}

	public override string GetNameOrConnectionString(ConnectionStringResolveArgs args)
	{
		if (args["DbContextConcreteType"] as Type == typeof(ADbContext))
		{
			var configuration = AppConfigurations.Get(WebContentDirectoryFinder.CalculateContentRootFolder());
			// 返回 ADbContext 的 ConnectionString
			return configuration.GetConnectionString(AllConsts.ADbConnectionStringName);
		}
		
        if (args["DbContextConcreteType"] as Type == typeof(BDbContext))
		{
			var configuration = AppConfigurations.Get(WebContentDirectoryFinder.CalculateContentRootFolder());
			// 返回 BDbContext 的 ConnectionString
			return configuration.GetConnectionString(HKERPCRMConsts.BDbConnectionStringName);
		}
		
		// 都不是则使用默认的数据库连接字符串

		return base.GetNameOrConnectionString(args);
	}

}

然后在我们的 EFCore 模块的预加载方法当中加入以下代码:

Configuration.ReplaceService(typeof(IConnectionStringResolver), () =>
{
	IocManager.IocContainer.Register(
		Component.For<IConnectionStringResolver, IDbPerTenantConnectionStringResolver>()
			.ImplementedBy<MulitDbContextConnectionStringResolver>()
			.LifestyleTransient()
		);
});

当然你也不要忘记在后面通过 AddDbContext() 方法来把你的数据库上下文添加到 Abp 里面去哦。

Configuration.Modules.AbpEfCore().AddDbContext<ADbContext>(options=>{ /*配置代码*/});
Configuration.Modules.AbpEfCore().AddDbContext<BDbContext>(options=>{ /*配置代码*/});