如何在 Spring4D 中实现作用域内共享并自动释放的 Context?
对于示例中一次创建 TTest,并让 TTest 与 TSub 共用 IContext 的需求,可以使用 PerResolve。如果同一业务作用域内多次调用工厂都要共享 Context,就需要额外管理作用域状态。释放一个 Scope 引用后,相关对象是否销毁,还取决于是否存在其他接口引用。
一次 Resolve 内共享:使用 PerResolve
PerResolve 的共享范围是一次根解析建立的依赖图。创建 ITest 时,直接注入 TTest 的 IContext 和通过 ISub 注入 TSub 的 IContext 可以是同一个实例。相关行为可参见 Spring4D 对该问题的说明。
假设相关接口、实现类及构造函数已定义,注册方式如下:
Container.RegisterType<IContext, TContext>.PerResolve;
Container.RegisterType<ITest, TTest>.AsTransient;
Container.RegisterType<ISub, TSub>.AsTransient;
Container.Build;
调用时,用一个接口变量持有根对象:
var
Test: ITest;
begin
Test := Container.Resolve<ITest>();
try
Test.Execute;
finally
Test := nil;
end;
end;
构造函数需要将依赖保存到接口字段中。例如,TSub 保存 FContext: IContext,TTest 保存 FContext: IContext 和 FSub: ISub。根对象释放后,其依赖链持有的接口引用也会随之释放。
这段代码没有引入显式 Scope,适合一次操作对应一个完整对象图的情况。
使用工厂时,区分两种共享边界
假设传入的 Func<ITest> 每次都调用 Container.Resolve<ITest>(),每次调用都会开始一次新的根解析。两次独立解析不会因为 PerResolve 自动共享同一个 Context。
下面两种需求的处理方式不同:
- 一次工厂调用创建的
TTest和TSub共享 Context:可以使用PerResolve。 - 同一次业务操作中,多次调用工厂得到的对象都共享 Context:需要由该操作持有一个 Context,并让工厂使用它。
如果业务代码只需调用一次工厂,可以直接接收并调用它,无须为了使用 PerResolve 再创建 Scope:
procedure DoInScope(TestFactory: Func<ITest>);
var
Test: ITest;
begin
Test := TestFactory();
try
Test.Execute;
finally
Test := nil;
end;
end;
这里沿用了原问题的过程名称,过程本身没有创建容器作用域。对象是否共享 Context,取决于工厂如何构造它们。
多次工厂调用共享:让工厂持有 Context
需要显式业务作用域时,可以在装配对象的地方创建一个 Context,再通过闭包将它交给工厂。下面的实现沿用题目中的 Func<ITest>,不依赖自定义 Spring4D 生命周期扩展:
function CreateScopedTestFactory: Func<ITest>;
var
Context: IContext;
begin
Context := TContext.Create;
Result :=
function: ITest
var
Sub: ISub;
begin
Sub := TSub.Create(Context);
Result := TTest.Create(Context, Sub);
end;
end;
procedure RunOperation;
var
TestFactory: Func<ITest>;
begin
TestFactory := CreateScopedTestFactory;
try
DoInScope(TestFactory);
finally
TestFactory := nil;
end;
end;
在这个示例中,RunOperation 管理作用域边界。同一个工厂捕获同一个 Context。在工厂有效期间,多次调用都会复用该实例,并分别创建新的 TTest 和 TSub。
业务过程 DoInScope 只接收工厂,无须访问容器。直接调用构造函数的代码集中在对象装配处。示例假设 TContext.Create 不需要额外参数;如果实际类还有依赖,应在这里通过相应构造函数或独立的创建工厂传入。
这是应用层实现的作用域,不会为 Container.Resolve 增加透明的作用域支持。示例在创建工厂时就创建 Context,不会等到首次使用时才创建。
为什么原来的 DelegateTo 会递归
在 IContext 的创建委托中再次执行:
Container.Resolve<IContext>
会重新进入同一个注册的创建路径,无法借此获得尚未创建的“原始实例”。
如果继续使用作用域缓存,缓存未命中时应通过独立的创建路径生成 Context,例如调用专门的工厂。这个创建路径不能再次解析同一个 scoped 注册;Context 的其他依赖也不能形成循环依赖。
委托还必须明确处理没有当前作用域的情况,例如抛出异常。原代码只在 TScope.InScope 为真时设置 Result,没有处理其他情况。
作用域结束与对象销毁的注意事项
对于使用引用计数的 TInterfacedObject,对象通常在最后一个接口引用释放时销毁。要使 Context 随业务操作结束而释放,需要同时满足以下条件:
ITest、ISub和IContext没有被保存到操作之外。- 工厂闭包没有被其他变量或长期存活的对象继续持有。
- 对象之间没有形成接口强引用环。
原例中的 Scope := nil 只释放该变量持有的引用。如果 test 仍持有 Context,Context 就会继续存活。不要为了立即销毁对象而对仍有接口引用的对象直接调用 Free。
如果必须保留 TScope.New 加任意 Container.Resolve 的调用形式,就需要实现当前作用域查找、实例缓存和退出清理,并接入所用 Spring4D 版本的扩展接口。还应明确嵌套作用域、异常退出以及跨线程调用的规则。题目没有提供版本,不能把示例中的 AsScoped 和 TScope 直接当作已确认可用的内置 API。
备注:内容仅供参考。
