网上有很多解读Mybatis源码的文章,无外乎断点跟踪源码,复制源码粘贴到文章中然后对代码写注释,也就是说先有了Mybatis然后才有的这些源码解析的文章,这就好比我们上语文课来解读课文一样。本章我们来自己动手实现Mapper文件的解析。
这是本地提交到GitHub代码的截图,主要是一个DaoMapper类实现了几乎所有功能。
这是本次代码在GitHub上展示的文件变动信息,由于目前处于开发后端框架阶段,工程还没有集成mvc所有功能及测试验证,所以只能一边实现代码,一边先发布文章。
很多事情说的远比做的简单,写代码就是这么一回事。你可能在后端开发中会用Mybatis,但是要很顺的按照自己的理解分析一遍整个运作流程还是有点困难的。我自己没看过Mybatis源码,在自己动手实现这个过程时,都是从突破口入手,确保走一条能走通的路才开始干的。
来看一下提交的5个类中最简单的一个实体类。
在培训学校或者大学课堂内老师肯定先教大家写在Java中用字符串调用jdbc执行SQL语句,肯定还会讲到SQL防注入,然后演示一波用恶意字符串实现SQL注入的操作。这里这个类就是接收解析后的预编译SQL,SQL中的参数值都会被替换成问号,在SQL预编译阶段确定了其结构,后续执行的过程才会捕获SQL注入异常。
上一章中我们有定义了Mapper文件的标签枚举,于是我们就能解析xml文档节点是那种标签,就能知道是增删改查中的哪种SQL语句,然后就能按分支展开讨论,用代码实现业务逻辑了。
上图是这次提交的5个类中的BaseUtil类的改动,增加了一个方法,实现像前端json对象一样链式取值的方法。考虑到一个复杂结构的实体参数有层次结构,那在SQL参数取值里面就可以用链式取值了,不用把参数打平成map结构。可以看到随着框架的不断迭代,工具类里面的方法也慢慢丰富起来了。
这是第3个类,里面是实现了一个第三方插件的接口类,这是按照其插件提供的方法的规定写的接口实现类,主要是用于一些代码反射时的权限自定义控制。这个插件是一个叫OGNL的技术的代码Java实现jar包,网上对其有诟病的就是安全问题,因为其动态编程和代码反射存在很大安全隐患,但是我们可以根据这个权限控制实现类来进行管理。一些分模块开发的工程,或者核心模块带代码不允许反射或者动态编程的就可以用这个类类做控制了,我们这里暂不做控制,按照我写的去用就可以了。
启动方法
在项目启动的入口添加Mapper解析代码,这是可以先开发的,属于打地基工作。想了下目前除了第一步加载配置文件方法必须先执行后面的几个步骤都可以没有顺序要求,后续可以考虑出错率高的先执行,节省项目错误时程序启动时间。
总体流程
现在看到的代码是最终呈现运作的代码,这就跟直接给你参考答案一样,整个解题过程我省略了。
首先,这个类有3个静态缓存,其中文档缓存和文件名缓存会在最后清空,最后只保留节点缓存。
其次,这个类有几个成员变量的定义,主要是为了减少一些方法的参数。
然后,我们来看总体解析流程的代码,整个过程很清晰顺畅的用注释给你写了,我就不复述了。上面先是扫描了一边所有存放Mapper文件的文件夹,获取到了所有文件对象,然后多线程解析xml文件。接着就开始串行校验xml文件,为什么是串行,因为涉及到嵌套依赖,并行会有问题。校验完根节点后就开始校验整个文档了。
递归读取文件
上面截图代码中获取mapper文件存放的包,然后读取下面所有xml文件,读取到线性的文件列表后,就可以根据文件数量来分每个配线程处理文件数量进行并发解析xml文件了。
多线程解析XML文件
根据总文件数量,按照最多30个线程进行分配每个线程处理的数量,除不尽的余数作为最后一个线程单独处理。然后处理把所有异常都抛出,启动方法会自动终止程序启动。
解析XML线程方法
前面提到当前类有两个静态成员变量缓存了xml文件名称和xml文件对象。后面在解析单个xml文件内容的时候,抛出的异常需要提示出xml文件路径。
在解析xml文件的时候需要校验xml文件语法,我用的jdk自带的xml解析类处理时会联网获取dtd文件,所以就把它的校验方式改为读取本地校验文件处理了。
注意到用的就是mybatis原始的语法校验规则文件,我的简化版mapper解析同样支持移植。
缓存所有文档节点
注意到写sql语句的mapper文件有一个很大的特点,就是其内容节点都是并列写的,增删改查语句都是在根节点下挂载的,这带来了很大好处就是做节点缓存只需要一次遍历即可,不需要递归遍历文档树。
节点缓存设置了二级索引,方便在做交叉引用命名空间的时候用到。缓存节点的同时校验每个xml文件的节点id不能重复。
节点校验
所有节点缓存设置好后,开始逐个遍历xml文档,校验下面的所有二级节点。
校验无非就是标签名称、属性名称以及属性值这三个,我们逐个来看校验逻辑:
节点校验:标签
只允许支持的节点标签可以输入,不支持的写了也没用,这样的强校验报错方便后续拓展没有支持的标签。
节点校验:属性值
通过枚举定义每个标签支持配置的属性,只对支持的属性进行校验,多余的属性在我的框架中用不到,但是也不会影响业务逻辑,为了能友好的支持MyBatis迁移过来的mapper文件,对不支持的属性做跳过处理,不再是抛出异常了。
而在校验属性的方法里面则先保留抛出异常的代码。必填属性没有值需要抛出异常,有值则需要校验值的合法性,下面我们来看下节点属性的校验:
属性值校验
其实前面的代码主要是把整个过程串起来,现在的代码主要是做业务逻辑校验,相对后面的代码偏逻辑一点,看起来会比较简单。目前要校验的地方就几个分别罗列如下:
属性值校验:id
id为空或者包含点都是非法的,其中id有点如果允许则破坏了命名空间的规则所有要报错。
属性值校验:namespace
命名空间被设计为类名就比较方便把类和mapper对应起来了,所以校验命名空间就只需要校验对应的类存不存在即可。
属性值校验:test表达式
mapper文件写SQL的优势之一就是写条件判断语句很清晰,多属性的拓展写法是用java代码拼接字符串所做不到的,对于表达式由于要依赖运行中的数据,所以暂时只能校验其表达式是否存在。
属性值校验:resultType
除了byte不支持外还去除了float的返回值。
属性值校验:index
这个主要用在数组操作上,方便拼接in条件的sql查询。
属性值校验:collection
这个是配合index一起使用的,做数组的遍历操作。
属性值校验:refid
最后这个是关联节点的属性,这个是相对复杂些。针对没有包含"."的从当前节点缓存获取节点对象,而包含有点的则是从所有命名空间里面查找,这就解释了前面为什么说校验id属性包含"."必须抛出异常。
递归校验
由于每个节点都可以自由组合sql片段,所以需要做递归校验。由于每个节点都可以自由组合sql片段,所以需要做递归校验。一般这种嵌套层次不会太深,直接用递归问题不大。
获取SQL语句
本来这个方法是应该放到讲dao的动态代理那一章去的,无奈这个方法写在这个类里面,又和整个类关系比较密切,所以把这个方法也放出来了。
在之前daoFactory的实现中用到了带有占位符的sql语句,这个方法就是提取这个sql的方法实现。后续在dao的接口代理类调用接口方法时会拿到namespace、id以及参数,就可以通过这个方法把节点的sql文本提取出来了。
由于每个节点都可以嵌套组合,所以在解析单个节点时还需要对节点的子节点做递归解析,解析子节点代码如下:
这个类的实现需要花大量的时间和精力去调试。像xml节点的遍历会有空文本节点这种根本不在自己的思考之内,但是调试的时候却偏偏出来了这个没用的节点;像要保持xml文档内容的排版格式要如何组合字符串;像数组遍历操作的自己实现;还有test表单式的实现所需要的jar包和方法实现等。
test表达式是需要用到一个ognl的jar包和javassist动态编程jir包的,我把源码解压出来看了,差不多几十个类,要自己实现支持自己框架的ognl其实也挺简单,但是要考虑到MyBatis移植,所以就先用jar包把所有ognl给支持了。
OGNL:test表达式
我的解析遇到test表单式用到了一个第三方插件包,下次遇到高级的语法时我再来拓展支持。
OGNL:forEach表达式
有MyBatis在前面开路,我只需要把这个标准拿过来用就行。
反正就是同样自己实现一下,这个就没用到ongl包的方法了,自己实现了一下,方便今后做技术拓展。像test表达式简单但是实现起来却比较复杂,就用专业的包处理了。这里面由于涉及到修改sql语句结构所以会处理#{}和${}的逻辑。
OGNL:#{}和${}替换
上面用到的字符串占位替换方法也是自己实现的一次遍历算法,代码比较长截图如下:
不解释这串代码了,逻辑很简单,在以前的章节套路网页模板的时候有比这个更复杂的方法,上面写有注释,这里不再重复写了。
后面的章节要先实现mvc控制才能最终实现dao的调用,所以dao的代理反射还是要放一放再来写。