深入Mybatis框架

深入Mybatis框架

學習了Spring之後,我們已經了解如何將一個類作為Bean交由IoC容器管理,也就是說,現在我們可以通過更方便的方式來使用Mybatis框架,我們可以直接把SqlSessionFactory、Mapper交給Spring進行管理,並且可以通過注入的方式快速地使用它們。

因此,我們要學習一下如何將Mybatis與Spring進行整合,那麼首先,我們需要在之前知識的基礎上繼續深化學習。

了解數據源

在之前,我們如果需要創建一個JDBC的連接,那麼必須使用DriverManager.getConnection()來創建連接,連接建立後,我們才可以進行數據庫操作。

而學習了Mybatis之後,我們就不用再去使用DriverManager為我們提供連接對象,而是直接使用Mybatis為我們提供的SqlSessionFactory工具類來獲取對應的SqlSession通過會話對象去操作數據庫。

那麼,它到底是如何封裝JDBC的呢?我們可以試着來猜想一下,會不會是Mybatis每次都是幫助我們調用DriverManager來實現的數據庫連接創建?我們可以看看Mybatis的源碼:

public SqlSession openSession(boolean autoCommit) {
    return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, autoCommit);
}

在通過SqlSessionFactory調用openSession方法之後,它調用了內部的一個私有的方法openSessionFromDataSource,我們接着來看,這個方法裏面定義了什麼內容:

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;

    DefaultSqlSession var8;
    try {
      	//獲取當前環境(由配置文件映射的對象實體)
        Environment environment = this.configuration.getEnvironment();
      	//事務工廠(暫時不提,下一板塊講解)
        TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
      	//配置文件中:<transactionManager type="JDBC"/>
      	//生成事務(根據我們的配置,會默認生成JdbcTransaction),這裡是關鍵,我們看到這裡用到了environment.getDataSource()方法
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      	//執行器,包括全部的數據庫操作方法定義,本質上是在使用執行器操作數據庫,需要傳入事務對象
        Executor executor = this.configuration.newExecutor(tx, execType);
      	//封裝為SqlSession對象
        var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
    } catch (Exception var12) {
        this.closeTransaction(tx);
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);
    } finally {
        ErrorContext.instance().reset();
    }
		
    return var8;
}

也就是說,我們的數據源配置信息,存放在了Transaction對象中,那麼現在我們只需要知道執行器到底是如何執行SQL語句的,我們就知道到底如何創建Connection對象了,就需要獲取數據庫的鏈接信息了,那麼我們來看看,這個DataSource到底是個什麼:

public interface DataSource  extends CommonDataSource, Wrapper {

  Connection getConnection() throws SQLException;

  Connection getConnection(String username, String password)
    throws SQLException;
}

我們發現,它是在javax.sql定義的一個接口,它包括了兩個方法,都是用於獲取連接的。因此,現在我們可以斷定,並不是通過之前DriverManager的方法去獲取連接了,而是使用DataSource的實現類來獲取的,因此,也就正式引入到我們這一節的話題了:

數據庫鏈接的建立和關閉是極其耗費系統資源的操作,通過DriverManager獲取的數據庫連接,

一個數據庫連接對象均對應一個物理數據庫連接,每次操作都打開一個物理連接,使用完後立即關閉連接,頻繁的打開、關閉連接會持續消耗網絡資源,造成整個系統性能的低下。

因此,JDBC為我們定義了一個數據源的標準,也就是DataSource接口,告訴數據源數據庫的連接信息,並將所有的連接全部交給數據源進行集中管理,當需要一個Connection對象時,可以向數據源申請,數據源會根據內部機制,合理地分配連接對象給我們。

一般比較常用的DataSource實現,都是採用池化技術,就是在一開始就創建好N個連接,這樣之後使用就無需再次進行連接,而是直接使用現成的Connection對象進行數據庫操作。

點擊查看源網頁

當然,也可以使用傳統的即用即連的方式獲取Connection對象,Mybatis為我們提供了幾個默認的數據源實現,我們之前一直在使用的是官方的默認配置,也就是池化數據源:

<dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>

一共三個選項:

  • UNPOOLED 不使用連接池的數據源
  • POOLED 使用連接池的數據源
  • JNDI 使用JNDI實現的數據源

解讀Mybatis數據源實現

那麼我們先來看看,不使用池化的數據源實現,它叫做UnpooledDataSource,我們來看看源碼:

public class UnpooledDataSource implements DataSource {
    private ClassLoader driverClassLoader;
    private Properties driverProperties;
    private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap();
    private String driver;
    private String url;
    private String username;
    private String password;
    private Boolean autoCommit;
    private Integer defaultTransactionIsolationLevel;
    private Integer defaultNetworkTimeout;

首先這個類中定義了很多的成員,包括數據庫的連接信息、數據庫驅動信息、事務相關信息等。

我們接着來看,它是如何實現DataSource中提供的接口的:

public Connection getConnection() throws SQLException {
    return this.doGetConnection(this.username, this.password);
}

public Connection getConnection(String username, String password) throws SQLException {
    return this.doGetConnection(username, password);
}

實際上,這兩個方法都指向了內部的一個doGetConnection方法,那麼我們接着來看:

private Connection doGetConnection(String username, String password) throws SQLException {
    Properties props = new Properties();
    if (this.driverProperties != null) {
        props.putAll(this.driverProperties);
    }

    if (username != null) {
        props.setProperty("user", username);
    }

    if (password != null) {
        props.setProperty("password", password);
    }

    return this.doGetConnection(props);
}

首先它將數據庫的連接信息也給添加到Properties對象中進行存放,並交給下一個doGetConnection來處理,套娃就完事了唄,接着來看下一層源碼:

private Connection doGetConnection(Properties properties) throws SQLException {
  	//若未初始化驅動,需要先初始化,內部維護了一個Map來記錄初始化信息,這裡不多介紹了
    this.initializeDriver();
  	//傳統的獲取連接的方式
    Connection connection = DriverManager.getConnection(this.url, properties);
  	//對連接進行額外的一些配置
    this.configureConnection(connection);
    return connection;
}

到這裡,就返回Connection對象了,而此對象正是通過DriverManager來創建的,因此,非池化的數據源實現依然使用的是傳統的連接創建方式,那我們接着來看池化的數據源實現,它是PooledDataSource類:

public class PooledDataSource implements DataSource {
    private static final Log log = LogFactory.getLog(PooledDataSource.class);
    private final PoolState state = new PoolState(this);
    private final UnpooledDataSource dataSource;
    protected int poolMaximumActiveConnections = 10;
    protected int poolMaximumIdleConnections = 5;
    protected int poolMaximumCheckoutTime = 20000;
    protected int poolTimeToWait = 20000;
    protected int poolMaximumLocalBadConnectionTolerance = 3;
    protected String poolPingQuery = "NO PING QUERY SET";
    protected boolean poolPingEnabled;
    protected int poolPingConnectionsNotUsedFor;
    private int expectedConnectionTypeCode;

我們發現,在這裡的定義就比非池化的實現複雜得多了,因為它還要考慮並發的問題,並且還要考慮如何合理地存放大量的鏈接對象,該如何進行合理分配,因此它的玩法非常之高級。

首先注意,它存放了一個UnpooledDataSource,此對象是在構造時就被創建,其實創建Connection還是依靠數據庫驅動創建,我們後面慢慢解析,首先我們來看看它是如何實現接口方法的:

public Connection getConnection() throws SQLException {
    return this.popConnection(this.dataSource.getUsername(), this.dataSource.getPassword()).getProxyConnection();
}

public Connection getConnection(String username, String password) throws SQLException {
    return this.popConnection(username, password).getProxyConnection();
}

可以看到,它調用了popConnection()方法來獲取連接對象,然後進行了一個代理,我們可以猜測,有可能整個連接池就是一個類似於棧的集合類型結構實現的。那麼我們接着來看看popConnection方法:

private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
  	//返回的是PooledConnection對象,
    PooledConnection conn = null;
    long t = System.currentTimeMillis();
    int localBadConnectionCount = 0;

    while(conn == null) {
        synchronized(this.state) {   //加鎖,因為有可能很多個線程都需要獲取連接對象
            PoolState var10000;
          	//PoolState存了兩個List,一個是空閑列表,一個是活躍列表
            if (!this.state.idleConnections.isEmpty()) {   //有空閑連接時,可以直接分配Connection
                conn = (PooledConnection)this.state.idleConnections.remove(0);  //ArrayList中取第一個元素
                if (log.isDebugEnabled()) {
                    log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
                }
              //如果已經沒有多餘的連接可以分配,那麼就檢查一下活躍連接數是否達到最大的分配上限,如果沒有,就new一個
            } else if (this.state.activeConnections.size() < this.poolMaximumActiveConnections) {
              	//注意new了之後並沒有立即往List裏面塞,只是存了一些基本信息
              	//我們發現,這裡依靠UnpooledDataSource創建了一個Connection對象,並將其封裝到PooledConnection中
                conn = new PooledConnection(this.dataSource.getConnection(), this);
                if (log.isDebugEnabled()) {
                    log.debug("Created connection " + conn.getRealHashCode() + ".");
                }
              //以上條件都不滿足,那麼只能從之前的連接中尋找了,看看有沒有那種卡住的鏈接(由於網絡問題有可能之前的連接一直被卡住,然而正常情況下早就結束並且可以使用了,所以這裡相當於是優化也算是一種撿漏的方式)
            } else {
              	//獲取最早創建的連接
                PooledConnection oldestActiveConnection = (PooledConnection)this.state.activeConnections.get(0);
                long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
              	//判斷是否超過最大的使用時間
                if (longestCheckoutTime > (long)this.poolMaximumCheckoutTime) {
                  	//超時統計信息(不重要)
                    ++this.state.claimedOverdueConnectionCount;
                    var10000 = this.state;
                    var10000.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
                    var10000 = this.state;
                    var10000.accumulatedCheckoutTime += longestCheckoutTime;
                  	//從活躍列表中移除此鏈接信息
                    this.state.activeConnections.remove(oldestActiveConnection);
                  	//如果開啟事務,還需要回滾一下
                    if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                        try {
                            oldestActiveConnection.getRealConnection().rollback();
                        } catch (SQLException var15) {
                            log.debug("Bad connection. Could not roll back");
                        }
                    }
										
                  	//這裡就根據之前的連接對象直接new一個新的連接(注意使用的還是之前的Connection對象,只是被重新封裝了)
                    conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
                    conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
                    conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
                  	//過期
                    oldestActiveConnection.invalidate();
                    if (log.isDebugEnabled()) {
                        log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
                    }
                } else {
                  //確實是沒得用了,只能卡住了(阻塞)
                  //然後記錄一下有幾個線程在等待當前的任務搞完
                    try {
                        if (!countedWait) {
                            ++this.state.hadToWaitCount;
                            countedWait = true;
                        }

                        if (log.isDebugEnabled()) {
                            log.debug("Waiting as long as " + this.poolTimeToWait + " milliseconds for connection.");
                        }

                        long wt = System.currentTimeMillis();
                        this.state.wait((long)this.poolTimeToWait);   //要是超過等待時間還是沒等到,只能放棄
                      	//注意這樣的話con就為null了
                        var10000 = this.state;
                        var10000.accumulatedWaitTime += System.currentTimeMillis() - wt;
                    } catch (InterruptedException var16) {
                        break;
                    }
                }
            }
						
          	//經過之前的操作,已經成功分配到連接對象的情況下
            if (conn != null) {
                if (conn.isValid()) {  //是否有效
                    if (!conn.getRealConnection().getAutoCommit()) {  //清理之前遺留的事務操作
                        conn.getRealConnection().rollback();
                    }

                    conn.setConnectionTypeCode(this.assembleConnectionTypeCode(this.dataSource.getUrl(), username, password));
                    conn.setCheckoutTimestamp(System.currentTimeMillis());
                    conn.setLastUsedTimestamp(System.currentTimeMillis());
                  	//添加到活躍表中
                    this.state.activeConnections.add(conn);
                    //統計信息(不重要)
                    ++this.state.requestCount;
                    var10000 = this.state;
                    var10000.accumulatedRequestTime += System.currentTimeMillis() - t;
                } else {
                  	//無效的連接,直接拋異常
                    if (log.isDebugEnabled()) {
                        log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
                    }

                    ++this.state.badConnectionCount;
                    ++localBadConnectionCount;
                    conn = null;
                    if (localBadConnectionCount > this.poolMaximumIdleConnections + this.poolMaximumLocalBadConnectionTolerance) {
                        if (log.isDebugEnabled()) {
                            log.debug("PooledDataSource: Could not get a good connection to the database.");
                        }

                        throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
                    }
                }
            }
        }
    }
	
  	//最後該幹嘛幹嘛,拿不到連接直接拋異常
    if (conn == null) {
        if (log.isDebugEnabled()) {
            log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
        }

        throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    } else {
        return conn;
    }
}

經過上面一頓猛如虎的操作之後,我們可以得到以下信息:

如果最後得到了連接對象(有可能是從空閑列表中得到,有可能是直接創建的新的,還有可能是經過回收策略回收得到的)。

那麼連接(Connection)對象一定會被放在活躍列表中(state.activeConnections)

那麼肯定有一個疑問,現在我們已經知道獲取一個鏈接會直接進入到活躍列表中,那麼,如果一個連接被關閉,又會發生什麼事情呢,我們來看看此方法返回之後,會調用getProxyConnection來獲取一個代理對象,實際上就是PooledConnection類:

class PooledConnection implements InvocationHandler {
  private static final String CLOSE = "close";
    private static final Class<?>[] IFACES = new Class[]{Connection.class};
    private final int hashCode;
  	//會記錄是來自哪一個數據源創建的的
    private final PooledDataSource dataSource;
  	//連接對象本體
    private final Connection realConnection;
  	//代理的鏈接對象
    private final Connection proxyConnection;
  ...

它直接代理了構造方法中傳入的Connection對象,也是使用JDK的動態代理實現的,那麼我們來看一下,它是如何進行代理的:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
  	//如果調用的是Connection對象的close方法,
    if ("close".equals(methodName)) {
      	//這裡並不會真的關閉連接(這也是為什麼用代理),而是調用之前數據源的pushConnection方法,將此連接改為為空閑狀態
        this.dataSource.pushConnection(this);
        return null;
    } else {
        try {
            if (!Object.class.equals(method.getDeclaringClass())) {
                this.checkConnection();
              	//任何操作執行之前都會檢查連接是否可用
            }

          	//該幹嘛幹嘛
            return method.invoke(this.realConnection, args);
        } catch (Throwable var6) {
            throw ExceptionUtil.unwrapThrowable(var6);
        }
    }
}

那麼我們最後再來看看pushConnection方法:

protected void pushConnection(PooledConnection conn) throws SQLException {
    synchronized(this.state) {   //老規矩,先來把鎖
      	//先從活躍列表移除此連接
        this.state.activeConnections.remove(conn);
      	//判斷此鏈接是否可用
        if (conn.isValid()) {
            PoolState var10000;
          	//看看閑置列表容量是否已滿(容量滿了就回不去了)
            if (this.state.idleConnections.size() < this.poolMaximumIdleConnections && conn.getConnectionTypeCode() == this.expectedConnectionTypeCode) {
                var10000 = this.state;
                var10000.accumulatedCheckoutTime += conn.getCheckoutTime();
                if (!conn.getRealConnection().getAutoCommit()) {
                    conn.getRealConnection().rollback();
                }

              	//把唯一有用的Connection對象拿出來,然後重新創建一個PooledConnection
                PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
              	//放入閑置列表,成功回收
                this.state.idleConnections.add(newConn);
                newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
                newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
                conn.invalidate();
                if (log.isDebugEnabled()) {
                    log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
                }

                this.state.notifyAll();
            } else {
                var10000 = this.state;
                var10000.accumulatedCheckoutTime += conn.getCheckoutTime();
                if (!conn.getRealConnection().getAutoCommit()) {
                    conn.getRealConnection().rollback();
                }

                conn.getRealConnection().close();
                if (log.isDebugEnabled()) {
                    log.debug("Closed connection " + conn.getRealHashCode() + ".");
                }

                conn.invalidate();
            }
        } else {
            if (log.isDebugEnabled()) {
                log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
            }

            ++this.state.badConnectionCount;
        }

    }
}

這樣,我們就已經完全了解了Mybatis的池化數據源的執行流程了。

只不過,無論Connection管理方式如何變換,無論數據源再高級,我們要知道,它都最終都會使用DriverManager來創建連接對象,而最終使用的也是DriverManager提供的Connection對象。

整合Mybatis框架

通過了解數據源,我們已經清楚,Mybatis實際上是在使用自己編寫的數據源(數據源有很多,之後我們再聊其他的)默認使用的是池化的數據源,它預先存儲了很多的連接對象。

那麼我們來看一下,如何將Mybatis與Spring更好的結合呢,比如我們現在希望將SqlSessionFactory交給IoC容器進行管理,而不是我們自己創建工具類來管理(我們之前一直都在使用工具類管理和創建會話)

首先導入依賴:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.25</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.7</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.0.6</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.3.13</version>
</dependency>

在mybatis-spring依賴中,為我們提供了SqlSessionTemplate類,它其實就是官方封裝的一個工具類,我們可以將其註冊為Bean,這樣我們隨時都可以向IoC容器索要,而不用自己再去編寫一個工具類了,我們可以直接在配置類中創建:

@Configuration
@ComponentScan("com.test")
public class TestConfiguration {
    @Bean
    public SqlSessionTemplate sqlSessionTemplate() throws IOException {
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config.xml"));
        return new SqlSessionTemplate(factory);
    }
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "//mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/study"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
  	<mappers>
        <mapper class="com.test.mapper.TestMapper"/>
    </mappers>
</configuration>
public static void main(String[] args) {
    ApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class);
    SqlSessionTemplate template = context.getBean(SqlSessionTemplate.class);
    TestMapper testMapper = template.getMapper(TestMapper.class);
    System.out.println(testMapper.getStudent());
}
@Mapper
public interface TestMapper {

    @Select("select * from student where sid = 1")
    Student getStudent();
}
@Data
public class Student {
    int sid;
    String name;
    String sex;
}

最後成功得到Student實體類,證明SqlSessionTemplate成功註冊為Bean可以使用了。

雖然這樣已經很方便了,但是還不夠方便,我們依然需要手動去獲取Mapper對象,那麼能否直接得到對應的Mapper對象呢,我們希望讓Spring直接幫助我們管理所有的Mapper,當需要時,可以直接從容器中獲取,我們可以直接在配置類上方添加註解:

@MapperScan("com.test.mapper")

這樣,Spring會自動掃描所有的Mapper,並將其實現註冊為Bean,那麼我們現在就可以直接通過容器獲取了:

public static void main(String[] args) throws InterruptedException {
    ApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class);
    TestMapper mapper = context.getBean(TestMapper.class);
    System.out.println(mapper.getStudent());
}

請一定注意,必須存在SqlSessionTemplate或是SqlSessionFactoryBean的Bean,否則會無法初始化(畢竟要數據庫的鏈接信息)

我們接着來看,如果我們希望直接去除Mybatis的配置文件,那麼改怎麼去實現呢?

我們可以使用SqlSessionFactoryBean類:

@Configuration
@ComponentScan("com.test")
@MapperScan("com.test.mapper")
public class TestConfiguration {
    @Bean
    public DataSource dataSource(){
        return new PooledDataSource("com.mysql.cj.jdbc.Driver",
                "jdbc:mysql://localhost:3306/study", "root", "123456");
    }

    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(@Autowired DataSource dataSource){
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        return bean;
    }
}

首先我們需要創建一個數據源的實現類,因為這是數據庫最基本的信息,然後再給到SqlSessionFactoryBean實例,這樣,我們相當於直接在一開始通過IoC容器配置了SqlSessionFactory,只需要傳入一個DataSource的實現即可。

刪除配置文件,重新再來運行,同樣可以正常使用Mapper。

從這裡開始,通過IoC容器,Mybatis已經不再需要使用配置文件了,之後基於Spring的開發將不會再出現Mybatis的配置文件。