更新
1、如果看不懂本文,或者比较困难,先别着急问问题,我单写了一个关于依赖注入的小Demo,可以下载看看,多思考思考注入的原理: https://github.com/anjoy8/BlogArti/tree/master/Blog.Core_IOC&DI 2、重要:如果你实现了解耦,也就是 api 层只引用了 IService 和 IRepository 的话,那每次修改 service 层,都需要清理解决方案,重新编译项目,因为这个时候你的api层的dll,还是之前未修改的代码。 3、重要 :请注意,依赖注入的目的不是为了解耦,依赖注入是为了控制反转,通俗来说,就是不用我们自己去 new 服务实例了,所以大家不需要一定去解耦(比如下文说到的我没有引用 Service层 和 Repository层),我下一个DDD系列,依赖注入就没有解耦,因为我用的是自带的注入,不是Autofac的反射dll ,我解耦的目的,是为了让大家更好的理解,服务是怎么注入到宿主容器里的。
代码已上传Github Gitee,文末有地址
说接上文,上回说到了《八 || API项目整体搭建 6.3 异步泛型 依赖注入初探》,后来的标题中,我把仓储两个字给去掉了,因为好像大家对这个模式很有不同的看法,嗯~可能还是我学艺不精,没有说到其中的好处,现在在学DDD领域驱动设计相关资料,有了好的灵感再给大家分享吧。
到目前为止我们的项目已经有了基本的雏形,后端其实已经可以搭建自己的接口列表了,框架已经有了规模,原本应该说vue了,但是呢,又听说近来Vue-cli已经从2.0升级到了3.0了,还变化挺大,前端大佬们,都不停歇呀。当然我还在学习当中,我也需要了解下有关3.0的特性,希望给没有接触到,或者刚刚接触到的朋友们,有一些帮助,当然我这个不是随波逐流,只是在众多的博文中,给大家一个入门参考,届时说3.0的时候,还是会说2.0的相关问题的。
虽然项目整体可以运行了,但是我还有几个小知识点要说下,主要是1、依赖注入和AOP相关知识;2、跨域代理等问题(因为Vue是基于Node开发的,与后台API接口不在同一个地址);3、实体类的DTO相关小问题;4、Redis缓存等;5、部署服务器中的各种坑;虽然都是很小的知识点,我还是都下给大家说下的,好啦,开始今天的讲解;
零、今天完成的绿色部分
一、依赖注入的理解和思考
更新(19-04-17):如果想好好的理解依赖注入,可以从以下几个方面入手: 1、项目之间引用是如何起作用的,比如为啥 api 层只是引用了 service 层,那为啥也能使用 repository 和 model 等多层的类?
2、项目在启动的时候,也就是运行时,是如何动态 获取和访问 每一个对象的实例的?也就是 new 的原理
3、项目中有 n 个类,对应 m 个实例等,那这些服务,都放在了哪里?肯定每一个项目都有专属自己的一块。如果项目不启动的话,内存里肯定是没有这些服务的。
4、使用接口(面向抽象)的好处?
5、在项目后期,如何业务需要要全部修改接口的实现类,比如想把 IA = new A();全部 改成 IA = new B();
6、反射的重要性,为什么要用到反射 dll ?
如果这些每一条自己都能说清楚,那肯定就知道依赖注入是干啥的了。
说到依赖,我就想到了网上有一个例子,依赖注入和工厂模式中的相似和不同:
(1)原始社会里,没有社会分工。须要斧子的人(调用者)仅仅能自己去磨一把斧子(被调用者)。相应的情形为:软件程序里的调用者自己创建被调用者。 (2)进入工业社会,工厂出现。斧子不再由普通人完毕,而在工厂里被生产出来,此时须要斧子的人(调用者)找到工厂,购买斧子,无须关心斧子的制造过程。相应软件程序的简单工厂的设计模式。 (3)进入“按需分配”社会,需要斧子的人不需要找到工厂,坐在家里发出一个简单指令:须要斧子。斧子就自然出如今他面前。相应Spring的依赖注入。
在上篇文章中,我们已经了解到了,什么是依赖倒置、控制反转(IOC),什么是依赖注入(DI),网上这个有很多很多的讲解,我这里就不说明了,其实主要是见到这样的,就是存在依赖
代码语言:javascript复制public class A : D
{
public A(B b)
{
// do something
}
C c = new C();
}
就比如我们的项目中的BlogController,只要是通过new 实例化的,都是存在依赖
代码语言:javascript复制public async Task<List<Advertisement>> Get(int id)
{
IAdvertisementServices advertisementServices = new AdvertisementServices();
return await advertisementServices.Query(d => d.Id == id);
}
使用依赖注入呢,有以下优点:
- 传统的代码,每个对象负责管理与自己需要依赖的对象,导致如果需要切换依赖对象的实现类时,需要修改多处地方。同时,过度耦合也使得对象难以进行单元测试。
- 依赖注入把对象的创造交给外部去管理,很好的解决了代码紧耦合(tight couple)的问题,是一种让代码实现松耦合(loose couple)的机制。
- 松耦合让代码更具灵活性,能更好地应对需求变动,以及方便单元测试。
举个栗子,就是关于日志记录的
日志记录:有时需要调试分析,需要记录日志信息,这时可以采用输出到控制台、文件、数据库、远程服务器等;假设最初采用输出到控制台,直接在程序中实例化ILogger logger = new ConsoleLogger(),但有时又需要输出到别的文件中,也许关闭日志输出,就需要更改程序,把ConsoleLogger改成FileLogger或者NoLogger, new FileLogger()或者new SqlLogger() ,此时不断的更改代码,就显得心里不好了,如果采用依赖注入,就显得特别舒畅。
我有一个个人的理解,不知道恰当与否,比如我们平时食堂吃饭,都是食堂自己炒的菜,就算是配方都一样,控制者(厨师)还是会有些变化,或者是出错,但是肯德基这种,由总店提供的,店面就失去了控制,就出现了第三方(总店或者工厂等),这就是实现了控制反转,我们只需要说一句,奥尔良鸡翅,嗯就拿出来一个,而且全部分店的都一样,我们不用管是否改配方,不管是否依赖哪些调理食材,哈哈。
二、常见的IoC框架有哪些
1、Autofac 原生
我常用的还是原生注入和 Autofac 注入。
Autofac:貌似目前net下用的最多吧 Ninject:目前好像没多少人用了 Unity:也是较为常见 微软 core 自带的 DI
其实.Net Core 有自己的轻量级的IoC框架,
ASP.NET Core本身已经集成了一个轻量级的IOC容器,开发者只需要定义好接口后,在Startup.cs的ConfigureServices方法里使用对应生命周期的绑定方法即可,常见方法如下
services.AddTransient<IApplicationService,ApplicationService>//服务在每次请求时被创建,它最好被用于轻量级无状态服务(如我们的Repository和ApplicationService服务) services.AddScoped<IApplicationService,ApplicationService>//服务在每次请求时被创建,生命周期横贯整次请求 services.AddSingleton<IApplicationService,ApplicationService>//Singleton(单例) 服务在第一次请求时被创建(或者当我们在ConfigureServices中指定创建某一实例并运行方法),其后的每次请求将沿用已创建服务。如果开发者的应用需要单例服务情景,请设计成允许服务容器来对服务生命周期进行操作,而不是手动实现单例设计模式然后由开发者在自定义类中进行操作。
当然.Net Core自身的容器还是比较简单,如果想要更多的功能和扩展,还是需要使用上边上个框架。
2、三种注入的生命周期
权重:
AddSingleton→AddTransient→AddScoped
AddSingleton的生命周期:
项目启动-项目关闭 相当于静态类 只会有一个
AddScoped的生命周期:
请求开始-请求结束 在这次请求中获取的对象都是同一个
AddTransient的生命周期:
请求获取-(GC回收-主动释放) 每一次获取的对象都不是同一个
这里来个简单的小DEMO:
1、定义四个接口,并分别对其各自接口实现,目的是测试Singleton,Scope,Transient三种,以及最后的 Service 服务:
代码语言:javascript复制 public interface ISingTest
{
int Age { get; set; }
string Name { get; set; }
}
public class SingTest: ISingTest
{
public int Age { get; set; }
public string Name { get; set; }
}
//--------------------------
public interface ISconTest
{
int Age { get; set; }
string Name { get; set; }
}
public class SconTest: ISconTest
{
public int Age { get; set; }
public string Name { get; set; }
}
//--------------------------
public interface ITranTest
{
int Age { get; set; }
string Name { get; set; }
}
public class TranTest: ITranTest
{
public int Age { get; set; }
public string Name { get; set; }
}
//-----------------------
public interface IAService
{
void RedisTest();
}
public class AService : IAService
{
private ISingTest sing; ITranTest tran; ISconTest scon;
public AService(ISingTest sing, ITranTest tran, ISconTest scon)
{
this.sing = sing;
this.tran = tran;
this.scon = scon;
}
public void RedisTest()
{
}
}
2、项目注入
3、控制器调用
代码语言:javascript复制 private ISingTest sing; ITranTest tran; ISconTest scon; IAService aService;
public ValuesController(ISingTest sing, ITranTest tran, ISconTest scon, IAService aService)
{
this.sing = sing;
this.tran = tran;
this.scon = scon;
this.aService = aService;
}
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> SetTest()
{
sing.Age = 18;
sing.Name = "小红";
tran.Age = 19;
tran.Name = "小明";
scon.Age = 20;
scon.Name = "小蓝";
aService.RedisTest();
return new string[] { "value1", "value2" };
}
// GET api/values/5
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
return "value";
}
4、开始测试,三种注入方法出现的情况
请求SetTest // GET api/values
AddSingleton的对象没有变
AddScoped的对象没有变化
AddTransient的对象发生变化
------------------------------------------------------------
请求 // GET api/values/5
AddSingleton的对象没有变
AddScoped的对象发生变化
AddTransient的对象发生变化
注意:
由于AddScoped对象是在请求的时候创建的
所以不能在AddSingleton对象中使用
甚至也不能在AddTransient对象中使用
所以权重为
AddSingleton→AddTransient→AddScoped
不然则会抛如下异常
三、较好用的IoC框架使用——Autofac
首先呢,我们要明白,我们注入是要注入到哪里——Controller API层。然后呢,我们看到了在接口调用的时候,如果需要其中的方法,需要using两个命名空间
代码语言:javascript复制 [HttpGet("{id}", Name = "Get")]
public async Task<List<Advertisement>> Get(int id)
{
IAdvertisementServices advertisementServices = new AdvertisementServices();//需要引用两个命名空间Blog.Core.IServices;Blog.Core.Services;
return await advertisementServices.Query(d => d.Id == id);
}
接下来我们就需要做处理:
1、引入nuget包
在Nuget中引入两个:Autofac.Extras.DynamicProxy(Autofac的动态代理,它依赖Autofac,所以可以不用单独引入Autofac)、Autofac.Extensions.DependencyInjection(Autofac的扩展)
2、接管ConfigureServices
让Autofac接管Starup中的ConfigureServices方法,记得修改返回类型IServiceProvider
代码语言:javascript复制 public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
#region 配置信息
//Blog.Core.Repository.BaseDBConfig.ConnectionString = Configuration.GetSection("AppSettings:SqlServerConnection").Value;
#endregion
#region Swagger
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Version = "v0.1.0",
Title = "Blog.Core API",
Description = "框架说明文档",
TermsOfService = "None",
Contact = new Swashbuckle.AspNetCore.Swagger.Contact { Name = "Blog.Core", Email = "Blog.Core@xxx.com", Url = "https://www.jianshu.com/u/94102b59cc2a" }
});
//就是这里
#region 读取xml信息
var basePath = PlatformServices.Default.Application.ApplicationBasePath;
var xmlPath = Path.Combine(basePath, "Blog.Core.xml");//这个就是刚刚配置的xml文件名
var xmlModelPath = Path.Combine(basePath, "Blog.Core.Model.xml");//这个就是Model层的xml文件名
c.IncludeXmlComments(xmlPath, true);//默认的第二个参数是false,这个是controller的注释,记得修改
c.IncludeXmlComments(xmlModelPath);
#endregion
#region Token绑定到ConfigureServices
//添加header验证信息
//c.OperationFilter<SwaggerHeader>();
var security = new Dictionary<string, IEnumerable<string>> { { "Blog.Core", new string[] { } }, };
c.AddSecurityRequirement(security);
//方案名称“Blog.Core”可自定义,上下一致即可
c.AddSecurityDefinition("Blog.Core", new ApiKeyScheme
{
Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入{token}"",
Name = "Authorization",//jwt默认的参数名称
In = "header",//jwt默认存放Authorization信息的位置(请求头中)
Type = "apiKey"
});
#endregion
});
#endregion
#region Token服务注册
services.AddSingleton<IMemoryCache>(factory =>
{
var cache = new MemoryCache(new MemoryCacheOptions());
return cache;
});
services.AddAuthorization(options =>
{
options.AddPolicy("Admin", policy => policy.RequireClaim("AdminType").Build());//注册权限管理,可以自定义多个
});
#endregion
#region AutoFac
//实例化 AutoFac 容器
var builder = new ContainerBuilder();
//注册要通过反射创建的组件
builder.RegisterType<AdvertisementServices>().As<IAdvertisementServices>();
//将services填充到Autofac容器生成器中
builder.Populate(services);
//使用已进行的组件登记创建新容器
var ApplicationContainer = builder.Build();
#endregion
return new AutofacServiceProvider(ApplicationContainer);//第三方IOC接管 core内置DI容器
}
这个时候我们就把AdvertisementServices的new 实例化过程注入到了Autofac容器中,
这个时候要看明白,前边的是实现类,后边的是接口,顺序不要搞混了。
3、构造函数方式来注入
依赖注入有三种方式(构造方法注入、setter方法注入和接口方式注入),我们平时基本都是使用其中的构造函数方式实现注入,
在BlogController中,添加构造函数,并在方法中,去掉实例化过程;
代码语言:javascript复制 readonly IAdvertisementServices _advertisementServices;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="advertisementServices"></param>
public BlogController(IAdvertisementServices advertisementServices)
{
_advertisementServices = advertisementServices;
}
[HttpGet("{id}", Name = "Get")]
public async Task<List<Advertisement>> Get(int id)
{
//IAdvertisementServices advertisementServices = new AdvertisementServices();//需要引用两个命名空间Blog.Core.IServices;Blog.Core.Services;
return await _advertisementServices.Query(d => d.Id == id);
}
4、效果调试,已经成功
然后运行调试,发现在断点刚进入的时候,接口已经被实例化了,达到了注入的目的。
如果没有问题,大家就需要想想,除了 Autofac 还有没有其他的不用第三方框架的注入方法呢?聪明如你,netcore 还真自带了注入扩展。
5、NetCore 自带的注入实现效果
当然,我们用 Asp.net core 自带的注入方式也是可以的,也挺简单的,这里先说下使用方法:
代码语言:javascript复制 // 注入 service
services.AddScoped<IAdvertisementServices, AdvertisementServices>();
// 注入 repository
services.AddScoped<IAdvertisementRepository, AdvertisementRepository>();
这个时候,我们发现已经成功的注入了,而且特别简单,那为啥还要使用 Autofac 这种第三方扩展呢,我们想一想,上边我们仅仅是注入了一个 Service ,但是项目中有那么多的类,都要一个个手动添加么,多累啊,答案当然不是滴~
四、整个 dll 程序集的注入
1、服务程序集注入方式 —— 未解耦
通过反射将 Blog.Core.Services 和 Blog.Core.Repository 两个程序集的全部方法注入
修改如下代码,注意这个时候需要在项目依赖中,右键,添加引用 Blog.Core.Services 层和 Repository 层 到项目中,如下图,这个时候我们的程序依赖了具体的服务:
核心代码如下,注意这里是 Load 模式(程序集名):
代码语言:javascript复制 var builder = new ContainerBuilder();
//注册要通过反射创建的组件
//builder.RegisterType<AdvertisementServices>().As<IAdvertisementServices>();
var assemblysServices = Assembly.Load("Blog.Core.Services");//要记得!!!这个注入的是实现类层,不是接口层!不是 IServices
builder.RegisterAssemblyTypes(assemblysServices).AsImplementedInterfaces();//指定已扫描程序集中的类型注册为提供所有其实现的接口。
var assemblysRepository = Assembly.Load("Blog.Core.Repository");//模式是 Load(解决方案名)
builder.RegisterAssemblyTypes(assemblysRepository).AsImplementedInterfaces();
//将services填充到Autofac容器生成器中
builder.Populate(services);
//使用已进行的组件登记创建新容器
var ApplicationContainer = builder.Build();
其他不变,运行项目,一切正常,换其他接口也可以
到这里,Autofac依赖注入已经完成,基本的操作就是这样,不过可能你还没有真正体会到注入的好处,挑战下吧,看看下边的内容,将层级进行解耦试试!
2、程序集注入 —— 实现层级解耦
这是一个学习的思路,大家要多想想,可能会感觉无聊或者没用,但是对理解项目启动和加载,还是很有必要的。
1、项目最终只依赖抽象
最终的效果是这样的:工程只依赖抽象,把两个实现层删掉,引用这两个接口层。
2、配置仓储和服务层的程序集输出
将 Blog.Repository 层和 Service 层项目生成地址改成相对路径,这样大家就不用手动拷贝这两个 dll 了,F6编译的时候就直接生成到了 api 层 bin 下了:
“...Blog.CorebinDebug”
3、使用 LoadFile 加载服务层的程序集
代码语言:javascript复制 var basePath = Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath;//获取项目路径
var servicesDllFile = Path.Combine(basePath, "Blog.Core.Services.dll");//获取注入项目绝对路径
var assemblysServices = Assembly.LoadFile(servicesDllFile);//直接采用加载文件的方法
这个时候,可能我们编译成功后,页面能正常启动,证明我们已经把 Service 和 Repository 两个服务层的所有服务给注册上了,但是访问某一个接口,还是会出现错误:
这个错误表示,我们的 SqlSugar 服务,没有被注册成功,那肯定就是我们的 Sqlsugar 程序集没有正常的引用,怎么办呢,这里有两种方法,请往下看。
4、Sqlsugar 加载失败,方式一:api层引用 sugar
你可以直接在 api 层,添加对 sqlsugar 的使用,也可以直接不用修改,比如我的项目,多级之间存在级联的关系,api >> IService >> Model >> sqlSugar :
原因:在 Api层,引入 sqlSugarCore nuget 包,因为存在层级是存在依赖的
注意!这个地方很多人不理解为啥:
解耦,并不是啥直接不用了! 解耦仅仅是去掉引用耦合,目的是不用在修改了service.dll 层的某一个方法的时候,而停到api.dll这个整个服务, 当项目启动的时候,还是需要将所有的服务都注册到主机里, autofac依赖注入,仅仅是用反射的方法,将service.dll 和 repository.dll 项目服务进行注册,这个过程和引用是一样的,因为如果你引用,当项目编译或启动的时候,也是把层服务全部注入到主机的过程, 所以sqlsugar还是需要引用,因为需要这个组件的服务。
5、方式二:不引用Sugar包!但是需要拷贝 .dll 文件
如果你就想要 api 层干净,就是不想引用 sqlsugar 层的话,那就除非是把 sugar下的所有dll文件都拷贝进去,其实这样也是可以的,只要把第三方的nuget包生成的dll文件全部拷贝就行,你可以看下,sqlsugar依赖了很多dll
但是这个时候我们需要使用 LoadFrom 模式,因为我们上边使用的是 LoadFile 作为反射加载,这样的话,有一个问题,就是Repository层引用的 sqlsugar 会提示找不到,那我们就换一个反射加载方式 —— LoadFrom:
代码语言:javascript复制//二者的区别
Assembly.LoadFile只载入相应的dll文件,比如Assembly.LoadFile("a.dll"),则载入a.dll,假如a.dll中引用了b.dll的话,b.dll并不会被载入。
Assembly.LoadFrom则不一样,它会载入dll文件及其引用的其他dll,比如上面的例子,b.dll也会被载入。
哦!是不是瞬间想到了什么,没错就是 Sqlsugar 加载问题,我们换成 LoadFrom ,并且删除引用 sqlsugar 来看看,但是!一定要把 sqlsugar.dll 和 它依赖的 MySql.Data.dll 给拷贝进来api层;
所以总体来说有三个情况:
1、loadfile 模式 引用 SqlSugar 2、loadfile 模式 引用 3、loadfrom 模式 拷贝 .dll 文件
6、连接字符串问题
注意:如果采用上边的方法,把 service 和 Repository 层虽然解耦了,但是必须采用 LoadFile( dll 文件) 的形式,这样就导致了,在 startup.cs 启动中,无法给其他类库中的静态属性赋值的能力,比如:
BaseDBConfig.ConnectionString = "数据库连接字符串";
这个在 startup.cs 的ConfigureServices 方法中,是无法生效的。解决办法:
1、不解耦,还是采用普通办法,引用两个层,用 Assembly.Load("Blog.Core.Services") 方式;
2、按照上边解耦,但是数据库连接字符串配置,需要在 Repostory 层
// public static string ConnectionString { get; set; } public static string ConnectionString = Appsettings.app(new string[] { "AppSettings", "RedisCaching", "ConnectionString" });//获取连接字符串
7、解除Service层和Repository层之间的耦合
还记得Blog.Core.Services中的BaseServices.cs么,它还是通过new 实例化的方式在创建,仿照contrller,修改BaseServices并在全部子类的构造函数中注入:
代码语言:javascript复制 public class BaseServices<TEntity> : IBaseServices<TEntity> where TEntity : class, new()
{
//public IBaseRepository<TEntity> baseDal = new BaseRepository<TEntity>();
public IBaseRepository<TEntity> baseDal;//通过在子类的构造函数中注入,这里是基类,不用构造函数
//...
}
public class AdvertisementServices : BaseServices<Advertisement>, IAdvertisementServices
{
IAdvertisementRepository dal;
public AdvertisementServices(IAdvertisementRepository dal)
{
this.dal = dal;
base.baseDal = dal;
}
}
好啦,现在整个项目已经完成了相互直接解耦的功能,以后就算是Repository和Service如何变化,接口层都不用修改,因为已经完成了注入,第三方Autofac会做实例化的过程。
8、容器内查看注入的服务数据
如果你想看看是否注入到容器里成功了,可以直接看看容器 ApplicationContainer 的内容:
五、 无接口项目注入
1、接口形式的注入
上边我们讨论了很多,但是都是接口框架的,
比如:Service.dll 和与之对应的 IService.dll,Repository.dll和与之对应的 IRepository.dll,
这样,我们在多层之间使用服务的话,直接将我们需要使用的 new 对象,注入到容器里,然后我们就可以使用相应的接口了,
比如:我们想在 controller 里使用AdvertisementServices 类,那我们就可以直接使用它的接口 IAdvertisementServices,这样就很好的达到了解耦的目的,这样我们就可以在API层,就轻松的把 Service.dll 给解耦了;
如果我们需要在 Service类里,使用 AdvertisementRepository ,我们就直接使用对应的接口 IAdvertisementRepository,这样,我们就从 Service 层中,把仓储层给解耦了。
2、如果没有接口
案例是这样的:
如果我们的项目是这样的,没有接口,会怎么办:
代码语言:javascript复制 // 服务层类
public class StudentService
{
StudentRepository _studentRepository;
public StudentService(StudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
public string Hello()
{
return _studentRepository.Hello();
}
}
// 仓储层类
public class StudentRepository
{
public StudentRepository()
{
}
public string Hello()
{
return "hello world!!!";
}
}
// controller 接口调用
StudentService _studentService;
public ValuesController(StudentService studentService)
{
_studentService = studentService;
}
这样的话,我们就不能使用上边的接口注入模式了,因为我们上边是把注入的服务,对应注册给了接口了 .AsImplementedInterfaces() ,我们就无法实现解耦了,因为根本没有了接口层,所以我们只能引用实现类层,这样注入:
通过 builder.RegisterAssemblyTypes(assemblysRepository); 方法直接注入服务,没有其他的东西。
如果你需要这个小demo,可以从我的Github里下载:https://github.com/anjoy8/Blog.Core/blob/master/Blog.Core/wwwroot/NoInterAutofacIOC.rar
3、如果是没有接口的单独实体类
代码语言:javascript复制 public class Love
{
// 一定要是虚方法
public virtual string SayLoveU()
{
return "I ♥ U";
}
}
//---------------------------
////只能注入该类中的虚方法
builder.RegisterAssemblyTypes(Assembly.GetAssembly(typeof(Love)))
.EnableClassInterceptors()
.InterceptedBy(typeof(BlogLogAOP));
六、同一接口多实现类注入
这里暂时没有实例代码,如果你正好需要,可以看看这个博友的栗子:https://www.cnblogs.com/fuyujian/p/4115474.html
我会在之后的时间写个栗子放到这里。