看了很多依赖注入的插件,有时候一直在想,是不是都需要定义一个容器来绑定依赖注入的动态库,难道就不能按需注入?我这里的诉求其实很简单,希望注入的实体,在项目中没有任何一个地方是需要强引用的。
这里以切换关系数据库为例子。我在开发中定义,不允许在数据库中写任何编程性的代码,例如视图、存储过程、方法等,这样做也是为了迁移数据库做准备的,这个是大前提,否则,我接下来举例就没有意义了。我认为,数据库就是提供高速存储服务就足够了。
在dotnet中,很多orm都支持多种数据库的支持,也都支持大部分的关系数据库,例如Oracle,Mssql,Mysql,Postgresql,Sqlite等,我把不同的数据库操作封装成不同的数据库访问层基类,例如 cyb.DAL.SQLServer,cyb.DAL.Oracle,....类似这样,这些实体都实现接口cyb.IDAL
- //mssql的数据库访问实体
- public abstract class BaseDAL<T> : IBaseDAL<T>
- where T:class
- {
- //orm数据库访问实体
- protected mssqlContext dbContext {get;private set;}
- }
-
-
- 接口
- public interface IBaseDAL<T>:IBaseDAL
- where T:class
- {
- }
-
所有的DAL基类都实现接口IBaseDAL,那么这里就已经定义好了DAL的规范,包括增删改查等等,再往下派生,定义一个员工表
namespace cyb.IDAL.Basic
public interface IDALEmployee : IBaseDAL
{
}
namespace cyb.DAL.Basic
public class DALEmployee : BaseDAL
{
}
到此,类DALEmployee和接口IDALEmployee已经产生了关系,且在整个系统中,只存在这么一种关系,DALEmployee是接口IDALEmployee唯一实现实体类。
为了加快搜索注入的程序集,我把程序集cyb.DAL.Basic放到目录 bin\DAL中。
在系统中定义一个IOCHelper工具类,此了要完成两个事情,第一,搜索接口的实现类;第二,缓存完成匹配的实现类;
public static createObject
参数path:是搜索实现类的位置,例如DAL或者其他什么,这个没有关系了,就是方便搜索,留空是在整个appdomain中搜索,就是第一次的时候会稍微慢一点点,毕竟搜索的程序集会比较多。
在工具类内部定义一个字典容器 Dictionary
方法体也简单,第一步就是找文件,当然是找dll文件,如果你写的程序集后缀不是dll,那么自己改改就好了,这里不贴出代码,其实代码挺多的,后边慢慢分析。大致思路如下:
- //iType是接口类型
- if(cache.TryGetValue(iType,out Type impType)
- {
- var imp = Activator.CreateInstance(typ);
- return imp;
- }
-
- var dir = new DirectoryInfo(这里是搜索的目录);
-
- var fis = dir.GetFiles("*.dll");
-
- foreach(var fi in fis)
-
- {
- //这里一定要加错误陷进,有时候,搜索到的dll文件不一定是dotnet的程序集,或者导出类时候是有问题的。
- try
- {
- var asm = Assembly.Load(fi.FullName);
- //这里就把实现类找出来了
- var types = asm.GetExportedTypes().Where(t => iType.IsAssignableFrom(t) && !t.IsInterface && t.IsAbstract == false).ToList();
- if(types.Count == 0)
- {
- //这里报个错,没找到
- return null;
- }
- if(types.Count > 1)
- {
- //这里找到多个实现类,也报个错
- return null;
- }
- cache[iType]=types[0];
- var imp = Activator.CreateInstance(typ);
- return imp;
- }
- catch(Exception ex)
- {
- logHelper.Error(ex);
- }
- }
其实代码可以改进一下,先从AppDomain中找已经加载的程序集,没找到再去找dll文件,细节就不提了,这里只是举例而已。
关键一步来了,看java实现注入的写法,挺爽的,于是也就有样学样了,做一个标注。在业务层对象BLLEmployee : IBLLEmployee中定义:
[Autowire]
protected IDALEmployee dal;
创建这个业务层的时候,同时要调用一下createObject
好嘛,控制层(Controler)创建的时候,就创建业务层,那么一层层往下创建,也就能一层层注入了。
最难的就是调用注入方法createObject
在dotnet core中,这种方法好像行不通,好吧,那就网上查看一下微软的文档,找到一个很好的办法,在加载注入dll时候,把依赖的文件也加载进来
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(MyResolveEventHandler);
其中 MyResolveEventHandler方法就是处理加载依赖的事件方法,里边怎么写网上也有代码,抄一下改一下就好了,我是改了很多的。
另外为了知道注入过程中出错的时候到底发生了什么,要在关键代码处输出最详细的日志,例如加载了哪个dll文件,导出类的时候是什么问题导致失败,创建实体对象的时候是什么原因出现异常等等,总之多写日志就是有好处的,简化了后续的调试,我遇到最多的就是找不到依赖文件,其次就是写实体类的时候,构造函数、属性、成员的初始化有问题,通过记录日志就非常方便的发现问题并能快速解决。一切问题都解决后,日志也就没有了。
最后,不要尝试对注入的容器做持久化,其实没有什么意义,在没有加载程序集的时候,实现类是不可能呢出现的,持久化重新加载的时候必然报错,不要为了那么一点点时间而干难堪的事情。
我也用样的方法尝试按常规来做依赖注入,就是使用网上常说的几种方法,都会有些问题,所以我就放弃了,另外,这样的注入方法自己写的,支持Framework和core,在不同架构下使用相同方法,对于程序员来说,没有任何不适。
在我的框架中,使用反射的机会还是很高的,但是同学们也要自己注意,毕竟IO操作还是有风险的,特别是linux下,要放开权限给程序,这里是相当于一个安全漏洞了,目前我还没更好的方法解决。动态加载文件,也很可能被恶意注入程序,所以,在使用的时候也要确保服务器的安全,我认为那是网管的事情,做一回甩锅侠。
好了,大致也就这样了,框架的应用就是想要程序员开发代码的时候,不要太过于关注技术问题,把精力都放到两个方面,一个是业务实现,一个是和产品经理斗智斗勇。如果看过我前边写的文章就知道了,我把redis,mq,内部通讯,分布式事务,内存缓存等都封装成了组件。
我还把与其他微服务交互的操作都封装成了cyb.DAL.Api程序集,当然是针对自己框架的情况下使用,因为这里封装了分布式事务的关键几个操作,程序员就象写一般DAL一样来操作WebApi,查询也是使用lambda。
谢谢大家关注我的框架设计和开发之旅!