java程序员访问数据库的方式有很多种,为了简化开发,都会选择使用框架访问数据库,而mybatis是我们常用的一种操作数据库的框架。
本期我们通过展示实际测试结果,带领大家分析mybatis的源码,提升对框架的理解。
本文相关的分析参照资料来源:
mybatis官方网站
mybatis源码包:版本号3.4.1
关注公众号,输入关键字“java-summary”,即可获得源码。
访问数据库的方式有多种,可以使用原生的JDBC,也可以使用spring+mybatis,还可以使用springboot+mybatis。这几种方式一个比一个简单,需要的配置也是一个比一个少,但原理都是通用的。最底层也还是我们熟悉的JDBC。
万变不离其宗,下面我们对照JDBC使用方式,分析mybatis源码。
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语句
package com.wuxiaolong.mybatis;
import java.sql.*;
/**
* Description: JDBC测试
*
* 只需要一个mysql的驱动jar包
*
* mysql
* mysql-connector-java
* 5.1.44
*
*
* @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包即可。
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:
*
* org.mybatis
* mybatis
* 3.4.1
*
*
* mysql
* mysql-connector-java
* 5.1.44
*
* @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方法。整个执行过程大概有三步:
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml")
加载配置文件中的信息,然后生成SqlSessionFactory。session.selectOne()
,第二种是使用UserEntityMapper.selectByPrimaryKey()
。这种方式我们使用了两个jar包,几个是驱动mysql-connector-java
,一个是mybatis框架mybatis
。使用mybatis框架,将上面JDBC的相关对象进行了封装,这里虽然我们只看到了SqlSession
,其实底层还是JDBC。
<configuration>
<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>
<mappers>
<mapper resource="mapper/UserEntityMapper.xml"/>
mappers>
configuration>
这个配置文件中的信息包括了上面JDBC中说的两类配置:数据库连接配置、sql语句配置。这里只是通过xml文件的形式进行集中配置的。其实本质都是一样的。
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执行的第二种方式。
<?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文件对应。
首先,打开mybatis.jar包,来看看项目的结构。
其中几个重要的package的简单说明:
看源码时,可以通过debug上面的MybatisTest.java一步一步往下看。这里是MybatisTest.java中SqlSessionFactory的生成这一步的代码:
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
获得SqlSessionFactory的目的是为了下面获得核心的SqlSession,从而访问数据库。获得SqlSessionFactory的过程就是解析mybatis-config.xml和UserEntityMapper.xml,并生成可重复使用的Configuration的过程。
通过debug模式,进入到这个SqlSessionFactoryBuilder().build()方法中,找到核心代码:
上面出现了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成员变量中:
上面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中的方法映射起来的过程。
执行完上面的整个过程,我们最终获得初始化完成的Configration,这个Configration已经持有了mybatis-config.xml和UserEntityMapper.xml这两个配置文件中的所有信息,后续的很多操作也是从这个配置类中获取相应的配置参数的,而且Configuration对象是全局唯一的。可以看出,这个Configration应该是很重的,内部包含了太多的信息。
调用DefaultSqlSessionFactory(Configuration configuration)的构造器,即可获得DefaultSqlSessionFactory。Configuration对象与DefaultSqlSessionFactory是1对1的关系,这也就意味着在一个DefaultSqlSessionFactory衍生出来的所有SqlSession作用域里,Configuration对象是全局唯一的。同时SqlSessionFactory提供了getConfiguration()接口来公开Configuration对象。
通过上面的代码我们获得了DefaultSqlSessionFactory,通过调用sqlSessionFactory.openSession()就可以获得SqlSession对象。
SqlSession session = sqlSessionFactory.openSession();
openSession方法通过调用openSessionFromDataSource方法获得DefaultSqlSession:
在获取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集合
处理上面介绍的对象外,还有几个数据库相关的对象也需要注意:
ParameterHandler:负责对用户传递的参数转换成JDBC Statement 所需要的参数,
ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
TypeHandler:负责java数据类型和jdbc数据类型之间的映射和转换
mybatis源码中,涉及了多种涉及模式,简单介绍几种:
相比较spring的源码,其实看mybatis的源码相对还是简单很多的。
看完源码可能发现其实框架没有我们想象中的那么难,只是封装的好一点,考虑的情况多一点,其本质还是一样的。
看mybatis源码,可以首先关注DefaultSqlSession这个对象。这个对象起到承上启下的作用;说到承上,是指解析mybatis-config.xml和UserEntityMapper.xml这两类配置文件文件的过程,最终的结果就是DefaultSqlSession的成员变量 Configuration;说到启下,就是使用Executor对象访问数据库的过程。所以我们说SqlSession是mybatis中非常重要的。
关注公众号,输入“java-summary”即可获得源码。
完成,收工!
【传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。