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

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

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

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

关注公众号,输入关键字“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包,来看看项目的结构。

mybatis源码包

其中几个重要的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”即可获得源码。

完成,收工!

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

猜你喜欢

转载自blog.csdn.net/wuxiaolongah/article/details/108036257