1. 程式人生 > >.NET 中依賴注入元件 Autofac 的效能漫聊

.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 值得嘗試。