概要:
在单独使用Mybatis时通常需要以下几行代码:
public class Demo {
public static void main(String[] args) {
// 第一阶段:MyBatis的初始化阶段
String resource = "mybatis-config.xml";
// 得到配置文件的输入流
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
// 得到SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 第二阶段:数据读写阶段
try (SqlSession session = sqlSessionFactory.openSession()) {
// 找到接口对应的实现
UserMapper userMapper = session.getMapper(UserMapper.class);
// 组建查询参数
User userParam = new User();
userParam.setSchoolName("Sunny School");
// 调用接口展开数据库操作
for (int i = 0; i < 2; i++) {
List<User> userList = userMapper.queryUserBySchoolName(userParam);
System.out.println(userList);
}
// 打印查询结果
// for (User user : userList) {
// System.out.println("name : " + user.getName() + " ; email : " + user.getEmail());
// }
}
}
}
mybatis完成一次查询的实现逻辑整体可以划为三部分:
- 解析配置文件生成全局配置 Configuration ,Configuration包含Mapper接口的注册器,类型处理器,xml的操作语句的映射等等。 在new SqlSessionFactoryBuilder().build(inputStream) 中实现
- 创建SqlSession获取Mapper接口的代理类。
- 最终交给Executor执行器进行参数解析,数据库操作和结果集映射,返回结果等操作。
下面以上面三块逻辑对mybatis执行流程以debug方式进行详细解析。
一. 全局配置的解析生成
SqlSessionFactoryBuilder的build方法获取配置文件的输入流,然后调用XMLConfigBuilder的parse方法解析生成一个Configuration全局配置对象。
从配置文件的根节点 /configuration开始解析
此方法完成Configuration对象的组装,包含了我们数据库的配置,默认的事务处理器,别名等环境信息,后重点看下mapperElement方法,它根据我们的配置把我们的Mapper接口保存到Configuration持有的MapperRegistry映射注册表中,如果配的是package会把包下所有接口都加载,我的配置是
<mappers>
<mapper resource="com/demo/UserMapper.xml"/>
</mappers>
所以debug的结果注册表中只有一个UserMapper,
MapperRegistry类中有一个名为knownMappers的Map,key为Mapper接口的Class对象,value为mapper接口的代理工厂。
MapperRegistry映射注册表的属性
看下这行代码干了啥 configuration.addMappers(mapperPackage);
调用的是MapperRegistry映射注册表的addMappers方法,继续点进去,里边完成的操作很简单,扫描包下符合条件的接口,然后添加到knownMappers属性中。最终MapperRegistry注册表中就包含了我们需要的所有Mapper接口映射。
二. 获取Mapper接口的代理对象
1. 获取数据库环境配置,创建默认的事务工厂,创建默认执行器,这里看下红色框框部门,可以看到CachingExecutor把SimpleExecutor包装了一下,显然是用了装饰器模式,CachingExecutor实现了mybatis的一级缓存,但是在于spring集成的时候,一级缓存会失效哦, 这里不细说了。
最后返回一个DefaultSqlSession对象
2.Sqlsession创建好了,下一步就是获取Mapper的代理对象了
最终调用的是MapperRegistry的getMapper方法,从knownMappers中获获取代理工厂,然后调用newInstance方法创建代理对象
可以看到 MapperProxy就是我们Mapper接口的代理对象, 学过java动态代理的都知道, 后边我们调用接口的方法实际会执行代理对象的invoke方法。 下面就开始分析Mybatis如何执行一次查询操作。
三.执行查询操作
1. 调用userMapper的queryUserBySchoolName查询方法会走到MapperProxy的invoke方法中
MapperMethod是接口中的方法信息,包含了我们的方法类型是SELECT, INSERT 还是UPDATE,返回结果是集合、map 还是void等等, 它会缓存到Mapper代理对象的methodCache属性中。
然后带着查询参数调用MapperMethod的execute方法。
该方法会根据你接口操作类型走不同的分支进行处理,我用的是一个查询语句类型是SELECT,并且返回的是一个集合,所以会走到executeForMany方法中
后边调用的是BaseExecutor中的query方法,先尝试从缓存获取结果,获取不到调用queryFromDatabase方法去数据库查
之后又调用doQuery方法,带do就是要真正执行查询操作去了
走到这一步可以看到sql还是带 ? 占位符的,这时参数还没映射到sql中,
可以看到经过prepareStatement方法后,此时sql中占位符已经填充好参数了,所以prepareStatement方法是用来处理参数映射的,这个后面再说。 然后下一步就是调用JDBC中PreparedStatement对象来执行数据库查询操作,ResultHandler来处理结果集映射将结果返回给我们,结果集映射挺复杂的,处理各种resultMap,嵌套查询,多结果集处理,还可以自定义类型处理器,延迟加载等很多特性,就不一一介绍了。
最后打印了两遍我的查询结果,发现两次查询得到的是用一个对象,说明了啥子? 说明用到了一级缓存。
参数映射
我的查询例子和sql
prepareStatement中调用StatementHandler的parameterize方法,最终调用DefaultParameterHandler进行参数映射,由于我的参数是一个User对象,舒服复杂类型参数,所以会背MetaObject包装,它的作用就是实现对包装对象进行反射操作的一些工具方法,如debug所示就获取到了User对象schoolName属性值 ”Sunny School“,
最后调用JDBC中的PreparedStatement对象给sql语句的参数赋值了
结束语
下班回家花了三个小时写了下mybatis源码,这是老夫第一次写文章,不管有木有人看,就当是我学习生涯的一个记录吧。