这样看 mybatis,谁都会分析源码!

用户头像
诸葛小猿
关注
发布于: 2020 年 08 月 16 日
这样看mybatis,谁都会分析源码!

java程序员访问数据库的方式有很多种,为了简化开发,都会选择使用框架访问数据库,而mybatis是我们常用的一种操作数据库的框架。



本期我们通过展示实际测试结果,带领大家分析mybatis的源码,提升对框架的理解。



本文相关的分析参照资料来源:





  • mybatis源码包:版本号3.4.1



关注公众号,输入关键字“java-summary”,即可获得源码。



1. JDBC访问数据库



访问数据库的方式有多种,可以使用原生的JDBC,也可以使用spring+mybatis,还可以使用springboot+mybatis。这几种方式一个比一个简单,需要的配置也是一个比一个少,但原理都是通用的。最底层也还是我们熟悉的JDBC。



万变不离其宗,下面我们对照JDBC使用方式,分析mybatis源码。



sql



CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(64) DEFAULT NULL COMMENT '姓名',
`passwd` varchar(64) DEFAULT NULL COMMENT '密码',
`age` int(11) DEFAULT NULL COMMENT '年龄',
`address` varchar(128) DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8



这里是本文测试需要用到的sql语句



TestJDBC.java



package com.wuxiaolong.mybatis;
import java.sql.*;
/**
* Description: JDBC测试
*
* 只需要一个mysql的驱动jar包
* <dependency>
* <groupId>mysql</groupId>
* <artifactId>mysql-connector-java</artifactId>
* <version>5.1.44</version>
* </dependency>
*
* @author 诸葛小猿
* @date 2020-08-13
*/
public class TestJDBC {
public static void main(String[] args) throws Exception{
// 数据库连接配置
String url = "jdbc:mysql://127.0.0.1:3306/java-summary?characterEncoding=UTF-8";
String username = "root";
String password = "123456";
String drive = "com.mysql.jdbc.Driver";
// sql语句配置
String sql = "select * from user where id=?";
// 1.加载驱动类
Class.forName(drive);
// 2.获取连接
Connection connection = DriverManager.getConnection(url, username, password);
// 3.创建 preparedStatement
PreparedStatement prepareStatement = connection.prepareStatement(sql);
// 3.初始化参数
prepareStatement.setInt(1, 1);
// 4.执行sql
ResultSet rs = prepareStatement.executeQuery();
// 打印结果
while (rs.next()){
System.out.println(rs.getString("username"));
}
// 关闭连接
connection.close();
}
}



JDBC的使用,主要步骤有四步:加载驱动类、获取连接对象、创建Statement、执行sql。



上面的配置信息可以看成两类:数据库连接配置、sql语句配置。大家记住这两配置,后面不管是使用spring+mybatis,还是使用springboot+mybatis,都离不开这两类配置。



这是最直接的连接方式,通过驱动直接连接数据库,只需要集成mysql-connector-java这一个jar包即可。



2.直接使用mybatis访问数据库



MybatisTest.java



package com.wuxiaolong.mybatis;
import com.wuxiaolong.mybatis.entity.UserEntity;
import com.wuxiaolong.mybatis.mapper.UserEntityMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.InputStream;
import java.io.Reader;
/**
* Description: 手动使用mybatis
*
* 需要引入两个jar:
* <dependency>
* <groupId>org.mybatis</groupId>
* <artifactId>mybatis</artifactId>
* <version>3.4.1</version>
* </dependency>
* <dependency>
* <groupId>mysql</groupId>
* <artifactId>mysql-connector-java</artifactId>
* <version>5.1.44</version>
* </dependency>
* @author 诸葛小猿
* @date 2020-08-13
*/
public class MybatisTest {
public static void main(String[] args) throws Exception {
// 1.加载配置文件,获得SqlSessionFactory
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 2.获取session,主要的CRUD操作均在SqlSession中提供
SqlSession session = sqlSessionFactory.openSession();
// 3.1执行sql方式一:通过方法全名
UserEntity user = session.selectOne("com.wuxiaolong.mybatis.mapper.UserEntityMapper.selectByPrimaryKey", 1);
System.out.println(user);
// 3.2执行sql方式二:通过Mapper接口
UserEntityMapper mapper = session.getMapper(UserEntityMapper.class);
UserEntity user2 = mapper.selectByPrimaryKey(1);
System.out.println(user2);
session.close();
}
}



上面是使用mybatis的简单测试样例,可以直接运行main方法。整个执行过程大概有三步:



  1. 通过InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml")加载配置文件中的信息,然后生成SqlSessionFactory。

  2. 通过SqlSessionFactory获得SqlSession。这里的SqlSession是关键,后面我们会重点分析源码。

  3. 使用SqlSession获得数据库中主键id=1的数据记录。这里使用了两种方式,第一种是使用session.selectOne(),第二种是使用UserEntityMapper.selectByPrimaryKey()



这种方式我们使用了两个jar包,几个是驱动mysql-connector-java,一个是mybatis框架mybatis。使用mybatis框架,将上面JDBC的相关对象进行了封装,这里虽然我们只看到了SqlSession,其实底层还是JDBC。



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>
<!-- 数据库连接配置:可以配置多个环境,default:配置某一个环境的唯一标识,表示默认使用哪个环境 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!-- 配置连接信息 -->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/java-summary?characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!-- 配置映射文件 sql语句配置-->
<mappers>
<!--方式一、指定多个Mapper.xml文件-->
<mapper resource="mapper/UserEntityMapper.xml"/>
<!--方式二、Mapper.xml文件所在的包的路径-->
<!-- <package name="com.wuxiaolong.mybatis.mapper"/>-->
</mappers>
</configuration>



这个配置文件中的信息包括了上面JDBC中说的两类配置:数据库连接配置、sql语句配置。这里只是通过xml文件的形式进行集中配置的。其实本质都是一样的。



UserEntityMapper.java



package com.wuxiaolong.mybatis.mapper;
import com.wuxiaolong.mybatis.entity.UserEntity;
/**
* Description: Mapper文件
*
* @author 诸葛小猿
* @date 2020-08-13
*/
public interface UserEntityMapper {
UserEntity selectByPrimaryKey(Integer id);
int insertSelective(UserEntity record);
}



这是用户对象的接口。在实际开发中直接使用这个类就可以了,参考上面的MybatisTest.java中的sql执行的第二种方式。



UserEntityMapper.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.wuxiaolong.mybatis.mapper.UserEntityMapper">
<resultMap id="BaseResultMap" type="com.wuxiaolong.mybatis.entity.UserEntity">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="passwd" jdbcType="VARCHAR" property="passwd" />
<result column="age" jdbcType="INTEGER" property="age" />
<result column="address" jdbcType="VARCHAR" property="address" />
</resultMap>
<sql id="Base_Column_List">
id, username, passwd, age, address
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from user
where id = #{id,jdbcType=INTEGER}
</select>
<insert id="insertSelective" parameterType="com.wuxiaolong.mybatis.entity.UserEntity">
insert into user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
id,
</if>
<if test="username != null">
username,
</if>
<if test="passwd != null">
passwd,
</if>
<if test="age != null">
age,
</if>
<if test="address != null">
address,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id,jdbcType=INTEGER},
</if>
<if test="username != null">
#{username,jdbcType=VARCHAR},
</if>
<if test="passwd != null">
#{passwd,jdbcType=VARCHAR},
</if>
<if test="age != null">
#{age,jdbcType=INTEGER},
</if>
<if test="address != null">
#{address,jdbcType=VARCHAR},
</if>
</trim>
</insert>
</mapper>



这是sql语句的Mapper.xml文件,这个文件用来写sql语句,和UserEntityMapper.java文件对应。



3.源码分析



3.1.mybatis.jar项目结构



首先,打开mybatis.jar包,来看看项目的结构。





其中几个重要的package的简单说明:



  • session包:这个包是MyBatis接口层的核心类。其中包括SqlSessionFactory、SqlSession这两个接口及其默认的实现类。SqlSession是实现所有数据库操作的API。除此之外,还有一个Configuration,这个类是mybatis的配置类,mybatis-config.xml和UserEntityMapper.xml都会被加载到这个类中。

  • builder包:这个包中包含了xml文件解析相关的类,其中XMLConfigBuilder是用来解析mybatis-config.xml文件的,XMLMapperBuilder是用来解析UserEntityMapper.xml文件的。

  • executor包:这个包中包含了数据库操作的相关方法,其中Executor是MyBatis的核心,围绕着它完成了数据库操作的完整过程。Executor主要提供了sql语句查询等数据库操作的相关方法。

  • cache包:缓存是MyBatis里比较重要的部分,有两种缓存:1.一级缓存。BaseExecutor中根据MappedStatement的Id、SQL、参数值以及rowBound(边界)来构造CacheKey,并使用BaseExccutor中的localCache来维护此缓存。2.全局的二级缓存,通过CacheExecutor来实现,其委托TransactionalCacheManager来保存/获取缓存,这个全局二级缓存比较复杂;通过配置可以开启二级缓存。全局二级缓存是基于Mapper.xml的namespace实现的,连表查询生产的缓存在单表更新时不一定会被更新,可能产生数据的不一致问题;同时二级缓存的失效策略是一个更新操作会导致该namespace下的所有缓存失效;所以这个二级缓存很鸡肋,一般都不用,实际开发中使用redis/memocached代替。

  • datasource包:这个包是MyBatis自身提供的一个简易的数据源/连接池,主要实现类是PooledDataSource,这个连接池的实现比较简单,实际项目中我们会继承集成其他的连接池,如阿里的druid。

  • transaction包:这个包是MyBatis自身提供一个简单的事物处理,并不支持内嵌事务这样较复杂的场景,所以在实际开发中会委托Spring来处理事务实现真正的与开发者隔离。

  • reflection包: 在MyBatis中大量地使用了反射,需要频繁地读取Class元数据,这个包对常见的反射操作进一步封装,以提供更简洁方便的API。

  • io包:这个包提供读取资源文件的API、封装MyBatis自身所需要的ClassLoader和加载顺序。



3.2.SqlSessionFactory



看源码时,可以通过debug上面的MybatisTest.java一步一步往下看。这里是MybatisTest.java中SqlSessionFactory的生成这一步的代码:



SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);



获得SqlSessionFactory的目的是为了下面获得核心的SqlSession,从而访问数据库。获得SqlSessionFactory的过程就是解析mybatis-config.xml和UserEntityMapper.xml,并生成可重复使用的Configuration的过程。



通过debug模式,进入到这个SqlSessionFactoryBuilder().build()方法中,找到核心代码:





3.3.XMLConfigBuilder



上面出现了XMLConfigBuilder,他是用来解析mybatis-config.xml文件的,具体解析的过程就是调用XMLConfigBuilder.parse()`,返回一个Configuration。下面的parse方法中可以看到mybatis-config.xml文件的节点标签:比如父节点configuration、settings标签,properties标签, typeAliases标签, environments标签,mappers标签等。





上面的environments标签解析,this.environmentsElement(root.evalNode("environments")) 就是获取mybatis-config.xml文件中数据库连接参数的过程。可以看出这里,这里生成了TransactionFactory、DataSourceFactory、DataSource等信息,并将信息存储在Configuration.environment成员变量中:





3.4.XMLMapperBuilder



上面mappers标签解析,this.mapperElement(root.evalNode("mappers")),就是获取mybatis-config.xml文件中mappers节点中配置的Mapper.xml的过程,mppers节点中有两种配置方式,一种是配置package,一种是配置resource,针对这两种配置有不同的分支做解析:





这里我们以mybatis-config.xml文件中mappers节点resource配置为例说明UserEntityMapper.xml的解析过程。整个解析使用的是XMLMapperBuilder.parse()方法,下面的parse方法中可以看到UserEntityMapper.xml文件的节点标签:比如父节点mapper、resultMap标签,sql标签, select标签, insert标签。其中子标签的解析也有具体的方法:





其中this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"))方法内部最终会调用Configration的this.mappedStatements.put(ms.getId(), ms),这里mappedStatements是一个HashMap,他的key就是测试类中 session.selectOne(key,params) 的key,比如key="com.wuxiaolong.mybatis.mapper.UserEntityMapper.selectByPrimaryKey";这其实就是将UserEntityMapper.java与UserEntityMapper.xml中的方法映射起来的过程。





3.5.Configration



执行完上面的整个过程,我们最终获得初始化完成的Configration,这个Configration已经持有了mybatis-config.xml和UserEntityMapper.xml这两个配置文件中的所有信息,后续的很多操作也是从这个配置类中获取相应的配置参数的,而且Configuration对象是全局唯一的。可以看出,这个Configration应该是很重的,内部包含了太多的信息。



调用DefaultSqlSessionFactory(Configuration configuration)的构造器,即可获得DefaultSqlSessionFactory。Configuration对象与DefaultSqlSessionFactory是1对1的关系,这也就意味着在一个DefaultSqlSessionFactory衍生出来的所有SqlSession作用域里,Configuration对象是全局唯一的。同时SqlSessionFactory提供了getConfiguration()接口来公开Configuration对象。



3.6.SqlSession



通过上面的代码我们获得了DefaultSqlSessionFactory,通过调用sqlSessionFactory.openSession()就可以获得SqlSession对象。



SqlSession session = sqlSessionFactory.openSession();



openSession方法通过调用openSessionFromDataSource方法获得DefaultSqlSession:





3.7.Executer



在获取SqlSession的同时,也会实例化一个Executer,后面执行sql的时候会用到。这里会根据Executor的类型差别,获取不同的Excutor,默认是SimpleExecutor;这些Executor都继承BaseExecutor或者直接实现Executor接口。 其中BaseExecutor的localCache是一级缓存。而CacheExecutor实现了二级缓存。



MyBatisExecutor,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护。





获取到SqlSession后,在测试类中就可以通过调用session.selectOne("com.wuxiaolong.mybatis.mapper.UserEntityMapper.selectByPrimaryKey", 1)获得主键id=1的用户信息了。



SqlSession是一套操作数据库的接口,包括数据库的CRUD的各种操作,也是mybatis中最核心的一部分内容之一。DefaultSqlSession.selectList()方法查询数据库的。首先通过key(如:com.wuxiaolong.mybatis.mapper.UserEntityMapper.selectByPrimaryKey)到Configration中找到这个sql语句相关的映射对象,然后调用executor.query()执行sql:





进入上面`executor.query()方法内部,我们最终找到了BaseExecutor.query()方法,在这里可以看出,mybatis的BaseExecutor对象是有一个缓存的,缓存在成员变量localCache中;其实,这个localCache就是我们常说的一级缓存。





进入到方法queryFromDatabase内部,最终在SimpleExecutor中找到了doQurey方法。看到这个方法,大家就很熟悉了,这里的Statement对象就是JDBC中使用的对象了,这里就是数据库的相关操作了。





这在了我们同时可以看到StatementHandler,他封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合



3.8.其他对象



处理上面介绍的对象外,还有几个数据库相关的对象也需要注意:



ParameterHandler:负责对用户传递的参数转换成JDBC Statement 所需要的参数,



ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;



TypeHandler:负责java数据类型和jdbc数据类型之间的映射和转换



3.9.设计模式



mybatis源码中,涉及了多种涉及模式,简单介绍几种:



  1. Builder模式:例如SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder;

  2. 工厂模式:例如SqlSessionFactory、MapperProxyFactory;

  3. 代理模式:Mybatis实现的核心,比如MapperProxy,用的是jdk的动态代理;

  4. 模板方法模式:例如BaseExecutor和SimpleExecutor;

  5. 装饰器模式,例如CachingExecutor本身就是一个Executor,而其成员变量delegate还是一个从构造器中初始化的Executor。



3.10.总结



相比较spring的源码,其实看mybatis的源码相对还是简单很多的。



看完源码可能发现其实框架没有我们想象中的那么难,只是封装的好一点,考虑的情况多一点,其本质还是一样的。



看mybatis源码,可以首先关注DefaultSqlSession这个对象。这个对象起到承上启下的作用;说到承上,是指解析mybatis-config.xml和UserEntityMapper.xml这两类配置文件文件的过程,最终的结果就是DefaultSqlSession的成员变量 Configuration;说到启下,就是使用Executor对象访问数据库的过程。所以我们说SqlSession是mybatis中非常重要的。





关注公众号,输入“java-summary”即可获得源码。



完成,收工!





传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。





发布于: 2020 年 08 月 16 日 阅读数: 116
用户头像

诸葛小猿

关注

我是诸葛小猿,一个彷徨中奋斗的互联网民工 2020.07.08 加入

公众号:foolish_man_xl

评论

发布
暂无评论
这样看mybatis,谁都会分析源码!