前言
今天会给大家分享我们常用的持久层框架——MyBatis的工作原理和源码解析,后续会围绕Mybatis框架做一些比较深入的讲解,之后这部分内容会归置到公众号菜单栏:连载中…-框架分析中,欢迎探讨!
说实话MyBatis是我第一个接触的持久层框架,在这之前我也没有用过Hibernate,从Java原生的Jdbc操作数据库之后就直接过渡到了这个框架上,当时给我的第一感觉是,有一个框架太方便了。
举一个例子吧,我们在Jdbc操作的时候,对于对象的封装,我们是需要通过ResultSet.getXXX(index)
来获取值,然后在通过对象的setXXX()
方法进行手动注入,这种重复且无任何技术含量的工作一直以来都是被我们程序猿所鄙视的一环,而MyBatis就可以直接将我们的SQL查询出来的数据与对象直接进行映射然后直接返回一个封装完成的对象,这节省了程序猿大部分的时间,当然其实JdbcTemplate也可以做到,但是这里先不说。
MyBatis的优点有非常多,当然这也只有同时使用过Jdbc和MyBatis之后,产生对比,才会有这种巨大的落差感,但这并不是今天要讨论的重点,今天的重心还是放在MyBatis是如何做到这些的。
对于MyBatis,给我个人的感受,其工作流程实际上分为两部分:第一,构建,也就是解析我们写的xml配置,将其变成它所需要的对象。第二,就是执行,在构建完成的基础上,去执行我们的SQL,完成与Jdbc的交互。而这篇的重点会先放在构建上。
Xml配置文件
玩过这个框架的同学都知道,我们在单独使用它的时候,会需要两个配置文件,分别是mybatis-config.xml和mapper.xml,在官网上可以直接看到,当然这里为了方便,我就直接将我的xml配置复制一份。
<!-- mybatis-config.xml --> <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- 和spring整合后 environments配置将废除 --> <environments default="development"> <environment id="development"> <!-- 使用jdbc事务管理 --> <transactionManager type="JDBC" /> <!-- 数据库连接池 --> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://xxxxxxx:3306/test?characterEncoding=utf8"/> <property name="username" value="username" /> <property name="password" value="password" /> </dataSource> </environment> </environments> <!-- 加载mapper.xml --> <mappers> <!-- <package name=""> --> <mapper resource="mapper/DemoMapper.xml" ></mapper> </mappers> </configuration>
<!-- DemoMapper.xml --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.DemoMapper"> <select id="queryTest" parameterType="Map" resultType="Map"> select * from test WHERE id =#{id} </select> </mapper>
我们不难看出,在mybatis-config.xml这个文件主要是用于配置数据源、配置别名、加载mapper.xml,并且我们可以看到这个文件的<mappers>
节点中包含了一个<mapper>
,而这个mapper所指向的路径就是另外一个xml文件:DemoMapper.xml,而这个文件中写了我们查询数据库所用的SQL。
而,MyBatis实际上就是将这两个xml文件,解析成配置对象,在执行中去使用它。
解析
MyBatis需要什么配置对象?
虽然在这里我们并没有进行源码的阅读,但是作为一个程序猿,我们可以凭借日常的开发经验做出一个假设。假设来源于问题,那么问题就是:为什么要将配置和SQL语句分为两个配置文件而不是直接写在一起?
是不是就意味着,这两个配置文件会被MyBatis分开解析成两个不同的Java对象?
不妨先将问题搁置,进行源码的阅读。
环境搭建
首先我们可以写一个最基本的使用MyBatis的代码,我这里已经写好了。
public static void main(String[] args) throws Exception { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); //创建SqlSessionFacory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); /******************************分割线******************************/ SqlSession sqlSession = sqlSessionFactory.openSession(); //获取Mapper DemoMapper mapper = sqlSession.getMapper(DemoMapper.class); Map<String,Object> map = new HashMap<>(); map.put("id","123"); System.out.println(mapper.selectAll(map)); sqlSession.close(); sqlSession.commit(); }
看源码重要的一点就是要找到源码的入口,而我们可以从这几行程序出发,来看看构建究竟是在哪开始的。
首先不难看出,这段程序显示通过字节流读取了mybatis-config.xml文件,然后通过SqlSessionFactoryBuilder.build()
方法,创建了一个SqlSessionFactory(这里用到了工厂模式和构建者模式),前面说过,MyBatis就是通过我们写的xml配置文件,来构建配置对象的,那么配置文件所在的地方,就一定是构建开始的地方,也就是build方法。
构建开始
进入build方法,我们可以看到这里的确有解析的意思,这个方法返回了一个SqlSessionFactory,而这个对象也是使用构造者模式创建的,不妨继续往下走。
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { //解析mybatis-config.xml //XMLConfigBuilder 构造者 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); //parse(): 解析mybatis-config.xml里面的节点 return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } }
进入parse():
public Configuration parse() { //查看该文件是否已经解析过 if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } //如果没有解析过,则继续往下解析,并且将标识符置为true parsed = true; //解析<configuration>节点 parseConfiguration(parser.evalNode("/configuration")); return configuration; }
注意parse的返回值,Configuration,这个似曾相识的单词好像在哪见过,是否与mybatis-config.xml中的<configuration>
节点有所关联呢?
答案是肯定的,我们可以接着往下看。
看到这里,虽然代码量还不是特别多,但是至少现在我们可以在大脑中得到一个大致的主线图,也如下图所示:
沿着这条主线,我们进入parseConfiguration(XNode)方法,接着往下看。
private void parseConfiguration(XNode root) { try { //解析<Configuration>下的节点 //issue #117 read properties first //<properties> propertiesElement(root.evalNode("properties")); //<settings> Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); loadCustomLogImpl(settings); //别名<typeAliases>解析 // 所谓别名 其实就是把你指定的别名对应的class存储在一个Map当中 typeAliasesElement(root.evalNode("typeAliases")); //插件 <plugins> pluginElement(root.evalNode("plugins")); //自定义实例化对象的行为<objectFactory> objectFactoryElement(root.evalNode("objectFactory")); //MateObject 方便反射操作实体类的对象 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 //<environments> environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); // typeHandlers typeHandlerElement(root.evalNode("typeHandlers")); //主要 <mappers> 指向我们存放SQL的xxxxMapper.xml文件 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
可以看到这个方法已经在解析<configuration>
下的节点了,例如<settings>
,<typeAliases>
,<environments>
和<mappers>
。
这里主要使用了分步构建,每个解析不同标签的方法内部都对Configuration对象进行了set或者其它类似的操作,经过这些操作之后,一个Configuration对象就构建完毕了,这里由于代码量比较大,而且大多数构建都是些细节,大概知道怎么用就可以了,就不在文章中说明了,我会挑一个主要的说,当然有兴趣的同学可以自己去pull MyBatis的源码看看。