Bootstrap

这样看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包
 *         
 *             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,都离不开这两类配置。

这是最直接的连接方式,通过驱动直接连接数据库,只需要集成这一个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:
 *         
 *             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方法。整个执行过程大概有三步:

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

mybatis-config.xml





    
    
        
            
            
                
                
                
                
                
            
        
    


    
    
        
        

        
        
    


这个配置文件中的信息包括了上面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





  
    
    
    
    
    
  

  
    id, username, passwd, age, address
  


  

  
    insert into user
    
      
        id,
      
      
        username,
      
      
        passwd,
      
      
        age,
      
      
        address,
      
    
    
      
        #{id,jdbcType=INTEGER},
      
      
        #{username,jdbcType=VARCHAR},
      
      
        #{passwd,jdbcType=VARCHAR},
      
      
        #{age,jdbcType=INTEGER},
      
      
        #{address,jdbcType=VARCHAR},
      
    
  

这是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标签解析, 就是获取mybatis-config.xml文件中数据库连接参数的过程。可以看出这里,这里生成了TransactionFactory、DataSourceFactory、DataSource等信息,并将信息存储在Configuration.environment成员变量中:

3.4.XMLMapperBuilder

上面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标签。其中子标签的解析也有具体的方法:

其中方法内部最终会调用Configration的,这里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后,在测试类中就可以通过调用获得主键id=1的用户信息了。

SqlSession是一套操作数据库的接口,包括数据库的CRUD的各种操作,也是mybatis中最核心的一部分内容之一。DefaultSqlSession.selectList()方法查询数据库的。首先通过key(如:com.wuxiaolong.mybatis.mapper.UserEntityMapper.selectByPrimaryKey)到Configration中找到这个sql语句相关的映射对象,然后调用执行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源码中,涉及了多种涉及模式,简单介绍几种:

3.10.总结

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

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

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

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

完成,收工!

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