依赖注入的理解与应用
1. 为什么要用依赖注入(DI)
什么是依赖注入,为什么要使用呢?简单通俗说就是一个类需要另一个类来协助工作,就产生了依赖,所以需要的依赖项就要【注入】过来一起来协同完成工作。
软件设计原则中有一个依赖倒置原则(DIP)讲的是要依赖于抽象,不要依赖于具体,高层模块不应该依赖于低层模块, 二者应该依赖于抽象。简单的说就是为了更好的解耦。而控制反转(Ioc)就是这样的一个实现思路, 这个思路的其中一种实现方式就是依赖注入(DI)
感觉有点绕, 举个栗子:老李是一个维修工, 现在要出任务去维修, 得先去申领个扳手。
老李: “请给我一把可以拧7mm大小的六角螺丝的扳手.”, 然后库管老张就从仓库里拿了一把这样的大力牌扳手给老李。
在这个例子中, 维修工老李只要告诉库管我要一个 "可以拧7mm大小的六角螺丝"的扳手即可, 他不用关心扳手的品牌和样式, 也不用采购扳手,更不用关心这个扳手是怎么来的。而对于库管, 他只需提供满足这样规则的一个扳手即可, 不用去关心老李拿着这个扳手之后去干什么。所以老李和老张都只是关心"可以拧7mm大小的六角螺丝的"这个规则即可, 也就是说, 如果后期仓库里不再提供大力牌扳手, 而是提供了这样的大牛牌扳手, 无论换了什么牌子和样式, 只要仍满足这个规则, 老李仍然可以正常工作。它们定义了一个规则(比如接口IWrench7mm), 二者都依赖于这个规则, 然后仓库无论提供大力牌(WrenchDaLi : IWrench7mm)还是大牛牌(WrenchDaNiu : IWrench7mm), 都不影响正常工作.
这就是依赖倒置原则(DIP), 不依赖于具体(牌子), 高层模块(老李)不应该依赖于低层模块(大力牌扳手), 二者应该依赖于抽象(IWrench7mm:可以拧7mm大小的六角螺丝)。如果直接由老李去获取(new)大力牌扳手, 那么当业务改变要求采用大牛牌的时候, 我们就要去修改老李的代码。为了解耦, 在本例中我们只要在配置中让仓库由原来的提供大力牌改为提供大牛牌即可。老李要使用的时候, 可以通过注入(构造器、属性、方法)的方式, 将仓库提供的扳手实例提供给老李使用。
2. 依赖注入理解
引入依赖注入的目的是为了解耦。说白了就是面向接口编程,通过调用接口的方法,而不直接实例化对象去调用。
这样做的好处就是如果添加了另一个种实现类,不需要修改之前代码,只需要修改注入的地方将实现类替换。上面说的通过接口调用方法,实际上还是需要去实例化接口的实现类,只不过不需要我们手动new 构造实现类,而是交给如微软的DI、Autofac这些工具去构建实现类。我们只需要告诉它们,某个类是某个接口的实现类,当用到的时候,工具(比如,微软的DI)会自动通过构造函数实例化类。
3. 依赖的服务如何注入
打开Startup这个文件, 看一下里面的ConfigureServices方法。顾名思义, 这个方法是用来配置服务,系统默认已经添加了一些服务, 剩下的就是我们把自己需要的用的添加进去。参数为服务集合IServiceCollection对象,这种对象提供了AddSingleton、AddScoped和AddTransient 三种方法来添加服务,三种方法添加的服务的生命周期不一样。
实例:
添加一个名为DIDemo的.NET CORE MVC项目,在该项目下创建一个服务文件夹(Servers)
(1)定义接口ICount
(2)实现接口类Count
至此,服务(类)有了,那么如何能让这个服务为我们所用呢?或者说为我们服务呢?
(3)把类(服务)在Startup文件中通过ConfigureServices方法注入服务。
.NET Core 中自带的DI容器,可以理解为StartUp.CS文件。——不准确
使用容器的好处,由容器来统一管理实例的创建和销毁,你只需要关心怎么用就行了,不需要关系怎么创建跟销毁。
当然容器创建的实例都是有生命周期的。三种创建方法创建的实例生命周期不一样。
- Transient: 瞬态模式,每一次访问都会创建一个新的实例
- Scoped: 域模式,在同一个Scope内只初始化一个实例 ,可以理解为( 每一个request级别只创建一个实例,同一个http
request会在一个 scope内)。对象在一次请求中是相同的,但在不同请求中是不同的。 - Singleton :单例模式,整个应用程序生命周期以内只创建一个实例 此外,常用注入方式有三种。
public void ConfigureServices(IServiceCollection services)
{
……
//下面先以AddScopend方法阐述下常用的三种注入方式
//1.最常用的注入方式,以接口形式暴露服务。下面2中方式意思一样
//1.1 AddScopend后面是(),里面的接口和实现类必须套一层typeof
services.AddScoped(typeof(ICount), typeof(Count));
//1.2 AddScopend后面是<>,里面就直接写接口和实现类,当然最后有一个()
services.AddScoped<ICount, Count>();
//2.自己注入自己,以实现形式暴露服务
services.AddScoped(typeof(Count));
services.AddScoped<Count>();
//3.需要传参的构造函数的类的注入(后面实例有应用讲解)
// services.AddScoped(typeof(ICount), sp => { return new Count(参数); }) ;
//services.AddScoped<ICount>(sp => { return new Count(参数);}) ;
……
}
(4)接下来分析演示三种注入方法的区别:
上面ConfigureServices方法中保留下面的瞬态模式
//第1种:瞬态模式,每一次访问都会创建一个新的实例
services.AddTransient<ICount, Count>();
服务注入之后,我们就要用它。切换到控制器。那么能把服务实例注入到控制器中来呢?有属性注入、构造方法注入、方法注入。这里一般会用构造方法注入。
public class HomeController : Controller
{
private ICount _count;//方便本类其他方法的调用,所以定义一个私有字段来接收
public HomeController(ICount count)//通过构造方法注入实例,ASP.NET CORE内置了依赖注入容器
{
_count = count;
}
//说明:请求到home控制器,自然调用home控制器的构造方法,构造方法中需要一个ICount类型的对象,它怎么来的呢?这就是因为.NET Core内置了依赖注入容器,这个时候就会到StartUp.cs文件中的ConfigureServices方法中去找相应的依赖,而在那里告诉了ICount由Count来实现( services.AddTransient<ICount, Count>();),所以这时会去调用Count 的构造方法实例化Count对象。
//接下来就可以在控制器中使用_count
public IActionResult Index()
{
int c = _count.MyCount();
ViewBag.count = c;
return View();
}
前端展示
运行效果,不断刷新页面也总是0,因为瞬态模式注入的服务,每一次访问都会创建一个新的实例。
上面ConfigureServices方法中改为下面的单例模式
//第2种:单例模式,整个应用程序生命周期以内只创建一个实例
services.AddSingleton<ICount, Count>();
运行效果,不断刷新页面不断增加1.
继续把上面ConfigureServices方法中改为下面的域模式
//第3种:域模式,在同一个Scope内只初始化一个实例 ,可以理解为( 每一个request级别只创建一个实例,同一个http request会在一个 scope内)
services.AddScoped<ICount, Count>();
运行效果,不断刷新页面一直保持为0,因为每次刷新页面都是一个新的请求,所以总是0,在一个请求内产生的实例对象才是唯一。
改进下测试代码
public class HomeController : Controller
{
private IServiceProvider _provider;
private ICount _count;//方便本类其他方法的调用,所以定义一个私有字段来接收
public HomeController(ICount count,IServiceProvider provider)//通过构造方法注入实例,ASP.NET CORE内置了依赖注入容器
{
_count = count;
_provider = provider;
}
//接下来就可以在控制器中使用_count
public IActionResult Index()
{
//int c = _count.MyCount();
//ViewBag.count = c;
//注意导入using Microsoft.Extensions.DependencyInjection;
ICount count1 = _provider.GetService<ICount>();
ICount count2 = _provider.GetService<ICount>();
int c1=count1.MyCount();
int c2 = count2.MyCount();
ViewBag.c1 = c1;
ViewBag.c2 = c2;
//ICount counter1 = _provider.GetService<ICount>();
//ICount counter2 = _provider.GetService<ICount>();
//int c1 = counter1.Get();
//int c2 = counter2.Get();
return View();
}
测试发现c1、c2分别为0,1
但是每次刷新又重新为0,1
因为每次刷新页面都是一个新的请求,所以总是0,在一个请求内产生的实例对象才是唯一
4.如何在视图中直接使用依赖注入
如下图修改视图,主要是通过@inject去注入实例对象
@inject DIDemo.Services.ICount count
@{
int c = count.MyCount();
}
运行结果为2,原因是因为在控制器中生成的实例中已经得到0、1,而视图中的实例还是通过控制器注入时生成的那个实例,所以这个时候为2
如果在startup中是以瞬态模式注入,那么都是0,不断刷新也还都是0;如果是以单例模式注入,那么开始就是0 1 2,不断刷新就依次加1,即第一次刷新就变为3 4 5,再刷新就变为6 7 8
八、中间件的理解与初步应用
1. 中间件概念通俗理解
一个web应用程序都是把http请求封装成一个管道,一般来说每一次的请求都要先经过管道的一系列操作,最终到达我们写的代码(主程序)中。那么中间件就是在应用程序管道中的一个组件,用来拦截请求过程进行一些其他处理和响应(比如验证用户身份是否合法、程序中是否有异常等)。中间件可以有很多个,每一个中间件都可以对管道中的请求进行拦截,它可以决定是否将请求转移给下一个中间件。
简言之:中间件是用于组成应用程序管道来处理请求和响应的组件。与过滤器类似,都属于AOP应用。
在ASP.NET Core中,针对HTTP请求采用pipeline(管道)方式来处理,而管道容器内可以挂载很多中间件(处理逻辑)“串联”来处理HTTP请求,每一个中间件都有权决定是否需要执行下一个中间件,或者直接做出响应。这样的机制使得HTTP请求能够很好的被层层处理和控制,并且层次清晰处理起来方便。
那么,何时使用中间件呢?一般是在我们的应用程序当中和业务关系不大的一些需要在管道中做的事情可以使用,比如身份验证,Session存储,日志记录等。
微软官方的一个中间件管道请求图:
从上图可以看出除最后一个中间件外,每个中间件包含3部分,一个进入时处理逻辑,一个是调用next方法进入下一个中间件,一个是退出中间件的处理逻辑。
意思即是每个委托均可在下一个委托前后执行操作。
此外,委托还可以决定不将请求传递给下一个委托,这就是对请求管道进行短路。
2. 自定义中间件
每个中间件在构建后就是一个RequestDelegate委托对象。实例:在当前解决方案下创建一个.NET Core mvc项目,命名为MiddlewareDemo,然后在其中创建一个文件夹Middlewares,再在其中创建MyMiddleware类。
public class MyMiddleware
{
//private IConfiguration _configuration;
//第一步:
private RequestDelegate _next;// using Microsoft.AspNetCore.Http;
//加一个构造方法.构造方法第一个参数必须是RequestDelegate类型,表示为中间件类型,即是表示为下一个中间件。定义中间件时必须包含对下一个中间件的引用。
public MyMiddleware(RequestDelegate next)
{
_next = next;//通过私有字段去接收下一个中间件的引用,因为我们在其他地方需要用这个下一个中间件next。这步是关键,必须的有,这个实现把中间件串联起来
//_configuration = configuration;
}
//第二步增加Task InvokeAsync(HttpContext context)方法,方法名称固定为InvokeAsync,返回值为Task
public async Task InvokeAsync(HttpContext context)
{
await context.Response.WriteAsync("test");
await _next.Invoke(context);
}
}
接下来在StartUp.cs文件的Configure方法中注入启用中间件。使用UseMiddleware方法注册:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseMiddleware<MyMiddleware>();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
中间件管道在应用程序启动时构建,默认情况下,中间件的执行顺序根据Startup.cs文件中,在public void Configure(IApplicationBuilder app){} 方法中注册的先后顺序执行。
自定义中间件总结:
- 自定义中间件类
•构造方法带RequestDelegate(下一个中间件的引用) 参数
•增加Task InvokeAsync(HttpContext context)方法 - 注册启用中间件
IApplicationBuilder.UseMiddleware()
此外还可以使用 Run、Map 和 Use 扩展方法来注册启用中间件。
app.Use(),IApplicationBuilder接口原生提供,注册等都用它。 Use不会主动短路整个HTTP管道,但是也不会主动调用下一个中间件,必须自行调用await next.Invoke(); 如果不使用这个方法去调用下一个中间件那么Use此时的效果其实和Run是相同的。
app.Run() ,是一个扩展方法,它需要一个RequestDelegate委托,里面包含了Http的上下文信息,没有next参数,因为它总是在管道最后一步执行。Run方法会使得可以使管道短路,顾名思义就是终结管道向下执行不会调用next()委托,所以Run方法最好放在管道的最后来执行。
app.Map(),也是一个扩展方法,类似于MVC的路由,用途一般是一些特殊请求路径的处理。Map可以根据提供的URL来路由中间件 。见下面例子(3)。
(1)如果中间件的逻辑比较简单,可以不去创建一个类,直接用
IApplicationBuilder.Use(
Func<RequestDelegate,RequestDelegate> middleware)
如:
app.Use(next =>
{
return context =>
{
return Task.CompletedTask;
};
});
(2)终点中间件——Run方法
整个中间件流水线的最后一个中间件,使用IApplicationBuilder.Run注册。
app.Run(context =>
{
return Task.CompletedTask;
});
(3)分支中间件——Map方法
根据决策逻辑确定后续执行路线,分支后不能再合并
//假设app端的请求均已api开头.下面的意思就表示请求是以api开头的就会进入MyMiddleware中间件,后续的中间件就不会执行。 .Map方法以路径作为决策
app.Map("/api", builder => {
builder.UseMiddleware<MyMiddleware>();
});
测试,在默认地址后加入/api就会调用MyMiddleware中间件,从而显示test。
其实我们的 asp.net core项目中本身已经包含了很多个中间件。
如果想添加使用更多的中间件,通过NuGet包管理器引用安装Microsoft.AspNetCore.Diagnostics
修改 Startup 类中的 Configure() 方法添加中间件 app.UseWelcomePage
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
//上面的代码在非开发环境时,UseExceptionHandler是第一个被加入到管道中的中间件,因为会捕获之后调用中出现的任何异常,然后跳转到设置的异常页/home/Error
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseWelcomePage();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}