1. 程式人生 > 實用技巧 >Mybatis原始碼閱讀(二)

Mybatis原始碼閱讀(二)

本文主要介紹Java中,不使用XML和使用XML構建SqlSessionFactory,通過SqlSessionFactory 中獲取SqlSession的方法,使用SqlsessionManager管理Sqlsession複用等等..以及相關的示例程式碼

SqlSession

SqlSessions 是由 SqlSessionFactory 例項建立的。SqlSessionFactory 物件包含建立 SqlSession 例項的各種方法。而 SqlSessionFactory 本身是由 SqlSessionFactoryBuilder 建立的,它可以從 XML、註解或 Java 配置程式碼來建立 SqlSessionFactory。

使用 MyBatis 的主要 Java 介面就是 SqlSession。你可以通過這個介面來執行命令,獲取對映器示例和管理事務。在介紹 SqlSession 介面之前,我們先來了解如何獲取一個 SqlSession 例項。

舉個例子

public class Ttest {
    private Long id;
    private String context;
....
}

TestMapper.java

public interface TestMapper {
    Ttest getOne(Long id);
}

TestMapper.xml

<mapper namespace="com.liangtengyu.mapper.TestMapper">
    <select id="getOne" resultType="com.liangtengyu.entity.Ttest">
		select * from t_test where id  = #{id}
	</select>
</mapper>

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>
    <!--開啟日誌輸出-->
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>
    <!--配置類別名,配置後在Mapper配置檔案(通常我們將編寫SQL語句的配置檔案成為Mapper配置檔案)中需要使用pojo包中的類時,使用簡單類名即可-->
    <typeAliases>
        <package name="com.liangtengyu.entity"/>
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <package name="com.liangtengyu.mapper"/>
    </mappers>

</configuration>

來個測試方法:

   @Test
    public void testMyBatisBuild() throws IOException {
        Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
        SqlSession sqlSession = factory.openSession();
        TestMapper mapper = sqlSession.getMapper(TestMapper.class);
        Ttest one = mapper.getOne(1L);
        System.out.println(one);
        sqlSession.close();
    }

執行測試方法,控制檯列印日誌:

Checking to see if class com.liangtengyu.mapper.TestMapper matches criteria [is assignable to Object]
Opening JDBC Connection 
Created connection 2083117811.   //建立的連線名
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@7c29daf3]
==>  Preparing: select * from t_test where id = ?
==> Parameters: 1(Long)
<==    Columns: id, context
<==        Row: 1, 123



<==      Total: 1
Ttest{id=1, context='123'}
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@7c29daf3]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@7c29daf3]
Returned connection 2083117811 to pool. //用完了又放回連線池中

SqlSessionFactoryBuilder 創建出SqlSessionFactory,然後從SqlSessionFactory中得到SqlSession,最後通過SqlSession得到Mapper介面物件進行資料庫操作。

我們打個斷點.來跟蹤SqlSessionFactoryBuilder的原始碼:


F7跟進 發現一堆build 而我們現在用的是傳入reader的那個方法


我們可以看到,他幫我們傳了2個Null引數給下一個build,我們跟著這個build繼續往下跟.


這個build會將xml解析.然後呼叫parser.parse()方法將xml轉化成Configuration,傳入下一個build
繼續下一個build


這個build終於幹了我們最關心的事,他建立了DefaultSqlSessionFactory 返回SqlSessionFactory.
我們進來看看這個類:

public class DefaultSqlSessionFactory implements SqlSessionFactory {
        //它是SqlSessionFactory的實現類.
  private final Configuration configuration;
        //通過傳入一個Configuration 來構造
  public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
  }
  ...

這樣一看,那我們直接給它來個configuration不就可以建立一個SqlSessionFactory嗎.

我們來試試

 //使用傳入Configuration方式建立SqlSessionFactoryBuilder
    @Test
    public void testMyBatisBuild1() throws IOException {
        DataSource datasource = getDatasource();//首先建立資料來源
        Environment e = new Environment("test", new JdbcTransactionFactory(), datasource);//傳入datasource和JdbcTransactionFactory
        Configuration configuration = new Configuration();//構建一個Configuration
        configuration.setEnvironment(e);
        configuration.setLogImpl(StdOutImpl.class);//使用控制檯輸出日誌實現
        configuration.getTypeAliasRegistry().registerAlias(Ttest.class);
        configuration.addMapper(TestMapper.class);
        //傳入configuration                               
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(configuration);
        SqlSession sqlSession = build.openSession();
        TestMapper mapper = sqlSession.getMapper(TestMapper.class);
        Ttest one = mapper.getOne(1L);
        System.out.println(one);
        sqlSession.close();
    }
    //獲取資料來源方法
    public UnpooledDataSource getDatasource(){
        UnpooledDataSource unpooledDataSource = new UnpooledDataSource();
        unpooledDataSource.setUrl("jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8");
        unpooledDataSource.setDriver("com.mysql.jdbc.Driver");
        unpooledDataSource.setUsername("root");
        unpooledDataSource.setPassword("123456");
        return unpooledDataSource;
    }

執行結果:

Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
Opening JDBC Connection
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3dd4520b]
==>  Preparing: select * from t_test where id = ?
==> Parameters: 1(Long)
<==    Columns: id, context
<==        Row: 1, 123



<==      Total: 1
Ttest{id=1, context='123'}
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3dd4520b]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@3dd4520b]
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(configuration);
//在構造SqlSessionFactory時實際呼叫的還是DefaultSqlSessionFactory 所以我們直接使用
DefaultSqlSessionFactory factory = new DefaultSqlSessionFactory(configuration);
//也是一樣的效果


那麼他的內部是如何建立session的我們來看看原始碼
根據斷點我們到了factory.opensession()方法 F7進入方法

F8單步 發現呼叫了openSessionFromDataSource方法


三個引數.第一個引數是configuration.getDefaultExecutorType()
這個引數是Configuration類中定義的預設型別.

ExecutorType

package org.apache.ibatis.session;
  //還有其它 的型別 如下.
/**
 * @author Clinton Begin
 */
public enum ExecutorType {
  SIMPLE, REUSE, BATCH
}

大家可能對 ExecutorType 引數感到陌生。這個列舉型別定義了三個值:

ExecutorType.SIMPLE:該型別的執行器沒有特別的行為。它為每個語句的執行建立一個新的預處理語句。

ExecutorType.REUSE:該型別的執行器會複用預處理語句。

ExecutorType.BATCH:該型別的執行器會批量執行所有更新語句,如果 SELECT 在多個更新中間執行,將在必要時將多條更新語句分隔開來,以方便理解。這裡不再深入討論

level

是稱為 TransactionIsolationLevel,

事務隔離級別支援 JDBC 的五個隔離級別(NONEREAD_UNCOMMITTEDREAD_COMMITTEDREPEATABLE_READSERIALIZABLE),並且與預期的行為一致。

autoCommit

向 autoCommit 可選引數傳遞 true 值即可開啟自動提交功能


繼續往下 進到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);
      return new DefaultSqlSession(configuration, executor, autoCommit);//返回DefaultSqlSession
    } 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();
    }
  }

返回的DefaultSqlSession實現了SqlSession的所有方法


我們進入到Sqlsession類中檢視一下實現它的都有哪些類


一共有兩個,而且SqlSessionManager還實現了SqlSessionFactory

SqlSessionManager

public class SqlSessionManager implements SqlSessionFactory, SqlSession {

  private final SqlSessionFactory sqlSessionFactory;
  private final SqlSession sqlSessionProxy;//這裡使用了代理,來增強sqlsession

  private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();
  //使用Threadlocal管理本執行緒的sqlsession來複用sqlsession
  private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {
    this.sqlSessionFactory = sqlSessionFactory;
    this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[]{SqlSession.class},
        new SqlSessionInterceptor());
  }
  //這個方法幫我們直接建立了sqlSessionFactory並且將傳入的sqlSessionFactory的SqlSession進行了代理
  public static SqlSessionManager newInstance(Reader reader) {
    return new SqlSessionManager(new SqlSessionFactoryBuilder().build(reader, null, null));
  }
  .....
   public static SqlSessionManager newInstance(Reader reader) {
    return new SqlSessionManager(new SqlSessionFactoryBuilder().build(reader, null, null));
  }

  public static SqlSessionManager newInstance(Reader reader, String environment) {
    return new SqlSessionManager(new SqlSessionFactoryBuilder().build(reader, environment, null));
  }

  public static SqlSessionManager newInstance(Reader reader, Properties properties) {
    return new SqlSessionManager(new SqlSessionFactoryBuilder().build(reader, null, properties));
  }

  public static SqlSessionManager newInstance(InputStream inputStream) {
    return new SqlSessionManager(new SqlSessionFactoryBuilder().build(inputStream, null, null));
  }

  public static SqlSessionManager newInstance(InputStream inputStream, String environment) {
    return new SqlSessionManager(new SqlSessionFactoryBuilder().build(inputStream, environment, null));
  }

  public static SqlSessionManager newInstance(InputStream inputStream, Properties properties) {
    return new SqlSessionManager(new SqlSessionFactoryBuilder().build(inputStream, null, properties));
  }

  public static SqlSessionManager newInstance(SqlSessionFactory sqlSessionFactory) {
    return new SqlSessionManager(sqlSessionFactory);
  }
  
  .....
   @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //使用Threadlocal管理本執行緒的sqlsession來複用sqlsession
      final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();
      if (sqlSession != null) {//獲取本執行緒的sqlsession
        try {
          return method.invoke(sqlSession, args);//實際呼叫
        } catch (Throwable t) {
          throw ExceptionUtil.unwrapThrowable(t);
        }
      } else {
        try (SqlSession autoSqlSession = openSession()) {
          try {
            final Object result = method.invoke(autoSqlSession, args);
            autoSqlSession.commit();
            return result;
          } catch (Throwable t) {
            autoSqlSession.rollback();
            throw ExceptionUtil.unwrapThrowable(t);
          }
        }
      }
    }

SqlSessionManager
他實現了Session介面。意味著,SqlSessionManager集成了 sqlSessionFactory和session 的功能。通過SqlSessionManager,開發者可以不在理會SqlSessionFacotry的存在,直接面向Session程式設計。

SqlSessionManager 內部提供了一個sqlSessionProxy,這個sqlSessionProxy提供了所有Session介面的實現,而實現中正是使用了上面提到的本地執行緒儲存的session例項。

這樣,在同一個執行緒實現不同的sql操作,可以複用本地執行緒session,避免了DefaultSqlSessionFactory實現的每一個sql操作都要建立新的session例項

下面讓我們用一個簡單的例項來試試

@Test
    public void testMyBatisBuild3() throws IOException {
        Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
// SqlSessionFactory build = new SqlSessionFactoryBuilder().build(reader);
// 不使用SqlSessionFactory 使用SqlSessionManager.newInstance();
        SqlSessionManager sqlSessionManager = SqlSessionManager.newInstance(reader);
        SqlSession sqlSession = sqlSessionManager.openSession();
        TestMapper mapper = sqlSession.getMapper(TestMapper.class);
        Ttest one = mapper.getOne(1L);
        System.out.println(one);
        sqlSession.close();
    }

執行結果:

Reader entry: ����1	getOne0(Ljava/lang/Long;)Lcom/liangtengyu/entity/Ttest;
Find JAR URL: file:/Users/tengyu/IdeaProjects/mybatis-study/target/classes/com/liangtengyu/mapper/TestMapper.xml
Not a JAR: file:/Users/tengyu/IdeaProjects/mybatis-study/target/classes/com/liangtengyu/mapper/TestMapper.xml
Reader entry: <?xml version="1.0" encoding="UTF-8"?>
Checking to see if class com.liangtengyu.mapper.TestMapper matches criteria [is assignable to Object]
Opening JDBC Connection
Created connection 1585787493.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@5e853265]
==>  Preparing: select * from t_test where id = ?
==> Parameters: 1(Long)
<==    Columns: id, context
<==        Row: 1, 123



<==      Total: 1
Ttest{id=1, context='123'}
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@5e853265]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@5e853265]
Returned connection 1585787493 to pool.

本章主要講了MyBatis構建SqlSessionFactory方式,過程,和sqlsession的建立,以及使用SqlSessionManager管理session複用的實現方式.

下一篇研究資料來源的池化和資料來源載入過程.

加群一起學習吧

關注公眾號:java寶典