关于 Abp 替换了 DryIoc 框架之后的问题
在之前有些过一篇文章 《使用 DryIoc 替换 Abp 的 DI 框架》 ,在该文章里面我尝试通过以替换 IocManager
内部的 IContainer
来实现使用我们自己的 DI 框架。替换了之后我们基本上是可以正常使用了,不过仍然还存在有以下两个比较显著的问题。
- 拦截器功能无法正常使用,需要重复递归查找真实类型,消耗性能。
- 针对于通过
IServiceCollection.AddScoped()
方法添加的 Scoped 类型的解析存在问题。
下面我们就来针对于上述问题进行问题的分析与解决。
1. 问题 1
1.1 现象与原因
首先,来看一下问题 1 ,针对于问题 1 我在 Github 上面向作者请教了一下,造成嵌套注册的原因很简单。因为之所以我们解析的时候,原来的注册类型会解析出来代理类。
关于上述原因可以参考 DryIoc 的 Github 问题 #50 。
这是因为 DryIoc 是通过替换了原有注册类型的实现,而如果按照之前我们那篇文章的方法,每次注册事件被触发的时候就会针对注册类型嵌套一层代理类。这样如果某个类型有多个拦截器,这样就会造成一个类型嵌套的问题,在外层的拦截器被拦截到的时候无法获取到当前代理的真实类型。
1.2 思路与解决方法
解决思路也比较简单,就是我们在注册某个类型的时候,触发了拦截器注入事件。在这个时候,我们并不真正的执行代理类的一个操作。而是将需要代理的类型与它的拦截器类型通过字典存储起来,然后在类型完全注册完成之后,通过遍历这个字典,我们来一次性地为每一个注册类型进行拦截器代理。
思路清晰了,那么我们就可以编写代码来进行实现了,首先我们先为 IocManager 增加一个内部的字典,用于存储注册类-拦截器。
public class IocManager : IIocManager
{
// ... 其他代码
private readonly List<IConventionalDependencyRegistrar> _conventionalRegistrars;
private readonly ConcurrentDictionary<Type, List<Type>> _waitRegisterInterceptor;
// ... 其他代码
public IocManager()
{
_conventionalRegistrars = new List<IConventionalDependencyRegistrar>();
_waitRegisterInterceptor = new ConcurrentDictionary<Type, List<Type>>();
}
// ... 其他代码
}
之后我们需要开放两个方法用于为指定的注册类型添加对应的拦截器,而不是在类型注册事件被触发的时候直接生成代理类。
public interface IIocRegistrar
{
// ... 其他代码
/// <summary>
/// 为指定的类型添加拦截器
/// </summary>
/// <typeparam name="TService">注册类型</typeparam>
/// <typeparam name="TInterceptor">拦截器类型</typeparam>
void AddInterceptor<TService, TInterceptor>() where TInterceptor : IInterceptor;
/// <summary>
/// 为指定的类型添加拦截器
/// </summary>
/// <param name="serviceType">注册类型</param>
/// <param name="interceptor">拦截器类型</param>
void AddInterceptor(Type serviceType,Type interceptor);
// ... 其他代码
}
public class IocManager : IIocManager
{
// ... 其他代码
/// <inheritdoc />
public void AddInterceptor<TService, TInterceptor>() where TInterceptor : IInterceptor
{
AddInterceptor(typeof(TService),typeof(TInterceptor));
}
/// <inheritdoc />
public void AddInterceptor(Type serviceType, Type interceptorType)
{
if (_waitRegisterInterceptor.ContainsKey(serviceType))
{
var interceptors = _waitRegisterInterceptor[serviceType];
if (interceptors.Contains(interceptorType)) return;
_waitRegisterInterceptor[serviceType].Add(interceptorType);
}
else
{
_waitRegisterInterceptor.TryAdd(serviceType, new List<Type> {interceptorType});
}
}
// ... 其他代码
}
然后针对所有拦截器的监听事件进行替换,例如工作单元拦截器:
internal static class UnitOfWorkRegistrar
{
/// <summary>
/// 注册器初始化方法
/// </summary>
/// <param name="iocManager">IOC 管理器</param>
public static void Initialize(IIocManager iocManager)
{
// 事件监听处理
iocManager.RegisterTypeEventHandler += (manager, type, implementationType) =>
{
HandleTypesWithUnitOfWorkAttribute(iocManager,type,implementationType.GetTypeInfo());
HandleConventionalUnitOfWorkTypes(iocManager,type, implementationType.GetTypeInfo());
};
// 校验当前注册类型是否带有 UnitOfWork 特性,如果有则注入拦截器
private static void HandleTypesWithUnitOfWorkAttribute(IIocManager iocManager,Type serviceType,TypeInfo implementationType)
{
if (IsUnitOfWorkType(implementationType) || AnyMethodHasUnitOfWork(implementationType))
{
// 添加拦截器
iocManager.AddInterceptor(serviceType,typeof(UnitOfWorkInterceptor));
}
}
// 处理特定类型的工作单元拦截器
private static void HandleConventionalUnitOfWorkTypes(IIocManager iocManager,Type serviceType,TypeInfo implementationType)
{
// ... 其他代码
if (uowOptions.IsConventionalUowClass(implementationType.AsType()))
{
// 添加拦截器
iocManager.AddInterceptor(serviceType,typeof(UnitOfWorkInterceptor));
}
}
// ... 其他代码
}
}
处理完成之后,我们需要在 RegisterAssemblyByConvention()
方法的内部真正地执行拦截器与代理类的生成工作,逻辑很简单,遍历之前的 _waitRegisterInterceptor 字典,依次使用 ProxyUtils 与 DryIoc 进行代理类的生成与绑定。
public class IocManager : IIocManager
{
// ... 其他代码
/// <summary>
/// 使用已经存在的规约注册器来注册整个程序集内的所有类型。
/// </summary>
/// <param name="assembly">等待注册的程序集</param>
/// <param name="config">附加的配置项参数</param>
public void RegisterAssemblyByConvention(Assembly assembly, ConventionalRegistrationConfig config)
{
var context = new ConventionalRegistrationContext(assembly, this, config);
foreach (var registerer in _conventionalRegistrars)
{
registerer.RegisterAssembly(context);
}
if (config.InstallInstallers)
{
this.Install(assembly);
}
// 这里使用 TPL 并行库的原因是因为存在大量仓储类型与应用服务需要注册,应最大限度利用 CPU 来进行操作
Parallel.ForEach(_waitRegisterInterceptor, keyValue =>
{
var proxyBuilder = new DefaultProxyBuilder();
Type proxyType;
if (keyValue.Key.IsInterface)
proxyType = proxyBuilder.CreateInterfaceProxyTypeWithTargetInterface(keyValue.Key, ArrayTools.Empty<Type>(), ProxyGenerationOptions.Default);
else if (keyValue.Key.IsClass())
proxyType = proxyBuilder.CreateClassProxyTypeWithTarget(keyValue.Key,ArrayTools.Empty<Type>(),ProxyGenerationOptions.Default);
else
throw new ArgumentException($"类型 {keyValue.Value} 不支持进行拦截器服务集成。");
var decoratorSetup = Setup.DecoratorWith(useDecorateeReuse: true);
// 使用 ProxyBuilder 创建好的代理类替换原有类型的实现
IocContainer.Register(keyValue.Key,proxyType,
made: Made.Of(type=>type.GetConstructors().SingleOrDefault(c=>c.GetParameters().Length != 0),
Parameters.Of.Type<IInterceptor[]>(request =>
{
var objects = new List<object>();
foreach (var interceptor in keyValue.Value)
{
objects.Add(request.Container.Resolve(interceptor));
}
return objects.Cast<IInterceptor>().ToArray();
}),
PropertiesAndFields.Auto),
setup: decoratorSetup);
});
_waitRegisterInterceptor.Clear();
}
// ... 其他代码
}
这样的话,在调用控制器或者应用服务方法的时候能够正确的获取到真实的代理类型。
图:
可以看到拦截器不像原来那样是多个层级的情况,而是直接注入到代理类当中。
通过 invocation
参数,我们也可以直接获取到被代理对象的真实类型。
2. 问题 2
2.1 现象与原因
问题 2 则是由于 DryIoc 的 Adapter 针对于 Scoped 生命周期对象的处理不同而引起的,比较典型的情况就是在 Startup
类当中使用 IServiceCollection.AddDbContxt<TDbContext>()
方法注入了一个 DbContext 类型,因为其方法内部默认是使用 ServiceLifeTime.Scoped
周期来进行注入的。
public static IServiceCollection AddDbContext<TContextService, TContextImplementation>(
[NotNull] this IServiceCollection serviceCollection,
[CanBeNull] Action<DbContextOptionsBuilder> optionsAction = null,
ServiceLifetime contextLifetime = ServiceLifetime.Scoped,
ServiceLifetime optionsLifetime = ServiceLifetime.Scoped)
where TContextImplementation : DbContext, TContextService
=> AddDbContext<TContextService, TContextImplementation>(
serviceCollection,
optionsAction == null
? (Action<IServiceProvider, DbContextOptionsBuilder>)null
: (p, b) => optionsAction.Invoke(b), contextLifetime, optionsLifetime);
按照正常的逻辑,一个 Scoped 对象的生命周期应该是与一个请求一致的,当请求结束之后该对象被释放,而且在该请求的生命周期范围内,通过 Ioc 容器解析出来的 Scoped 对象应该是同一个。如果有新的请求,则会创建一个新的 Scoped 对象。
但是使用 DryIoc 替换了原有 Abp 容器之后,现在如果在一个控制器方法当中解析一个 Scoped 周期的对象,不论是几次请求获得的都是同一个对象。因为这种现象的存在,在 Abp 的 UnitOfWorkBase
当中完成一次数据库查询操作之后,会调用 DbContext
的 Dispose()
方法释放掉 DbContext
。这样的话,在第二次请求因为获取的是同一个 DbContext,这样的话就会抛出对象已经被关闭的异常信息。
除了开发人员自己注入的 Scoped 对象,在 Abp 的 Zero 模块内部重写了 Microsoft.Identity 相关组件,而这些组件也是通过 IServiceCollection.AddScoped()
方法与 IServiceCollection.TryAddScoped()
进行注入的。
public static AbpIdentityBuilder AddAbpIdentity<TTenant, TUser, TRole>(this IServiceCollection services, Action<IdentityOptions> setupAction)
where TTenant : AbpTenant<TUser>
where TRole : AbpRole<TUser>, new()
where TUser : AbpUser<TUser>
{
services.AddSingleton<IAbpZeroEntityTypes>(new AbpZeroEntityTypes
{
Tenant = typeof(TTenant),
Role = typeof(TRole),
User = typeof(TUser)
});
//AbpTenantManager
services.TryAddScoped<AbpTenantManager<TTenant, TUser>>();
//AbpEditionManager
services.TryAddScoped<AbpEditionManager>();
//AbpRoleManager
services.TryAddScoped<AbpRoleManager<TRole, TUser>>();
services.TryAddScoped(typeof(RoleManager<TRole>), provider => provider.GetService(typeof(AbpRoleManager<TRole, TUser>)));
//AbpUserManager
services.TryAddScoped<AbpUserManager<TRole, TUser>>();
services.TryAddScoped(typeof(UserManager<TUser>), provider => provider.GetService(typeof(AbpUserManager<TRole, TUser>)));
//SignInManager
services.TryAddScoped<AbpSignInManager<TTenant, TRole, TUser>>();
services.TryAddScoped(typeof(SignInManager<TUser>), provider => provider.GetService(typeof(AbpSignInManager<TTenant, TRole, TUser>)));
// ... 其他注入代码
return new AbpIdentityBuilder(services.AddIdentity<TUser, TRole>(setupAction), typeof(TTenant));
}
以上代码与 DbContext 产生的异常现象一致,都会导致每次请求获取的都是同一个对象,而 Abp 在底层会在每次请求结束后进行释放,这样也会造成后续请求访问到已经被释放的对象。
上面这些仅仅是替换 DryIoc 框架后产生的异常现象,具体的原因在于 DryIoc 官方编写的 DryIoc.Microsoft.DependencyInjection 扩展。这是针对于 ASP.NET Core 自带的 DI 框架进行替换的 Adapter 适配器,大体原理就是通过实现 IServiceScopeFactory
接口与 IServiceScope
接口替换掉原有 DI 框架的实现。以实现接管容器注册与生命周期的管理。
这里的重点就是 IServiceScopeFactory
接口,通过名字我们可以得知这是一个工厂,他拥有一个 CreateScope()
方法以创建一个 Scoped 范围。在 MVC 处理请求的时候,通过 CreateScope()
方法获得一个子容器,请求结束之后调用子容器的 Dispose()
方法进行释放。
伪代码大概如下:
public void Request()
{
var factory = serviceProvider.GetService<IServiceScopeFactory>();
using(var scoped = factory.CreateScope())
{
scoped.Resove<HomeController>().Index();
scoped.Resove<TestDbContext>();
}
}
public class HomeController : Controller
{
public HomeController(TestDbContext t1)
{
// 这里的 t1 在 scoped 子容器释放之后会被释放
}
public IActionResult Index()
{
var t2 = IocManager.Instance.Resove<TestDbContext>();
}
}
可以看到它通过 using 语句块包裹了 CreateScope()
方法,在 HomeController
解析的时候,其内部的 t1 对象是通过子容器进行解析创建出来的,那么它的生命周期跟随子容器的销毁而被销毁。子容器销毁的时间则是在一次 Http 请求结束之后,那么我们每次请求的时候 t1 的值都会不一样。
而 t2 则有点特殊,因为我们重写 IocManager
类的时候就已经知道这个 Instance
是一个静态实例,而我们在这里通过 Instance 进行解析出来的对象是从这个静态实例的容器当中解析的。这个静态容器是不会随着请求的结束而被释放,因此每次请求得到的 t2 值都是一样的。
2.1 思路与解决方法
思路比较简单,只需要在 IocManager
的 Resolve()
方法进行解析的时候,通过静态容器 IContainer
同样创建一个子容器即可。
更改原来的解析方法 Resolve()
,在解析的时候通过 IocContainer
的 OpenScope()
创建一个新的子容器,然后通过这个子容器进行实例解析。下面是针对 TestApplicationService
的 GetScopedObject()
方法进行测试的结果。
子容器:
351e8576-6f70-4c9b-8cda-02d46a22455d
a4af414b-103e-4972-b7e2-8b8b067c1ce1
04bd79d5-33a2-4e2c-87ae-e72f345c4232
Ioc 静态容器:
2e5dfd1f-36d9-4d62-94cd-c6cc66e316ef
2e5dfd1f-36d9-4d62-94cd-c6cc66e316ef
2e5dfd1f-36d9-4d62-94cd-c6cc66e316ef
虽然直接通过 OpenScope()
来构建子容器是可以解决 Scope 对象每次请求都为一个对象的 BUG,但是解析出来的子容器没有调用 Dispose()
方法进行释放。
目前有一个临时的解决思路,即在 IIocManager
增加一个属性字段 ChildContainer
,用于存储每次请求创建的临时 Scope 对象,之后 IocManager 内部优先使用 ChildContainer 进行对象解析。
首先我们来到 IIocManager
接口,为其添加一个 ChildContainer 只读属性与 InitializeChildContainer()
的初始化方法。
public interface IIocManager : IIocRegistrar, IIocResolver, IDisposable
{
// ... 其他代码
/// <summary>
/// 子容器
/// </summary>
/// <remarks>本属性的值一般是由 DryIocAdapter 当中创建,而不应该在其他地方进行赋值。</remarks>
IResolverContext ChildContainer { get; }
/// <summary>
/// 初始化子容器
/// </summary>
/// <param name="container">用于初始化 IocManager 内部的子容器</param>
void InitializeChildContainer(IResolverContext container);
}
在 IocManager
类型当中实现这两个新增的方法和属性,并且更改一个 Resolve()
方法的内部逻辑,优先使用子容器进行对象解析。
public class IocManager : IIocManager
{
// ... 其他代码
/// <inheritdoc />
public IResolverContext ChildContainer { get; private set; }
/// <inheritdoc />
public void InitializeChildContainer(IResolverContext container)
{
ChildContainer = container;
}
/// <summary>
/// 从 Ioc 容器当中获取一个对象
/// 返回的对象必须通过 (see <see cref="IIocResolver.Release"/>) 进行释放。
/// </summary>
/// <typeparam name="T">需要解析的目标类型</typeparam>
/// <returns>解析出来的实例对象</returns>
public T Resolve<T>()
{
if (ChildContainer == null) return IocContainer.Resolve<T>();
if (!ChildContainer.IsDisposed) return ChildContainer.Resolve<T>();
return IocContainer.Resolve<T>();
}
// ... 其他代码
}
这里仅更改了其中一个解析方法作为示范,如果正式使用的时候,请将 IocManager
的所有 Resolve()
实现都进行相应的更改。
效果图:
因为是同一个请求,所以 Scope 生命周期的对象在这个请求的生存周期内应该解析的都是同一个对象。下面是第二次请求时的情况:
可以看到,第二次请求的时候解析出来的 ScopeClass
类型实例都是同一个对象,其 Guid 值都变成 abd004e0-3792-4e6d-85b3-e721d8dde009
。
3. 演示项目的 GitHub 地址
https://github.com/GameBelial/Abp-DryIoc
关于 Abp 替换了 DryIoc 框架之后的问题的更多相关文章
- abp vNext微服务框架分析
本文转载自:https://www.cnblogs.com/william-xu/p/11245738.html abp vNext新框架的热度一直都很高,于是最近上手将vNext的微服务Demo做了 ...
- abp.zero 9.0框架的前端Angular使用说明
abp.zero 9.0框架的前端Angular使用说明 目录 abp.zero 9.0框架的前端Angular使用说明 摘要 1 部署及启动 1.1 依赖包安装 1.2 使用yarn安装依赖包 1. ...
- 使用 DryIoc 替换 Abp 的 DI 框架
一.背景 你说我 Castle Windsor 库用得好好的,为啥要大费周章的替换成 DryIoc 库呢?那就是性能,DryIoc 是一款优秀而且轻量级的 DI 框架,整个项目代码就两个文件,加起来代 ...
- ABP框架 - 启动配置
文档目录 本节内容: 配置ABP 替换内置服务 配置模块 为一个模块创建配置 ABP在启动时,提供基础框架和模型来配置和模块化. 置ABP 在预初始化事件中进行配置,示例: kid1412注:XmlL ...
- ABP使用及框架解析系列 - [Unit of Work part.2-框架实现]
前言 ABP ABP是“ASP.NET Boilerplate Project”的简称. ABP的官方网站:http://www.aspnetboilerplate.com ABP在Github上的开 ...
- 实战框架ABP
abp及实战框架概述 接触abp也快一年了,有过大半年的abp项目开发经验,目前项目中所用的abp框架版本为0.10.3,最新的abp框架已经到了1.4,并且支持了asp.net core.关于abp ...
- ABP框架 - 多层结构
文档目录 本节内容: 简介 ABP结构 多层 其它层(通用) 领域(Core)层 应用层 基础层 Web & 表示层 其它 总结 简介 一个应用的代码库的分层是一个广为接受的技术,用来减少复杂 ...
- ABP框架 - 功能管理
文档目录 本节内容: 简介 关于 IFeatureValueStore 功能类型 Boolean 功能 Value 功能 定义功能 基本功能属性 其它功能属性 功能层次 检查功能 使用Requires ...
- 手工搭建基于ABP的框架(2) - 访问数据库
为了防止不提供原网址的转载,特在这里加上原文链接: http://www.cnblogs.com/skabyy/p/7517397.html 本篇我们实现数据库的访问.我们将实现两种数据库访问方法来访 ...
随机推荐
- LoadRunner录制脚本时没有响应——无法启动浏览器问题总结
1.ie浏览器去掉启用第三方浏览器扩展 2.loadrunner11 键盘F4,在browser Emulation点击change,在弹出的提示框中Browser version 选择8.0,pla ...
- js几种数组遍历方法.
第一种:普通的for循环 ; i < arr.length; i++) { } 这是最简单的一种遍历方法,也是使用的最多的一种,但是还能优化. 第二种:优化版for循环 ,len=arr.len ...
- 实验十五 GUI编程练习与应用程序部署
实验十五 GUI编程练习与应用程序部署 实验时间 2018-12-6 一:理论部分 1.Java 程序的打包:编译完成后,程序员将.class 文件压缩打包为 .jar 文件后,GUI 界面序就可以 ...
- sublime text2 安装及使用教程
1.下载安装包地址:https://www.sublimetext.com/2 2.安装,一直点下一步就好,将下列选项打钩,这样文件右键就可以直接用sublime text2打开 3.新建一个html ...
- 高速上手C++11 14 笔记2
lambda表达式和std function bind 两者配合构成了函数新的使用方法. 智能指针 sharedptr, uniqueptr, weak_ptr auto pointer = std: ...
- VUE实现登录然后跳转到原来的页面
可以在路由里面设置需要登录的界面,判断下没有登录就跳转到登录界面,登录了就不用登录,这里用的是一个存储的 router.beforeEach((to, from, next) => { if(t ...
- Java 包与类的命名(util、service、tool、dao )区别
util 通用的.与业务无关的,可以独立出来,可供其他项目使用.方法通常是public static,一般无类的属性,如果有,也是public static. service 与某一个业务有关,不是通 ...
- oracle RAC
RAC安装步骤 1 配置共享存储 2 Grid Infrastructure软件的安装,GI主要用于cluster ,storage的管理 3 安装数据库软件 ...
- C语言 字符二维数组(多个字符串)探讨 求解
什么是二维字符数组? 二维字符数组中为什么定义字符串是一行一个? “hello world”在C语言中代表什么? 为什么只能在定义时才能写成char a[10]="jvssj" ...
- 不停止nginx服务,使配置文件生效
ps -ef | grep "nginx: master process" | grep -v "grep" | awk -F ' ' '{print $2}' ...