Shashlik.Kernel - dotnet-shashlik/shashlik GitHub Wiki
- 配置类自动装载
- 约定式服务注册
- 服务注册条件
- 自动装配
- 全局锁
service.AddShashlik(Configuration);
这一句代码就完成了整个 Shashlik 装配处理,但你也有必要了解其内部处理流程。
/// <summary>
/// AddShashlik,执行典型的shashlik服务配置
/// </summary>
/// <param name="services"></param>
/// <param name="rootConfiguration">根配置</param>
/// <param name="dependencyContext">依赖上下文,null使用默认配置</param>
/// <param name="disableAutoOptionTypes">禁用自动装配的option类型</param>
/// <returns></returns>
public static IServiceCollection AddShashlik(
this IServiceCollection services,
IConfiguration rootConfiguration,
DependencyContext? dependencyContext = null,
params Type[] disableAutoOptionTypes)
{
return services.AddShashlikCore(rootConfiguration, dependencyContext)
// 配置装载
.AutowireOptions(disableAutoOptionTypes)
// 注册约定的服务
.RegistryConventionServices()
// 自动服务装配
.AutowireServices()
// 执行服务过滤
.DoFilter();
}
- AddShashlikCore:注册 Shashlik 核心服务。dependencyContext:Shashlik 扫描的整个应用依赖上下文,建议使用默认 null 即默认的依赖上下文。
- AutowireOptions: 开始处理配置文件自动装载,该方法执行后就可以直接注入 IOptions 相关接口获取配置了。disableAutoOptionTypes:需要禁用自动装配的配置类,如果你想手动配置某个类型可以通过此参数禁用。
- RegistryConventionServices: 按约定方式注册所有的服务,即包含
Transient
、Singleton
、Scoped
特性的类和接口。 - AutowireServices: 自动装配。即继承自
IAutowire
的类型。 - DoFilter: 执行服务注册条件过滤。
启动 Shashlik。主要执行IServiceProviderAutowire
的自动装配,以及为 Shashlik 提供扩展基础(如 EfCore 的自动迁移、AspNetCore 相关装配),并注册静态GlobalKernelServiceProvider
全局根IServiceProvider
。
public static IKernelServiceProvider UseShashlik(this IServiceProvider serviceProvider)
{
var kernelServiceProvider = new InnerKernelServiceProvider(serviceProvider);
kernelServiceProvider.AutowireServiceProvider();
GlobalKernelServiceProvider.InitServiceProvider(kernelServiceProvider);
return kernelServiceProvider;
}
使用 Shashlik 前:
service.Configure<AMapOptions>(Configuration.GetSection("Shashlik.AMap"));
当你的应用中充满大量配置类时,你就会有大量的这种配置代码,现在使用AutoOptions
吧,做到 0 代码配置装载。
使用 Shashlik 后:
[AutoOptions("Shashlik.AMap")]
public class AMapOptions{}
构造函数:public AutoOptionsAttribute(string section, bool supportDot = true)
。
-
string section
: 配置阶段名称 -
bool supportDot
:是否支持将.
转换为:
,一般.
更符合配置习惯,如果配置节点名称本身包含:
那你需要设为false
。
Shashlik 已定义Transient
、Singleton
、Scoped
三个特性来用于注册你的服务,如果还不满足你的骚操作,你可以自行实现ServiceAttribute
类。
两个重要参数
- IgnoreServices:需要忽略的服务类型集合
- RequireRegistryInheritedChain:是否需要注册整个继承链,仍然会忽略
IgnoreServices
定义的忽略类型。
重要规则
- 特性使用在接口或抽象类上:将注册接口和最终实现类(继承链没有子类)为服务。
- 特性使用在实现类上:注册该类所有的父类、接口为服务,不会注册子类
- 允许规则
1
和规则2
组合,即继承链上可以同时包含多个特性标签,Shashlik 将组合注册这些服务。 - 一个继承链上不允许包含多种生命周期类型:比如如下代码是不允许的:
[Singleton] public interface IA {} [Transient] public class A : IA {}
下面通过实际例子来说明这些规则。
- 注册自己
[Singleton]
public class A {}
GetService<A>().ShouldBeOfType<A>();
- 使用在接口上,注册接口和最终实现类
[Singleton]
public interface IA {}
public class A : IA {}
// 注册 IA服务,实现类为A
GetService<IA>().ShouldBeOfType<A>();
GetService<A>().ShouldBeOfType<A>();
- 使用在抽象类上,注册接口和最终实现类
[Singleton]
public abstract class AA {}
public class A : AA {}
// 注册 IA服务,实现类为A
GetService<AA>().ShouldBeOfType<A>();
GetService<A>().ShouldBeOfType<A>();
- 使用在接口上,仅注册接口和最终实现类
[Singleton]
public interface IA {}
public abstract class AA : IA {}
public class A : AA {}
// 注册 IA服务,实现类为A
GetService<IA>().ShouldBeOfType<A>();
// 不会注册中间的类型和接口,如果需要同时注册AA,可以在AA上同时添加Singleton或者A类型上添加Singleton,再或者设置RequireRegistryInheritedChain=true
GetService<AA>().ShouldBeNull();
GetService<A>().ShouldBeOfType<A>();
- 使用在实现类上,注册自身和所有父类、接口
public interface IA {}
[Singleton]
public class A : IA {}
// 注册IA和A为服务,实现类为A
GetService<IA>().ShouldBeOfType<A>();
GetService<A>().ShouldBeOfType<A>();
- 使用在接口上,注册最终实现类。
[Singleton]
public interface IA {}
public class A : IA {}
public class B : A {}
// 注册IA和B为服务,实现类为B
GetService<IA>().ShouldBeOfType<B>();
GetService<B>().ShouldBeOfType<B>();
// 并不会注册A,因为A并不是最终实现类
GetService<A>().ShouldBeNull();
- 泛型,会注册匹配的泛型定义最终实现类,以及对应的泛型实现
[Singleton]
public interface IA<T> {}
public class A<T> : IA<T> {}
public class B : IA<int> {}
// A<T>和IA<T>是匹配的泛型定义,等同于注册service.AddSinleton(typeof(IA<>), typeof(T<>));
GetServices<IA<int>>().Any(r => r.GetType() == typeof(A<int>)).ShouldBeTrue();
// B实现了IA<int>,等同于service.AddSinleton<IA<int>, B>();
GetServices<IA<int>>().Any(r => r.GetType() == typeof(B)).ShouldBeTrue();
GetServices<IA<string>>().Any(r => r.GetType() == typeof(A<int>)).ShouldBeTrue();
GetServices<IA<string>>().Any(r => r.GetType() == typeof(B)).ShouldBeFalse();
GetService<A<int>>().ShouldBeNotNull();
GetService<A<string>>().ShouldBeNotNull();
GetService<B>().ShouldBeNotNull();
- 忽略服务类型,某些服务需要忽略时使用,必须继承链中有 IDispose 等完全没必要注册的接口
[Singleton(typeof(IA))]
public interface IA {}
public class A : IA {}
// 忽略IA类型服务
GetService<IA>().ShouldBeNull();
// 只会注册最终实现类
GetService<A>().ShouldBeOfType<A>();
-
IgnoreServices
忽略服务类型,某些服务需要忽略时使用,比如继承链中有 IDispose 等完全没必要注册的接口
[Singleton(typeof(IA))]
public interface IA {}
public class A : IA {}
// 忽略IA类型服务
GetService<IA>().ShouldBeNull();
// 只会注册最终实现类
GetService<A>().ShouldBeOfType<A>();
-
RequireRegistryInheritedChain
注册整个继承链
public interface IA : IDispose{}
public interface IB : IDispose{}
// 注册整个继承链,但是忽略IDispose
[Singleton(typeof(IDispose), RequireRegistryInheritedChain = true)]
public abstract class A : IA {}
public class B : A {}
GetService<IDispose>().ShouldBeNull();
GetService<IA>().ShouldBeOfType<B>();
GetService<A>().ShouldBeOfType<B>();
GetService<B>().ShouldBeOfType<B>();
// IB和A并不是同一继承链,他们没任何关系,不会注册
GetService<IB>().ShouldBeNull();
特性LatestImplementationAttribute
用于标记实现类作为最后的实现类,一般用于某接口有多个实现时需要指定某个类为最后的实现,在依赖注入系统中,一个服务可能会有多个实现,但最后使用时GetService
都是获取的最后一个实现类。那么这个特性就是满足这个需求。一个服务有多个实现类都定义了LatestImplementationAttribute
将变得毫无意义且最后的结果可能不是预期的。例:
[Singleton]
public interface ICache {}
public class MemoryCache : ICache {}
// 将RedisCache标记为最后实现类
[LatestImplementation]
public class RedisCache : ICache {}
public class MemrcachedCache : ICache {}
GetService<ICache>().ShouldBeOfType<RedisCache>();
- 应该尽量把约定特性使用在实现类上,显示的通过
IgnoreServices
参数忽略掉不需要的注册的服务,因为接口和抽象类的实现类一般都是未知的,他们的继承链情况也是未知的。 - 如果你设计的接口需要实现自动注册,可以在接口上使用约定特性,那么通过 GetServices 方法可以获取到所有的注册服务,这经常用于自动化处理。例,设计一个付款接口,付款方式可以有很多种。
//
[Singleton]
public interface IPay{
// 支付通道
string PayChannel { get; }
// 请求支付数据
PayData GetPayData(PayInput input);
}
public class WxJsapiPay : IPay {}
public class WxAppPay : IPay {}
public class AliH5Pay : IPay {}
public class WxAppPay : IPay {}
这里就注册四种支付通道,用户在实际支付时,传入支付参数payChannel
就可以获取到具体的支付方式。
var pay = GetServices<IPay>().First(r => r.PayChannel == payChannel);
pay.GetPayData(input);
有些类型我们希望在满足一定条件下才进行服务注册,这个时候就可以使用条件注册。比如当系统中不存在 ICache 服务时,才注册 MemoryCache。
所有的条件注册特性都继承自ConditionBaseAttribute
,仅允许使用在实现类上,可以任意扩展,Shashlik 已实现如下条件特性:
-
ConditionDependsOn
:当服务类型存在时-
Type[] Types
: 服务类型 -
ConditionType ConditionType
:条件类型,ALL
:所有服务类型都存在时才注册,ANY
:只要某一个服务类型存在时就注册,默认值ALL
-
-
ConditionDependsOnMissing
:当服务类型不存在时-
Type[] Types
: 服务类型 -
ConditionType ConditionType
:条件类型,ALL
:必须所有服务类型都不存在时才注册,ANY
:只要某一个服务类型不存在时就注册,默认值ALL
-
-
ConditionOnHostEnvironment
:当前运行环境满足条件时-
string EnvName
: 运行环境值,比如当前环境为Production
-
bool IgnoreCase
: 是否忽略大小写,默认true
-
-
ConditionOnProperty
:当配置数据的值满足条件时-
string Property
: 属性名称,例:Shashlik.Sms.Enable -
Type ValueType
: 条件值的类型 -
object? Value
: 条件值 -
object? DefaultValue
: 默认条件值,当不存在该配置数据时的值,默认为 null -
bool IgnoreCase
: 当条件值类型为string
时,是否忽略大小写,默认true
-
bool SupportDot
: 是否支持将.
转换为:
,一般.
更符合配置习惯,如果配置节点名称本身包含:
那你需要设为false
,默认true
-
例:
// 当不存在ICache时,注册MemoryCache
[ConditionDependsOnMissing(typeof(ICache))]
[Singleton]
public class MemoryCache : ICache {}
// 当系统中存在CSRedisClient服务才注册RedisLock
[ConditionDependsOn(typeof(CSRedisClient))]
[Singleton]
public class RedisLock : ILock {}
// 当前环境为测试环境时注册EmptySms
[ConditionOnHostEnvironment("Test")]
[Singleton]
public class EmptySms : ISms {}
// 当属性值Shashlik.Sms.Enable为true时,注册AliyunSms
[ConditionOnProperty(typeof(bool), "Shashlik.Sms.Enable", true, DefaultValue = true)]
[Singleton]
public class AliyunSms : ISms {}
Shashlik 的自动装配和 Spring 框架的自动不是一个概念,如果你熟悉 Abp 框架,你可以理解为 Abp 的 Module 类型,但却比 Abp 的更加强大、灵活。
下面通过 Shashlik.Redis 的自动装配例子来说明:
[AutoOptions("Shashlik.Redis")]
public class RedisOptions
{
/// <summary>
/// 是否启用
/// </summary>
public bool Enable { get; set; } = true;
/// <summary>
/// 连接字符串
/// </summary>
public string? ConnectionString { get; set; }
/// <summary>
/// 哨兵配置
/// </summary>
public string[] Sentinels { get; set; } = new string[0];
/// <summary>
/// 只读
/// </summary>
public bool Readonly { get; set; }
/// <summary>
/// 自定义Client创建,高优先级
/// </summary>
public Func<CSRedisClient>? CSRedisClientFactory { get; set; }
}
/// <summary>
/// redis自动装配,装配顺序200
/// </summary>
[Order(200)]
public class RedisAutowire : IServiceAutowire
{
public RedisAutowire(IOptions<RedisOptions> options)
{
Options = options.Value;
}
private RedisOptions Options { get; }
public void Configure(IKernelServices kernelService)
{
if (!Options.Enable)
return;
var csRedis = Options.CSRedisClientFactory?.Invoke() ?? new CSRedisClient(Options.ConnectionString, Options.Sentinels, Options.Readonly);
RedisHelper.Initialization(csRedis);
kernelService.Services.AddSingleton(csRedis);
kernelService.Services.TryAddSingleton<IRedisSnowflakeId, DefaultRedisSnowflakeId>();
kernelService.Services.AddSingleton<IDistributedCache>(
new Microsoft.Extensions.Caching.Redis.CSRedisCache(RedisHelper.Instance));
}
}
首先约定式服务注册不是万能的,在引用一些三方框架时,就需要手动去注册一些服务,这里我们就需要手动注册CSRedisClient
和CSRedisCache
。首先 Shashlik.Redis 设计必须是可复用,也不希望使用方自己来注册这些服务,只要引用了 Shashlik.Redis 并且没有关闭 Redis(Shashlik.Redis.Enable
),那么这些服务的注册希望完全自动化,也就是使用者 0 代码,这就是IServiceAutowire
服务自动装配的使用。我们可以在不同的场景定义、使用不同的自动装配。
自动装配类型结构:
- IAutoWire:自动装配基础接口,已定义约定
[Transient(typeof(IAutowire), RequireRegistryInheritedChain = true)]
,即所有的自动装配类型都将自动完成注册。- IServiceAutoWire:服务自动装配,在服务注册阶段,在 AddShashlik 的
.AutowireServices()
开始装配,一般用于模块的配置、服务注册。当约定式服务注册不能满足需求时,一般使用IServiceAutoWire
来处理。- RedisAutowire: Redis 自动装配。
- IdentityAutowire:AspNetCore.Identity 自动装配。
- Ids4Autowire:IdentityServer4 自动装配。
- 其它......
- IServiceProviderAutowire:IServiceProvider 自动装配,服务配置已完成,在
UseShashlik
时装配。 - IApplicationStartAutowire:Host 启动时装配。
- IApplicationStopAutowire:Host 停止时装配。
- IServiceAutoWire:服务自动装配,在服务注册阶段,在 AddShashlik 的
简单的自定义扩展都可以使用以下两个扩展方法:
- 基于 IKernelServices 的自动装配,主要用于服务注册阶段的装配。
public static IKernelServices Autowire<T>(this IKernelServices kernelServices, Action<T> autowireAction) where T : IAutowire
- 基于 IKernelServiceProvider 的自动装配,主要用于应用配置阶段的装配。
public static IKernelServiceProvider Autowire<T>(this IKernelServiceProvider kernelServiceProvider, Action<T> autowireAction) where T : IAutowire
还不满足你的需求?你可以使用最基础的IAutowireProvider<T>
接口来完成你的装配任务。例:
public class ApplicationLifetimeHostedService : IHostedService
{
public ApplicationLifetimeHostedService(
IAutowireProvider<IApplicationStartAutowire> applicationStartAutowireProvider,
IAutowireProvider<IApplicationStopAutowire> applicationStopAutowireProvider,
IKernelServices kernelServices,
IServiceProvider serviceProvider)
{
ApplicationStartAutowireProvider = applicationStartAutowireProvider;
ApplicationStopAutowireProvider = applicationStopAutowireProvider;
KernelServices = kernelServices;
ServiceProvider = serviceProvider;
}
private IServiceProvider ServiceProvider { get; }
private IAutowireProvider<IApplicationStartAutowire> ApplicationStartAutowireProvider { get; }
private IAutowireProvider<IApplicationStopAutowire> ApplicationStopAutowireProvider { get; }
private IKernelServices KernelServices { get; }
public Task StartAsync(CancellationToken cancellationToken)
{
// 自定义装配IApplicationStartAutowire
var dic = ApplicationStartAutowireProvider.Load(KernelServices, ServiceProvider);
ApplicationStartAutowireProvider.Autowire(dic,
async r => await r.ServiceInstance.OnStart(cancellationToken));
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
// 自定义装配IApplicationStartAutowire
var dic = ApplicationStopAutowireProvider.Load(KernelServices, ServiceProvider);
ApplicationStopAutowireProvider.Autowire(dic,
async r => await r.ServiceInstance.OnStop(cancellationToken));
return Task.CompletedTask;
}
}
这里以 Shashlik.Cap 对 CAP 框架的集成来说吧,集成 CAP 需要配置消息存储和消息中间件等配置,常规用法我们需要再 ConfigureServices 中配置,如下:
service.AddCap(options => {
options.Version = "Default";
options.UseMySql("...");
options.RabbitMQ("...");
});
Shashlik.Cap 是不存在对 MySql 和 RabbitMQ 引用的,最后用户会用什么技术是未知的,那么如何将这些配置下放呢?这里就可以使用自动装配。
- 定义装配接口
ICapAutowire
。
/// <summary>
/// cap 自动装配,主要是配置cap
/// </summary>
public interface ICapAutowire : IAutowire
{
void Configure(CapOptions capOptions);
}
- 在 CAP 的 service 装配类中完成
ICapAutowire
的装配。
/// <summary>
/// event bus自动装配,装配顺序600
/// </summary>
[Order(600)]
public class CapAutowire : IServiceAutowire
{
public CapAutowire(IOptions<ShashlikCapOptions> capOptions)
{
CapOptions = capOptions.Value;
}
private ShashlikCapOptions CapOptions { get; }
public void Configure(IKernelServices kernelService)
{
if (!CapOptions.Enable)
return;
kernelService.Services.TryAddSingleton<IEventPublisher, DefaultEventPublisher>();
kernelService.Services.TryAddSingleton<INameRuler, DefaultNameRuler>();
kernelService.Services.AddCap(r =>
{
CapOptions.CopyTo(r, true);
// 装配ICapAutowire
kernelService.Autowire<ICapAutowire>(a => a.Configure(r));
});
var exists = kernelService.Services.FirstOrDefault(r => r.ServiceType == typeof(IConsumerServiceSelector));
if (exists != null)
kernelService.Services.Remove(exists);
kernelService.Services.Add(ServiceDescriptor.Describe(typeof(IConsumerServiceSelector),
typeof(CapConsumerServiceSelector), ServiceLifetime.Singleton));
}
}
重点就是kernelService.Autowire<ICapAutowire>(a => a.Configure(r));
这句代码,将自动装载所有的ICapAutowire
实现类并调用Configure
配置方法。
So easy,你也可以根据你的场景定制你的自动装配。
也许你已经注意到了Order
特性了,这个特性就是用来处理装配顺序的,对应的还有AfterAt
、BeforeAt
。一个类型不要实现多个装配接口,这会影响装配逻辑。
举个例来说,我们设计一个 Redis 缓存库,比如在 Redis 程序集配置完成,也就是RedisAutowire
执行完成后,否则就可能因为 Redis 未完成初始化而产生错误。
[AfterAt(typeof(RedisAutowire))]
pubic class RedisCacheAutowire : IServiceAutowire
{
// 配置代码
}
-
Order
:按照序号进行装配,从小达到排序。 -
AfterAt
:在指定的装配类型之后,优先级比Order
高。 -
BeforeAt
:在指定的装配类型之前,优先级比Order
高。
一个应用中通常都需要用到锁的场景,Shashlik提供通用的全局锁接口抽象,定义如下:
/// <summary>
/// 抽象的分布式锁,kernel内部内存锁实现
/// </summary>
[Singleton]
public interface ILock
{
/// <summary>
/// 开始锁定,如果在<paramref name="waitTimeoutSeconds"/>时间内未获取到锁,将抛出锁失败异常(<see cref="Shashlik.Kernel.Exceptions.LockFailureException"/>)
/// </summary>
/// <param name="key">锁key</param>
/// <param name="lockSeconds">锁定时长, 秒</param>
/// <param name="autoDelay">是否自动延期</param>
/// <param name="waitTimeoutSeconds">等待锁的超时时间, 秒</param>
/// <returns>锁释放实例</returns>
/// <exception cref="Shashlik.Kernel.Exceptions.LockFailureException">锁失败异常</exception>
IDisposable Lock(string key, int lockSeconds, bool autoDelay = true, int waitTimeoutSeconds = 60);
}
Shashlik.Kernel内部已有基于内存的实现MemoryLock
,同时Shashlik.Redis有Redis版本的实现,可以实现分布式锁。当然你可以自行实现该接口,然后什么都不用干,就会自动注册。