控制反转(Inversion of Control,IoC)是一种设计原则,它把创建和管理类对象的控制权从调用者转移到框架容器中。事先在框架容器中注册各种类对象,由框架容器统一负责类对象的创建、装配和管理;调用者无须显式创建类对象,只需声明使用即可。在这里,类对象就是服务。IoC进一步实现了各模块之间的解耦,充分体现了技术人在“高内聚、低耦合”设计方向上的不懈追求!
依赖注入(Dependency Injection,DI)是实现IoC的最流行方式,被各大技术框架所采用。
本文详细介绍.NET框架的DI用法,并给出一个Spring框架的DI示例用以对比。
.NET中的依赖注入
使用流程
安装NuGet包:Microsoft.Extensions.DependencyInjection
定义服务,一般包括接口和实现接口的具体类,具体类可能会依赖其它的服务(有其它服务类型的成员变量,即关联关系)
日志服务:定义日志服务接口并实现一个控制台日志服务
1 2 3 4 5 6 7 8 9 10 11 12 public interface ILogService { public void LogError (string message ) ; public void LogInfo (string message ) ; } public class ConsoleLogService : ILogService { public void LogError (string message ) { Console.WriteLine($"Error: {message} " ); } public void LogInfo (string message ) { Console.WriteLine($"Info: {message} " ); } }
配置服务:定义配置服务接口并实现一个环境变量配置服务
1 2 3 4 5 6 7 8 public interface IConfigService { public string ? GetValue(string key); } public class EnvVarConfigService : IConfigService { public string ? GetValue(string key) { return Environment.GetEnvironmentVariable(key); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public interface IMailService { public void Send (string title, string to, string body ) ; } public class MailService : IMailService { private readonly ILogService _logProvider; private readonly IConfigReader _configService; public MailService (ILogService logProvider, IConfigReader configService ) { _logProvider = logProvider; _configService = configService; } public void Send (string title, string to, string body ) { _logProvider.LogInfo("开始读取配置信息:" ); var port = _configService.GetValue("port" ); if (port == null ) _logProvider.LogError("读取端口信息失败!" ); else _logProvider.LogInfo($"Port: {port} " ); _logProvider.LogInfo("读取配置信息完成!" ); _logProvider.LogInfo("开始发送邮件:" ); Console.WriteLine($"发邮件了:{title} ,{to} ,{body} " ); _logProvider.LogInfo("发送邮件完成!" ); } }
使用邮件服务:先注册服务,再定位并使用服务,在实际项目中,服务的注册和使用可以分离
1 2 3 4 5 6 7 8 9 10 11 12 13 static void Main (string [] args ) { var services = new ServiceCollection(); services.AddScoped<ILogService, ConsoleLogService>(); services.AddScoped<IConfigService, EnvVarConfigService>(); services.AddScoped<IMailService, MailService>(); using (var sp = services.BuildServiceProvider()) { var mailService = sp.GetRequiredService<IMailService>(); mailService.Send("第一封邮件" , "LiRuohan" , "你好,依赖注入!" ); } Console.ReadKey(); }
如果采用传统方式实现邮件服务,我们需要手动创建MailService
的对象,并且在创建之前需要创建ConsoleLogService
和EnvVarConfigService
的对象:
1 2 3 4 5 6 7 static void Main (string [] args ) { var logService=new ConsoleLogService(); var configService=new EnvVarConfigService(); var mailService=new MailService(logService,configService); mailService.Send("第一封邮件" , "LiRuohan" , "你好,依赖注入!" ); Console.ReadKey(); }
采用这种方式,一旦需要使用邮件服务,就必须手动创建日志服务对象和配置服务对象,自然而然需要知道三种服务的具体类。如果后期需要用新的服务类替换既有服务类,那么就必须修改所有手动创建服务的代码,这在大型项目中非常令人厌恶!
采用DI方式的代码中,只需要修改注册服务时的服务具体类即可,更加充分体现了“面向接口编程”的思想。
服务注册
创建一个ServiceCollection
对象,就可以向该对象中注册服务了,注册服务完成之后,调用该对象的BuildServiceProvider()
就可以获得ServiceProvider
对象,后续这个对象进行服务定位。
三种生命周期的服务:
AddSingleton()
:
单例模式,容器只会创建一个示例,每次请求服务时,容器都会返回该实例。
适用于无状态服务,例如上面的日志服务和配置服务。
AddScoped()
:
作用域模式,在同一个作用域内容器会返回相同的实例,在不同的作用域内会重新创建新的实例。
作用域理解:一般而言,每次请求服务器时都会创建一个新的作用域,对于非Web应用而言,默认处于一个相同的作用域中,该作用域不同于语法作用域。
适用于有状态服务。
AddTransient()
:
瞬态模式,每次请求服务时,容器都会创建一个新的实例。
慎用,对性能可能造成严重影响,需要评估创建新实例的代价。
常见的服务注册语法:
1 2 3 4 5 6 7 8 9 services.AddSingleton<ILogService, ConsoleLogService>(); services.AddSingleton<IConfigService, EnvVarConfigService>(); services.AddScoped<MailService>(); using (var sp = services.BuildServiceProvider()) { var mailService = sp.GetRequiredService<MailService>(); mailService.Send("第一封邮件" , "LiRuohan" , "你好,依赖注入!" ); }
类型参数可以用泛型的方法表达,也可以用类型参数的方式表达,一般用前者即可
1 2 3 4 services.AddSingleton<ILogService, ConsoleLogService>(); services.AddSingleton(typeof (ILogService), typeof (ConsoleLogService));
如果在构造的时候需要给属性赋值,那么可以用Lambda表达式传参
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class JsonConfigService : IConfigService { public string JsonPath { get ; set ; } public string ? GetValue(string key) { ConfigurationBuilder configBuilder = new ConfigurationBuilder(); if (string .IsNullOrWhiteSpace(JsonPath)) return null ; configBuilder.AddJsonFile(JsonPath, optional: false , reloadOnChange: false ); IConfigurationRoot configRoot = configBuilder.Build(); return configRoot[key]; } } services.AddScoped<IConfigService>(c => new JsonConfigService() { JsonPath = "./config.json" });
如果要为同一服务注册多个实现类,可以为每个实现类指定一个key,后期用这进行服务定位
1 2 services.AddKeyedScoped<IConfigService>("json" , (p, k) => new JsonConfigService() { JsonPath = "./config.json" }); services.AddKeyedScoped<IConfigService, EnvVarConfigService>("config" );
如果针对同一接口进行了多次注册,服务定位时以最后那个为准
服务定位
通过容器创建的对象,会自动进行依赖注入,手动创建的对象则不会,这就要求显式从容器中获取根对象。
IServiceCollection
的BuildServiceProvider()
方法可以返回ServiceProvider
对象,通过该对象就可以进行服务定位。
GetService<T>()
:获取服务,没有返回空
GetRequiredService<T>()
:获取服务,没有抛出异常
GetServices<>()
:获取同一接口的所有服务,返回服务数组
也可以通过给定的key名称定位服务
拓展
服务提供者提供服务注册快捷函数,避免服务注册者显式声明服务类,这在.NET框架中被大量使用,主要思路是编写IServiceCollection
的扩展方法
1 2 3 4 5 6 7 8 9 10 namespace Microsoft.Extensions.DependencyInjection { public static class ConfigExtensions { public static void AddJsonConfigService (this IServiceCollection serviceCollection,string jsonPath ) { serviceCollection.AddScoped<IConfigService>(c => new JsonConfigService() { JsonPath = jsonPath }); } public static void AddEnvVarConfigService (this IServiceCollection serviceCollection ) { serviceCollection.AddScoped<IConfigService, EnvVarConfigService>(); } } }
1 2 3 services.AddJsonConfigService("config.json" ); services.AddEnvVarConfigService();
配置覆盖机制,在实际工程中,项目配置可能会集中放置在一台服务器,但是开发过程中应该允许对默认配置进行覆盖,这时可以采用下列方式,在LayeredConfigReader
中_services
会捕捉注册的所有IConfigService
服务,在GetValue
中就可以按照顺序遍历服务获取值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public interface IConfigReader { public string ? GetValue(string key); } public class LayeredConfigReader : IConfigReader { private readonly IEnumerable<IConfigService> _services; public LayeredConfigReader (IEnumerable<IConfigService> services ) { _services = services; } public string ? GetValue(string key) { string ? value = null ; foreach (var service in _services) { string ? newValue= service.GetValue(key); if (newValue != null ) { value = newValue; } } return value ; } } services.AddScoped<IConfigService>(p => new JsonConfigService() { JsonPath = "config.json" }); services.AddScoped<IConfigService, EnvVarConfigService>(); services.AddScoped<IConfigReader, LayeredConfigReader>();
Spring中的依赖注入
当前,Web开发的框架百花齐放,但是它们的很多原理是相似的,在Spring框架中也有DI机制,且DI是Spring框架的实现基石,下面将上述.NET实例改成Spring实例。
1 2 3 4 5 6 7 8 9 10 11 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > </dependencies >
1 2 3 4 5 6 package com.example.testdi;public interface LogService { public void LogError (String message) ; public void LogInfo (String message) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.example.testdi;import org.springframework.stereotype.Service;@Service public class ConsoleLogService implements LogService { @Override public void LogError (String message) { System.out.println("Error: " + message); } @Override public void LogInfo (String message) { System.out.println("Info: " + message); } }
1 2 3 4 5 package com.example.testdi;public interface ConfigService { public String GetValue (String key) ; }
1 2 3 4 5 6 7 8 9 10 11 package com.example.testdi;import org.springframework.stereotype.Service;@Service public class EnvValConfigService implements ConfigService { @Override public String GetValue (String key) { return key + "_1" ; } }
1 2 3 4 5 package com.example.testdi;public interface MailService { public void Send (String title, String to, String body) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.example.testdi;import org.springframework.stereotype.Service;@Service public class MailServiceImpl implements MailService { private final LogService logService; private final ConfigService configService; public MailServiceImpl (LogService log, ConfigService config) { this .logService = log; this .configService = config; } @Override public void Send (String title, String to, String body) { logService.LogInfo("开始读取配置信息:" ); var port = configService.GetValue("port" ); if (port == null ) logService.LogError("读取端口信息失败!" ); else logService.LogInfo("Port: " + port); logService.LogInfo("读取配置信息完成!" ); logService.LogInfo("开始发送邮件:" ); System.out.println("发邮件了: " + title + to + body); logService.LogInfo("发送邮件完成!" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.example.testdi;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration @ComponentScan(basePackages = "com.example.testdi") public class MailSenderConsoleApp { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (MailSenderConsoleApp.class); var greeting = context.getBean(MailServiceImpl.class); greeting.Send("第一封邮件" , "LiRuohan" , "你好,依赖注入!" ); context.close(); } }
Spring中的依赖注入有两种形式:①基于XML配置文件的依赖注入;②基于注解的依赖注入。后者更为简洁灵活,是最常用的方式。
Spring依赖注入的服务注册和服务定位与.NET有很大差异,后者通过代码集中注册;前者采用“注解+扫描”的分布式方式,更为灵活。
Autofac依赖注入库
使用流程
为方便对比,将上述.NET实例的DI库调整为Autofac,服务定义与原实例相同,变化体现在服务注册和定位上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static void Main (string [] args ) { var builder = new ContainerBuilder(); builder.RegisterType<ConsoleLogService>().As<ILogService>().InstancePerLifetimeScope(); builder.RegisterType<EnvVarConfigService>().As<IConfigService>().InstancePerLifetimeScope(); builder.Register(c => new JsonConfigService() { JsonPath = "config.json" }).As<IConfigService>().InstancePerLifetimeScope(); builder.RegisterType<MailService>().As<IMailService>().InstancePerLifetimeScope(); var container = builder.Build(); var mailService = container.Resolve<IMailService>(); mailService.Send("第一封邮件" , "LiRuohan" , "你好,依赖注入!" ); Console.ReadKey(); }
服务注册
服务的生命周期:
单例模式:builder.RegisterType<ConsoleLogService>().As<ILogService>().SingleInstance()
作用域模式:builder.RegisterType<ConsoleLogService>().As<ILogService>().InstancePerLifetimeScope()
瞬态模式:builder.RegisterType<ConsoleLogService>().As<ILogService>().InstancePerDependency()
,不指定什么周期默认就是瞬态模式
服务的注册语法:
1 2 3 4 5 6 services.AddScope<MailService>(); services.AddScope<IMailService, MailService>(); builder.RegisterType<MailService>().As<IMailService>().InstancePerLifetimeScope(); builder.RegisterType<MailService>().InstancePerLifetimeScope();
类型参数可以用泛型的方法表达,也可以用类型参数的方式表达,一般用前者即可
1 2 3 4 builder.RegisterType<MailService>().InstancePerLifetimeScope(); builder.RegisterType(typeof (MailService)).InstancePerLifetimeScope();
如果在构造的时候需要给属性赋值,那么可以用Lambda表达式传参
1 builder.Register(c => new JsonConfigService() { JsonPath = "config.json" }).As<IConfigService>().InstancePerLifetimeScope();
如果要为同一服务注册多个实现类,可以为每个实现类指定一个key,后期用这进行服务定位
1 2 3 builder.Register(c => new JsonConfigService() { JsonPath = "config.json" }).As<IConfigService>().Named<IConfigService>("json" ).InstancePerLifetimeScope(); builder.RegisterType<EnvVarConfigService>().As<IConfigService>().Named<IConfigService>("config" ).InstancePerLifetimeScope();
如果针对同一接口进行了多次注册,服务定位时以最后那个为准
服务定位
ContainerBuilder
的Build()
方法可以返回IContainer
对象,通过该对象就可以进行服务定位。
Resolve<T>()
:获取服务,没获取到服务是抛出异常
TryResolve<T>()
:获取服务,没有获取到时返回false
也可以通过给定的key名称定位服务:container.ResolveKeyed<IConfigService>("json")
属性注入
.NET的原生DI库无法实现属性注入,Autofac可以实现,即服务的一个属性也可以通过依赖注入自动赋值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public interface IUserRepository { void AddUser (string userName ) ; } public class UserRepository : IUserRepository { private readonly IDatabaseConnection _connection; public ILogger Logger { get ; set ; } public UserRepository (IDatabaseConnection connection ) { _connection = connection; } public void AddUser (string userName ) { _connection.connect(); Console.WriteLine($"User '{userName} ' added to the database." ); Logger.Log("User added." ); } } builder.RegisterType<UserRepository>().As<IUserRepository>().InstancePerLifetimeScope().PropertiesAutowired();
本实例中,UserRepository
通过构造函数注入成员变量_connection
,通过属性注入Logger
。
如果一个类中有属性需要进行注入,在注册该类时需要通过方法PropertiesAutowired()
开启属性注入。