.NET 中依赖注入组件 Autofac 的性能漫聊

Autofac 是一款超赞的 .NET IoC 容器 ,在众多性能测评中,它也是表现最优秀的一个。 它管理类之间的依赖关系, 从而使 应用在规模及复杂性增长的情况下依然可以轻易地修改。它的实现方式是将常规的.net类当做 组件 处理。

简单的性能测试

在 LINQPad 中,我们可以很容易的构建出一个测试环境(需要引入 Microsoft.Extensions.DependencyInjection 和 Autofac.Extensions.DependencyInjection 组件):

void Main()
{
	var services = new ServiceCollection();
	services.AddSingleton<ClassSingleton>();
	services.AddTransient<ClassTransient>();
	services.AddScoped<ClassScoped>();


	var builder = new Autofac.ContainerBuilder();
	builder.Populate(services);
	var provider = new AutofacServiceProvider(builder.Build());
	
	var singleton = provider.GetService<ClassSingleton>();
	singleton.Dump();
}

// You can define other methods, fields, classes and namespaces here
class ClassSingleton
{
	public string Name =>this.GetType().Name;
}
class ClassTransient
{
	public string Name => this.GetType().Name;
}
class ClassScoped
{
	public string Name => this.GetType().Name;
}

写一些简单的性能进行测试代码:

private static void TestSingleton(IServiceProvider provider, int times)
{
	for (int i = 0; i < times; i++)
	{
		var _ = provider.GetRequiredService<ClassSingleton>();
	}
}
private static void TestTransient(IServiceProvider provider, int times)
{
	for (int i = 0; i < times; i++)
	{
		var _ = provider.GetRequiredService<ClassTransient>();
	}
}
private static void TestScoped(IServiceProvider provider, int times)
{
	using (var scope = provider.CreateScope())
	{
		for (int i = 0; i < times; i++)
		{
			var _ = scope.ServiceProvider.GetRequiredService<ClassScoped>();
		}
	}
}

在 LINQPad 中对上述代码进行一万次、十万次、百万次三个量级的测试,得出以下报表(纵轴单位为“毫秒”):

从统计图中可以看到,即便是最耗时的 Transient 对象,百万级别创建的时间消耗也不到 400 毫秒。这说明,大多数情况下 Autofac 之类的 IoC 容器不会成为应用的性能瓶颈。

构造函数爆炸

当一个系统不断完善,业务在底层会被不断拆分为小的 Service ,然后在顶层(应用层或表现层)组装以完成功能。这表示在 Controller 中我们需要注入大量的 Service 才能保证功能完备。如果我们需要的对象通过构造函数注入,那么就会造成该构造函数的参数多到爆炸。

nopCommerce 是一个 ASP.NET 开发的电子商城系统,具备商城该有的各种功能和特性。在 ShoppingCartController 中你可以看到以下代码:

public ShoppingCartController(CaptchaSettings captchaSettings,
    CustomerSettings customerSettings,
    ICheckoutAttributeParser checkoutAttributeParser,
    ICheckoutAttributeService checkoutAttributeService,
    ICurrencyService currencyService,
    ICustomerActivityService customerActivityService,
    ICustomerService customerService,
    IDiscountService discountService,
    IDownloadService downloadService,
    IGenericAttributeService genericAttributeService,
    IGiftCardService giftCardService,
    ILocalizationService localizationService,
    INopFileProvider fileProvider,
    INotificationService notificationService,
    IPermissionService permissionService,
    IPictureService pictureService,
    IPriceFormatter priceFormatter,
    IProductAttributeParser productAttributeParser,
    IProductAttributeService productAttributeService,
    IProductService productService,
    IShippingService shippingService,
    IShoppingCartModelFactory shoppingCartModelFactory,
    IShoppingCartService shoppingCartService,
    IStaticCacheManager staticCacheManager,
    IStoreContext storeContext,
    ITaxService taxService,
    IUrlRecordService urlRecordService,
    IWebHelper webHelper,
    IWorkContext workContext,
    IWorkflowMessageService workflowMessageService,
    MediaSettings mediaSettings,
    OrderSettings orderSettings,
    ShoppingCartSettings shoppingCartSettings)
{
    ...
}

构造函数爆炸的性能问题

即便参数再多,在感官上也只是一个强迫症的问题。但构造函数爆炸所造成的影响不仅仅只是看上去没那么舒服而已。当我们注入一个对象时,IoC 容器会保证该对象以及其依赖的对象已经被正确初始化。所以我们不能简单的根据注入对象的数量来判断性能消耗,因为很有可能某个接口的实现依赖了数个其他对象。

当我们访问一个网页时,只会用到 Controller 中的某一个方法,通常,该方法不会对所有注入的对象都产生依赖。这也意味着我们创建了大量非必要的对象,为内存和 GC 造成了压力。

在 ASP.NET Core 中解决构造函数爆炸问题

ASP.NET Core 提供了一个名为 FromServicesAttribute 的属性来帮助解决必须在构造函数中注入依赖的问题。我们可以为 Action 的参数增加此属性,将所需的依赖注入进来:

public class ShoppingCartController : BasePublicController
{
      public IActionResult Notify([FromServices] INotificationService notificationService)
      {
            notificationService.Notify("...");
      }
}

这当然解决了构造函数爆炸的问题,很好。但同时,该方案也让方法的参数变得复杂也为单元测试留下了障碍,依旧不够完美。

使用 IServiceProvider 解决构造函数爆炸问题

在依赖注入容器中包含的所有对象都可以通过 IServiceProvider 获取到。基于该特性可以实现依赖对象的按需加载功能:

void Main()
{
	var services = new ServiceCollection();
	services.AddTransient<MyController>();
	services.AddTransient<MyService>();

	var builder = new Autofac.ContainerBuilder();
	builder.Populate(services);

	var provider = new AutofacServiceProvider(builder.Build());
	var controller = provider.GetRequiredService<MyController>();
	Console.WriteLine("NoCallMethod");
	controller.NoCallMethod();
	Console.WriteLine("CallMethod");
	controller.CallMethod();
}

// You can define other methods, fields, classes and namespaces here
class MyService
{
	public MyService()
	{
		Console.WriteLine("MyService 被创建");
	}
	public void SayHello()
	{
		Console.WriteLine("Hello");
	}
}
class MyController
{
	private IServiceProvider _serviceProvider;
	public MyController(IServiceProvider serviceProvider)
	{
		_serviceProvider = serviceProvider;
	}
	protected T LazyGetRequiredService<T>(ref T value)
	{
		if (value != null)
		{
			return value;
		}
		return value = _serviceProvider.GetRequiredService<T>();
	}
	private MyService _myService;
	public MyService MyService => LazyGetRequiredService(ref _myService);
	public void CallMethod()
	{
		MyService.SayHello();
	}
	public void NoCallMethod()
	{

	}
}

以上代码在 MyService 的构造函数中输出了创建日志。MyController 类型中通过 LazyGetRequiredService 方法实现了 MyService 的按需加载,构造函数也只剩下一个 IServiceProvider 对象。以上代码会产生下面的输出:

NoCallMethod
CallMethod
MyService 被创建
Hello
End

可以看到,在调用不依赖 MyService 的方法 NoCallMethod 时,MyService 并没有被创建。直到 CallMethod 被调用后用到了 MyService 时,它才被创建。

向 Volo.Abp 学习

Volo.Abp 在 4.2.0 版本中加入了一个新的接口: IAbpLazyServiceProvider 。

using System;

namespace Volo.Abp.DependencyInjection
{
    public interface IAbpLazyServiceProvider
    {
        T LazyGetRequiredService<T>();

        object LazyGetRequiredService(Type serviceType);

        T LazyGetService<T>();

        object LazyGetService(Type serviceType);

        T LazyGetService<T>(T defaultValue);

        object LazyGetService(Type serviceType, object defaultValue);

        object LazyGetService(Type serviceType, Func<IServiceProvider, object> factory);

        T LazyGetService<T>(Func<IServiceProvider, object> factory);
    }
}

其实现同样采用 IServiceProvider 创建对象,同时使用了字典来保存对实例的引用。如果你和我一样使用 Abp 开发代码,那么 LazyServiceProvider 值得尝试。

posted @ 2021-01-29 09:29  Soar、毅  阅读(4024)  评论(24编辑  收藏  举报