MyBatis原始碼解析

林本託發表於2020-11-15

在講解MyBatis之前,先說下傳統JDBC連線資料庫的弊端:
1.JDBC底層沒有實現連線池,從而導致運算元據庫需要頻繁的建立和釋放,影響效能;
2.JDBC的程式碼散落在Java程式碼中,如果需要修改SQL語句,需要重新編譯Java類;
3.使用PreparedStatement設定引數繁,佔位符和引數需要一一對應;
4.處理返回的結果集解析也很麻煩。

所以,在實際開發中,基本不會使用原生的JDBC來運算元據庫。

然後,說一下Hibernate和MyBatis的區別,現在網際網路公司基本上都是以MyBatis為主,因MyBatis能夠書寫比較複雜的SQL語句,比較靈活。

MyBatis的核心概念

類名 描述
Configuration 描述mybatis-config.xml全域性配置關係類
SqlSessionFactory Session管理工廠介面
SqlSession SqlSession介面。 SqlSession中提供了運算元據庫的方法,完成一次資料庫的訪問和結果的對映.不是執行緒安全
Executor 執行器。SqlSession通過執行器運算元據庫,呼叫StatementHandler房屋資料庫,並快取查詢結果
MappedStatement 底層封裝物件。對運算元據庫儲存封裝,包括sql語句、輸入輸出引數
StatementHandler 具體運算元據庫相關的handler介面
ResultSetHandler 具體運算元據庫返回結果的handler介面

下面我們新建一個專案,
首先建立一個表,並插入資料:

CREATE TABLE STUDENTS
(
    stud_id int(11)     NOT NULL AUTO_INCREMENT,
    name    varchar(50) NOT NULL,
    email   varchar(50) NOT NULL,
    dob     date DEFAULT NULL,
    PRIMARY KEY (stud_id)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = UTF8;

insert into students(stud_id, name, email, dob)
values (1, 'Student1', 'student1@gmail.com', '1990-06-25');
insert into students(stud_id, name, email, dob)
values (2, 'Student2', 'student2@gmail.com', '1990-06-25');

新建一個Student類:

package com.mybatis3.domain;

import java.util.Date;

public class Student {
    private Integer studId;
    private String name;
    private String email;
    private Date dob;

    public Student() {

    }

    public Student(Integer studId) {
        this.studId = studId;
    }

    public Student(Integer studId, String name, String email, Date dob) {
        this.studId = studId;
        this.name = name;
        this.email = email;
        this.dob = dob;
    }

    @Override
    public String toString() {
        return "Student [studId=" + studId + ", name=" + name + ", email="
                + email + ", dob=" + dob + "]";
    }
    // todo getter and setter
}

新建一個mapper類:

import java.util.List;

import com.mybatis3.domain.Student;

public interface StudentMapper {
    Student findStudentById(Integer id);
}

新建Service類:

public class StudentService {
    private Logger logger = LoggerFactory.getLogger(getClass());

    public Student findStudentById(Integer studId) {
        logger.debug("Select Student By ID :{}", studId);
        SqlSession sqlSession = MyBatisSqlSessionFactory.getSqlSession();
        try {
            StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
            return studentMapper.findStudentById(studId);
            //return sqlSession.selectOne("com.mybatis3.StudentMapper.findStudentById", studId);
        } finally {
            sqlSession.close();
        }
    }
}

新建一個SqlSessionFactory工具類:

package com.mybatis3.util;

import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Properties;

import org.apache.ibatis.datasource.DataSourceFactory;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class MyBatisSqlSessionFactory {
    private static SqlSessionFactory sqlSessionFactory;

    private static final Properties PROPERTIES = new Properties();

    static {
        try {
            InputStream is = DataSourceFactory.class.getResourceAsStream("/application.properties");
            PROPERTIES.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static SqlSessionFactory getSqlSessionFactory() {
        if (sqlSessionFactory == null) {
            try (InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml")) {
                sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            } catch (IOException e) {
                throw new RuntimeException(e.getCause());
            }
        }
        return sqlSessionFactory;
    }

    public static SqlSession getSqlSession() {
        return getSqlSessionFactory().openSession();
    }

    public static Connection getConnection() {
        String driver = PROPERTIES.getProperty("jdbc.driverClassName");
        String url = PROPERTIES.getProperty("jdbc.url");
        String username = PROPERTIES.getProperty("jdbc.username");
        String password = PROPERTIES.getProperty("jdbc.password");
        Connection connection = null;
        try {
            Class.forName(driver);
            connection = DriverManager.getConnection(url, username, password);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return connection;
    }
}

編寫StudentMapper.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.mybatis3.mappers.StudentMapper">
    <resultMap type="Student" id="StudentResult">
        <id property="studId" column="stud_id"/>
        <result property="name" column="name"/>
        <result property="email" column="email"/>
        <result property="dob" column="dob"/>
    </resultMap>

    <select id="findStudentById" parameterType="int" resultType="Student">
        select stud_id as studId, name, email, dob
        from Students
        where stud_id = #{studId}
    </select>
</mapper>

最後編寫test類:

package com.mybatis3.services;

import java.util.Date;
import java.util.List;

import org.junit.AfterClass;

import static org.junit.Assert.*;

import org.junit.BeforeClass;
import org.junit.Test;

import com.mybatis3.domain.Student;

public class StudentServiceTest {
    private static StudentService studentService;

    @Test
    public void testFindStudentById() {
        Student student = studentService.findStudentById(1);
        assertNotNull(student);
    }
}

整個程式碼結構如下:

現在正式除錯testFindStudentById方法,呼叫到 sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream)方法,
在這裡設定斷點,進入MyBatis的原始碼裡,

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

這行程式碼沒有必要看,接著往下看:

return build(parser.parse());

parse方法點進去:

parseConfiguration(parser.evalNode("/configuration"));

進入到parseConfiguration方法裡面:

這些屬性就是對應mybatis-config.xml裡面的引數,這個方法就是解析xml裡面所有的內容。
點選進入mapperElement方法,這個方法,就是規定了mapper標籤的載入優先順序順序:

如果配置重複,會丟擲異常。

回到上面的bulid方法:

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

可以看到就是返回我們SqlSessionFactory(是個介面)的例項物件,其引數就是我們的Configuration全域性配置類。

上面到此為止告一段落,接下來看下sqlSessionFactory.openSession()方法,點進去:

 public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }

上面的getDefaultExecutorType方法就是返回執行器型別,是個列舉,預設是SIMPLE:

public enum ExecutorType {
  SIMPLE, REUSE, BATCH
}

點進去openSessionFromDataSource方法,這個方法就是獲取環境資訊,建立一個事務,再獲取一個執行器:

 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType, autoCommit);
      return new DefaultSqlSession(configuration, executor);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

點進newExecutor方法:根據條件建立不同的執行器。
此方法裡有個非常重要的一段程式碼:

 if (cacheEnabled) {
      executor = new CachingExecutor(executor, autoCommit);
    }

CachingExecutor是建立一級快取的執行器。

到目前為止,已經拿到SqlSession,並對SimpleExecutor執行器進行初始化。

第三步:執行我們編寫的return sqlSession.selectOne("com.mybatis3.mappers.StudentMapper.findStudentById", studId);方法。

點進去selectOne,selectOne會呼叫DefaultSqlSession.selectList方法,點進去:

看下statement的引數值,就是StudentMapper.xml裡 mapper標籤名稱 + select標籤id的名稱,所以這裡的id是要唯一的。

這裡出現了一個新的核心類MappedStatement,對應的StudentMapper.xml裡的select,update,insert,delete語句。
下面程式碼:

      List<E> result = executor.<E>query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

進入query方法:

 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

getBoundSql就是繫結sql語句的引數,具體值如下:

createCacheKey方法建立快取key,點進去,一直找到createCacheKey方法,包括快取引數的更新,給sql建立了一個快取,快取可以是由id + sql + limit + offset組成,具有相同的key會做快取。

query方法會先拿到快取,快取如果不為空的話,判斷是否要清空快取,如果使用快取,且不是髒資料,則使用讀寫鎖進行加鎖,從不同的快取策略中獲取快取列表;
快取為空的話,會查詢資料庫,
進入 List<E> list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);方法,
在真正查詢資料庫之前,會再次判斷已經存在一級快取,如果沒有,執行queryFromDatabase方法,執行裡面的doQuery方法:

 public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

這裡又出現了一個核心類StatementHandler,這個介面是設計具體資料庫的操作。 進入 return handler.<E>query(stmt, resultHandler);方法:

 public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.<E> handleResultSets(ps);
  }

這裡出現的JDBC的PreparedStatement 類,不用解釋了吧,已經調到Java JDBC的這層API了。
進入到handleResultSets方法,執行此方法,就已經返回資料集的結果了。如下圖:

現在總結一下:

至此,基本上就是整個MyBatis查詢資料的一個過程。

相關文章