主頁 > 後端開發 > Mybatis原始碼初探——優雅精良的骨架

Mybatis原始碼初探——優雅精良的骨架

2020-10-03 04:19:00 後端開發

@

目錄
  • 前言
  • 精良的Mybatis骨架
    • 宏觀設計
    • 基礎支撐
      • 日志
        • 日志的加載
        • 日志的使用
      • 資料源
        • 資料源的創建
        • 池化技術原理
          • 資料結構
          • 獲取連接
          • 回收連接
      • 快取
        • 快取的實作
        • CacheKey
      • 反射
  • 總結

前言

Mybatis是一款半自動的ORM框架,是目前國內Java web開發的主流ORM框架,因此作為一名開發者非常有必要掌握其實作原理,才能更好的解決我們開發中遇到的問題;同時,Mybatis的架構和原始碼也是很優雅的,使用了大量的設計模式實作解耦以及高擴展性,所以對其設計思想,我們也非常有必要好好理解掌握,(PS:本系列文章基于3.5.0版本分析)

精良的Mybatis骨架

宏觀設計

Mybatsi的原始碼相較于Spring原始碼無論是架構還是實作都簡單了很多,它所有的代碼都在一個工程里面,在這個工程下分了很多包,每個包分工都很明確:
在這里插入圖片描述
別看模塊有這么多,實際上只需要分為三層:
在這里插入圖片描述
這樣分層后,是不是就很清晰了,基礎支撐層是一些通用組件的封裝,如日志、快取、反射、資料源等等,這些模塊支撐著核心業務邏輯的實作,并且如果需要我們可以將其直接用于到我們專案中,像反射模塊就是對JDK的反射進行了封裝,使其更加方便易用;核心處理層就是Mybatis的核心業務的實作了,通過底層支撐模塊,實作了組態檔和SQL決議、引數映射和系結、SQL執行和回傳結果的映射以及擴展插件的執行等等;最后介面層則是對外提供的服務,我們使用Mybatis時只需要通過該介面進行操作,對底層的實作無需關注,這樣分層的好處不用多說,讓我們的代碼更加簡潔易讀,同時可維護性和可擴展性也大大提高,另外從整個架構設計中我們可以看到一個設計模式的體現——門面模式,因為門面模式的設計思想就是對外提供一個統一的的介面,屏蔽掉內部系統實作的復雜性,使得用戶無需關注內部實作就能輕松使用所有功能,而這里的架構設計就是采用的這樣一個思想,舉一反三,再想想看其它的開源框架是不是都是這樣的設計?

基礎支撐

在了解了Mybatis的宏觀架構設計后,下面就是對原始碼的詳細分析,首先先來看幾個重點的基礎支撐模塊:

  • 日志
  • 資料源
  • 快取
  • 反射

日志

日志的加載

Mybatis本身是沒有實作日志功能的,而是引入第三方日志,但第三方日志都有自己的log級別,Mybatis需要解決的就是如何兼容這些日志組件,如何兼容呢?Mybatis使用了配接器模式來解決,在logging模塊下提供了一個統一的日志介面Log介面:

public interface Log {

  boolean isDebugEnabled();

  boolean isTraceEnabled();

  void error(String s, Throwable e);

  void error(String s);

  void debug(String s);

  void trace(String s);

  void warn(String s);

}

可以看到在這個介面中統一定義了各個日志級別,引入的第三方日志組件只需要實作該介面,在各個級別介面中呼叫各組件自身對應的API即可,從下面的類圖我們可以看到Mybatis支持了哪些三方日志組件:在這里插入圖片描述
看到這里你是否會有疑問,這些第三方日志組件是怎么加載的?加載順序又是怎樣的呢?難道是在需要用的地方才實體化么?當然不是,Mybatis這里又使用了一個設計模式——工廠模式,在日志模塊下有一個類LogFactory,日志的加載就是由該類實作的,通過這個類解耦了日志的實體化和日志的使用:

public final class LogFactory {

  public static final String MARKER = "MYBATIS";

  //被選定的第三方日志組件配接器的構造方法
  private static Constructor<? extends Log> logConstructor;

  //自動掃描日志實作,并且第三方日志插件加載優先級如下:slf4J → commonsLoging → Log4J2 → Log4J → JdkLog
  static {
    tryImplementation(LogFactory::useSlf4jLogging);
    tryImplementation(LogFactory::useCommonsLogging);
    tryImplementation(LogFactory::useLog4J2Logging);
    tryImplementation(LogFactory::useLog4JLogging);
    tryImplementation(LogFactory::useJdkLogging);
    tryImplementation(LogFactory::useNoLogging);
  }

  private LogFactory() {
    // disable construction
  }

  public static Log getLog(Class<?> aClass) {
    return getLog(aClass.getName());
  }

  public static Log getLog(String logger) {
    try {
      return logConstructor.newInstance(logger);
    } catch (Throwable t) {
      throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
  }

  public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
    setImplementation(clazz);
  }

  public static synchronized void useSlf4jLogging() {
    setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
  }

  public static synchronized void useCommonsLogging() {
    setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
  }

  public static synchronized void useLog4JLogging() {
    setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
  }

  public static synchronized void useLog4J2Logging() {
    setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
  }

  public static synchronized void useJdkLogging() {
    setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
  }

  public static synchronized void useStdOutLogging() {
    setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
  }

  public static synchronized void useNoLogging() {
    setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
  }

  
  private static void tryImplementation(Runnable runnable) {
    if (logConstructor == null) {//當構造方法不為空才執行方法
      try {
        runnable.run();
      } catch (Throwable t) {
        // ignore
      }
    }
  }
  //通過指定的log類來初始化構造方法
  private static void setImplementation(Class<? extends Log> implClass) {
    try {
      Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
      Log log = candidate.newInstance(LogFactory.class.getName());
      if (log.isDebugEnabled()) {
        log.debug("Logging initialized using '" + implClass + "' adapter.");
      }
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  }

}

通過上面的代碼我們可以清楚的看到日志的加載順序是怎樣的,并且只要加載成功了任何一個日志組件,其它的日志組件就不會被加載,

日志的使用

日志加載完成后,自然而然的我們就該思考的是哪些地方需要列印日志?Mybatis本身是對JDK原生的JDBC的包裝和增強,所以在以下幾個關鍵地方都應該列印日志:

  • 創建PreparedStatement和Statement時列印SQL陳述句和引數資訊
  • 獲取到查詢結果后列印結果資訊

問題是應該怎么優雅地增強這些方法呢?Mybatis使用了動態代理來實作,在日志模塊下的JDBC包就是代理類的實作,先來看看類圖:
在這里插入圖片描述
見名知義,看到這些類名我們應該就能清楚這些類的作用,它們就是對原生的JDBC API的增強,在呼叫相關的方法時,首先會進入到這些代理類的invoke方法里面,按照執行順序,首先進入呼叫的肯定是ConnectionLogger

public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {

 //真正的連接物件
  private final Connection connection;

  private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
    super(statementLog, queryStack);
    this.connection = conn;
  }

  @Override
  //對連接的增強
  public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {
    	//如果是從Obeject繼承的方法直接忽略
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      //如果是呼叫prepareStatement、prepareCall、createStatement的方法,列印要執行的sql陳述句
      //并回傳prepareStatement的代理物件,讓prepareStatement也具備日志能力,列印引數
      if ("prepareStatement".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);//列印sql陳述句
        }        
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);//創建代理物件
        return stmt;
      } else if ("prepareCall".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);//列印sql陳述句
        }        
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);//創建代理物件
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else if ("createStatement".equals(method.getName())) {
        Statement stmt = (Statement) method.invoke(connection, params);
        stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);//創建代理物件
        return stmt;
      } else {
        return method.invoke(connection, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

  public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
    InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
    ClassLoader cl = Connection.class.getClassLoader();
    return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
  }

  public Connection getConnection() {
    return connection;
  }

}

從invoke方法里我們可以看到主要對Connection的prepareStatementprepareCallcreateStatement方法進行了增強,列印日志并創建了對應的代理類回傳,其它幾個類實作原理都是一樣,這里不再贅述,
但還有個問題,其它幾個類的呼叫都是在創建連接之后,所以對應的代理類是由上一個階段的代理類創建的,那ConnectionLogger是在哪里創建的呢?自然是在獲取連接時,而獲取連接都是在我們的業務代碼執行階段的時候,Mybatis對執行階段又封裝了一個個Excutor執行器,詳細代碼后文分析,

資料源

資料源的創建

資料源都需要實作JDK的DataSource介面,Mybatis自己本身實作了資料源介面,同時也支持第三方的資料源,這里主要看看Mybatis內部的實作,同樣先來看一張類圖:
在這里插入圖片描述
從圖中我們可以看到DataSource的初始化同樣是通過工廠模式實作的,而其本身提供了三種資料源:

  • PooledDataSource:帶連接池的資料源
  • UnpooledDataSource:不帶連接池的資料源
  • JNDI資料源

最后一種此處不分析,UnpooledDataSource就是一個普通的資料源,實作了基本的資料源介面;而PooledDataSource是基于UnpooledDataSource實作的,只是在此之上提供了連接池功能,另外還需要注意PooledConnection,該類是連接池中存放的連接物件,但其并不是真正的連接物件,只是持有了真實連接的參考,并且是對真實連接進行增強的代理類,下面就主要分析連接池的實作原理,

池化技術原理

資料結構

首先來看下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;
  //從資料源取出來連接的時間戳
  private long checkoutTimestamp;
  //連接創建的的時間戳
  private long createdTimestamp;
  //連接最后一次使用的時間戳
  private long lastUsedTimestamp;
  //根據資料庫url、用戶名、密碼生成一個hash值,唯一標識一個連接池
  private int connectionTypeCode;
  //連接是否有效
  private boolean valid;

  /*
   * Constructor for SimplePooledConnection that uses the Connection and PooledDataSource passed in
   *
   * @param connection - the connection that is to be presented as a pooled connection
   * @param dataSource - the dataSource that the connection is from
   */
  public PooledConnection(Connection connection, PooledDataSource dataSource) {
    this.hashCode = connection.hashCode();
    this.realConnection = connection;
    this.dataSource = dataSource;
    this.createdTimestamp = System.currentTimeMillis();
    this.lastUsedTimestamp = System.currentTimeMillis();
    this.valid = true;
    this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
  }

......省略

  /*
   * 此方法專門用來增強資料庫connect物件,使用前檢查連接是否有效,關閉時對連接進行回收
   *
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {//如果是呼叫連接的close方法,不是真正的關閉,而是回收到連接池
      dataSource.pushConnection(this);//通過pooled資料源來進行回收
      return null;
    } else {
      try {
    	  //使用前要檢查當前連接是否有效
        if (!Object.class.equals(method.getDeclaringClass())) {
          // issue #579 toString() should never fail
          // throw an SQLException instead of a Runtime
          checkConnection();//
        }
        return method.invoke(realConnection, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  }

  private void checkConnection() throws SQLException {
    if (!valid) {
      throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
    }
  }
}

屬性和方法上都已經有了詳細的注釋,主要關注realConnection真實連接的參考和invoke方法增強,接著再看連接池的實作,這個類包含了很多屬性:

  private final PoolState state = new PoolState(this);

  //真正用于創建連接的資料源
  private final UnpooledDataSource dataSource;

  // OPTIONAL CONFIGURATION FIELDS
  //最大活躍連接數
  protected int poolMaximumActiveConnections = 10;
  //最大閑置連接數
  protected int poolMaximumIdleConnections = 5;
  //最大checkout時長(最長使用時間)
  protected int poolMaximumCheckoutTime = 20000;
  //無法取得連接是最大的等待時間
  protected int poolTimeToWait = 20000;
  //最多允許幾次無效連接
  protected int poolMaximumLocalBadConnectionTolerance = 3;
  //測驗連接是否有效的sql陳述句
  protected String poolPingQuery = "NO PING QUERY SET";
  //是否允許測驗連接
  protected boolean poolPingEnabled;
  //配置一段時間,當連接在這段時間內沒有被使用,才允許測驗連接是否有效
  protected int poolPingConnectionsNotUsedFor;
  //根據資料庫url、用戶名、密碼生成一個hash值,唯一標識一個連接池,由這個連接池生成的連接都會帶上這個值
  private int expectedConnectionTypeCode;

相信上面大部分屬性讀者們都不會陌生,在進行開發時應該都有配置過,其中有一個關鍵的屬性PoolState,這個是物件主要保存了空閑連接活躍連接,也就是連接池用來管理資源的,它包含了以下屬性:

  protected PooledDataSource dataSource;
  //空閑的連接池資源集合
  protected final List<PooledConnection> idleConnections = new ArrayList<>();
  //活躍的連接池資源集合
  protected final List<PooledConnection> activeConnections = new ArrayList<>();
  //請求的次數
  protected long requestCount = 0;
  //累計的獲得連接的時間
  protected long accumulatedRequestTime = 0;
  //累計的使用連接的時間,從連接取出到歸還,算一次使用的時間;
  protected long accumulatedCheckoutTime = 0;
  //使用連接超時的次數
  protected long claimedOverdueConnectionCount = 0;
  //累計超時時間
  protected long accumulatedCheckoutTimeOfOverdueConnections = 0;
  //累計等待時間
  protected long accumulatedWaitTime = 0;
  //等待次數 
  protected long hadToWaitCount = 0;
  //無效的連接次數 
  protected long badConnectionCount = 0;
獲取連接

了解了這些關鍵的屬性后,再來看看如何從連接池獲取連接,在PooledDataSource中有一個popConnection用于獲取連接:

  private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
    PooledConnection conn = null;
    long t = System.currentTimeMillis();//記錄嘗試獲取連接的起始時間戳
    int localBadConnectionCount = 0;//初始化獲取到無效連接的次數

    while (conn == null) {
      synchronized (state) {//獲取連接必須是同步的
        if (!state.idleConnections.isEmpty()) {//檢測是否有空閑連接
          // Pool has available connection
          //有空閑連接直接使用
          conn = state.idleConnections.remove(0);
          if (log.isDebugEnabled()) {
            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
          }
        } else {// 沒有空閑連接
          if (state.activeConnections.size() < poolMaximumActiveConnections) {//判斷活躍連接池中的數量是否大于最大連接數
            // 沒有則可創建新的連接
            conn = new PooledConnection(dataSource.getConnection(), this);
            if (log.isDebugEnabled()) {
              log.debug("Created connection " + conn.getRealHashCode() + ".");
            }
          } else {// 如果已經等于最大連接數,則不能創建新連接
            //獲取最早創建的連接
            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
            if (longestCheckoutTime > poolMaximumCheckoutTime) {//檢測是否已經以及超過最長使用時間
              // 如果超時,對超時連接的資訊進行統計
              state.claimedOverdueConnectionCount++;//超時連接次數+1
              state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;//累計超時時間增加
              state.accumulatedCheckoutTime += longestCheckoutTime;//累計的使用連接的時間增加
              state.activeConnections.remove(oldestActiveConnection);//從活躍佇列中洗掉
              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {//如果超時連接未提交,則手動回滾
                try {
                  oldestActiveConnection.getRealConnection().rollback();
                } catch (SQLException e) {//發生例外僅僅記錄日志
                  /*
                     Just log a message for debug and continue to execute the following
                     statement like nothing happend.
                     Wrap the bad connection with a new PooledConnection, this will help
                     to not intterupt current executing thread and give current thread a
                     chance to join the next competion for another valid/good database
                     connection. At the end of this loop, bad {@link @conn} will be set as null.
                   */
                  log.debug("Bad connection. Could not roll back");
                }  
              }
              //在連接池中創建新的連接,注意對于資料庫來說,并沒有創建新連接;
              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) {
                  state.hadToWaitCount++;//連接池累計等待次數加1
                  countedWait = true;
                }
                if (log.isDebugEnabled()) {
                  log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
                }
                long wt = System.currentTimeMillis();
                state.wait(poolTimeToWait);//阻塞等待指定時間
                state.accumulatedWaitTime += System.currentTimeMillis() - wt;//累計等待時間增加
              } catch (InterruptedException e) {
                break;
              }
            }
          }
        }
        if (conn != null) {//獲取連接成功的,要測驗連接是否有效,同時更新統計資料
          // ping to server and check the connection is valid or not
          if (conn.isValid()) {//檢測連接是否有效
            if (!conn.getRealConnection().getAutoCommit()) {
              conn.getRealConnection().rollback();//如果遺留歷史的事務,回滾
            }
            //連接池相關統計資訊更新
            conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
            conn.setCheckoutTimestamp(System.currentTimeMillis());
            conn.setLastUsedTimestamp(System.currentTimeMillis());
            state.activeConnections.add(conn);
            state.requestCount++;
            state.accumulatedRequestTime += System.currentTimeMillis() - t;
          } else {//如果連接無效
            if (log.isDebugEnabled()) {
              log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
            }
            state.badConnectionCount++;//累計的獲取無效連接次數+1
            localBadConnectionCount++;//當前獲取無效連接次數+1
            conn = null;
            //拿到無效連接,但如果沒有超過重試的次數,允許再次嘗試獲取連接,否則拋出例外
            if (localBadConnectionCount > (poolMaximumIdleConnections + 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.");
    }

    return conn;
  }

這里的邏輯相對比較復雜,我總結了整個步驟并畫了一張圖幫助理解:回圈獲取連接,首先判斷是否還存在空閑連接,如果存在,則直接使用,并洗掉一個空閑連接;如果不存在,優先判斷是否已經達到最大活躍連接數量,如果沒有則直接創建一個新的連接;如果已經達到最大活躍連接數,則從活躍連接池中取出最早的連接,判斷是否超時,如果沒有超時,則呼叫wait方法阻塞;如果超時,則統計超時連接資訊,并根據超時連接的真實連接創建新的連接,同時讓舊連接失效,經過以上步驟后,如果獲取到一個連接,則還需要判斷連接是否有效,有效連接需要回滾之前未提交的事務并添加到活躍連接池,無效連接則統計資訊并判斷是否已經超過重試次數,若沒有則繼續回圈下一次獲取連接,否則拋出例外,回圈完成后回傳獲取到的連接,
在這里插入圖片描述

回收連接

普通的連接是直接關閉,需要用的時候重新創建,而連接池則需要將連接回收到池中復用,避免重復創建連接提高效率,在PooledDataSource中的pushConnection就是用于回收連接的:

  protected void pushConnection(PooledConnection conn) throws SQLException {

    synchronized (state) {//回收連接必須是同步的
      state.activeConnections.remove(conn);//從活躍連接池中洗掉此連接
      if (conn.isValid()) {
    	  //判斷閑置連接池資源是否已經達到上限
        if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
        	//沒有達到上限,進行回收
          state.accumulatedCheckoutTime += conn.getCheckoutTime();
          if (!conn.getRealConnection().getAutoCommit()) {
            conn.getRealConnection().rollback();//如果還有事務沒有提交,進行回滾操作
          }
          //基于該連接,創建一個新的連接資源,并重繪連接狀態
          PooledConnection newConn = new PooledConnection(conn.getRealConnection(), 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.");
          }
          //喚醒其他被阻塞的執行緒
          state.notifyAll();
        } else {//如果閑置連接池已經達到上限了,將連接真實關閉
          state.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.");
        }
        state.badConnectionCount++;
      }
    }
  }

回收連接的邏輯就比較簡單了,不過還是有幾個地方需要注意:首先從活躍連接池移除掉該連接,然后判斷是否是有效連接以及空閑連接池是否還有位置,如果是有效連接且空閑連接池還有位置的話,則需要基于當前回收連接的真實連接并創建新的連接放入到空閑連接中,然后喚醒等待的執行緒;如果沒有則直接關閉真實連接,這兩個分支都需要將回收的連接中未提交的事務回滾并將連接置為無效,如果本來就是無效連接則只需要記錄獲取無效連接的次數,
在這里插入圖片描述
以上就是Mybatis資料源以及連接池的實作原理,其中池化技術是非常重要的,

快取

快取的實作

Mybatis有一級快取二級快取,一級快取是SqlSession級別的,只能存在于同一個SqlSession生命周期中;二級快取則是跨SqlSession,以namespace為單位的,但實際上Mybatis的二級快取非常雞肋,有可能出現臟讀的情況,一般不會使用,
但Mybatis對快取做了大量的擴展,提供了防止快取擊穿、快取清空策略、序列化、定時清空、日志等功能,設計非常優雅,所以此處主要領略這一模塊的設計思想,先來看看包的結構:
在這里插入圖片描述
從上圖中我們可以看到,Mybatis提供了統一的快取介面,impl和decorators包中都是它的實作類,從包的名字我們可以想到快取這里又是使用了一個設計模式——裝飾者模式,利用該模式動態得為快取添加功能,真正的實作就是impl包 下的PerpetualCache,通過HashMap來快取資料的(會不會出現并發安全問題?),key是CacheKey物件,value是快取的資料,為什么key是CacheKey物件,而不是一個字串呢?讀者可以想想,怎樣才能確定不會讀取到錯誤的快取,這個類最后來分析,而decorators包下的都是進行功能增強的裝飾者類,這里主要來看看BlockingCache是如何防止快取擊穿的,

public class BlockingCache implements Cache {

  //阻塞的超時時長
  private long timeout;
  //被裝飾的底層物件,一般是PerpetualCache
  private final Cache delegate;
  //鎖物件集,粒度到key值
  private final ConcurrentHashMap<Object, ReentrantLock> locks;

  public BlockingCache(Cache delegate) {
    this.delegate = delegate;
    this.locks = new ConcurrentHashMap<>();
  }
  @Override
  public void putObject(Object key, Object value) {
    try {
      delegate.putObject(key, value);
    } finally {
      releaseLock(key);
    }
  }

  @Override
  public Object getObject(Object key) {
    acquireLock(key);//根據key獲得鎖物件,獲取鎖成功加鎖,獲取鎖失敗阻塞一段時間重試
    Object value = https://www.cnblogs.com/yewy/p/delegate.getObject(key);
    if (value != null) {//獲取資料成功的,要釋放鎖
      releaseLock(key);
    }        
    return value;
  }

  @Override
  public Object removeObject(Object key) {
    // despite of its name, this method is called only to release locks
    releaseLock(key);
    return null;
  }
  
  private ReentrantLock getLockForKey(Object key) {
    ReentrantLock lock = new ReentrantLock();//創建鎖
    ReentrantLock previous = locks.putIfAbsent(key, lock);//把新鎖添加到locks集合中,如果添加成功使用新鎖,如果添加失敗則使用locks集合中的鎖
    return previous == null ? lock : previous;
  }
  
//根據key獲得鎖物件,獲取鎖成功加鎖,獲取鎖失敗阻塞一段時間重試
  private void acquireLock(Object key) {
	//獲得鎖物件
    Lock lock = getLockForKey(key);
    if (timeout > 0) {//使用帶超時時間的鎖
      try {
        boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
        if (!acquired) {//如果超時拋出例外
          throw new CacheException("Couldn't get a lock in " + timeout + " for the key " +  key + " at the cache " + delegate.getId());  
        }
      } catch (InterruptedException e) {
        throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
      }
    } else {//使用不帶超時時間的鎖
      lock.lock();
    }
  }
  
  private void releaseLock(Object key) {
    ReentrantLock lock = locks.get(key);
    if (lock.isHeldByCurrentThread()) {
      lock.unlock();
    }
  }
}

在呼叫getObject獲取資料時,首先呼叫acquireLock根據key獲取鎖,如果獲取到鎖,則從PerpetualCache快取中獲取資料,如果沒有則去資料庫查詢資料,回傳結果后添加到快取中并釋放鎖,注意去資料庫查詢資料時是根據key加了鎖的,因此相同key只會有一個執行緒到達資料庫查詢,也就不會出現快取擊穿的問題,這個思路也可以用到我們的專案中去,
以上就是Mybatis解決快取擊穿的思路,另外再來看一個裝飾者SynchronizedCache,提供同步的功能,該裝飾器就是在對快取的增刪API上加上了synchronized關鍵字,這個裝飾器就是用來防止二級快取出現并發安全問題的,而一級快取根本不存在并發安全問題,其余的裝飾者這里就不贅述了,感興趣的讀者可自行分析,

CacheKey

因為Mybatis中存在動態SQL,所以快取的key沒法僅用一個字串來表示,所以通過CacheKey來封裝所有可能影響快取的因素,那么哪些因素會影響到快取呢?

  • namespace + id
  • 查詢的sql
  • 查詢的引數
  • 分頁資訊

而在CacheKey中有以下屬性:

  private static final int DEFAULT_MULTIPLYER = 37;
  private static final int DEFAULT_HASHCODE = 17;

  private final int multiplier; //參與hash計算的乘數
  private int hashcode; //CacheKey的hash值,在update函式中實時運算出來的
  private long checksum; //校驗和,hash值的和
  private int count; //updateList的中元素個數
  private List<Object> updateList; //該集合中的元素決定兩個CacheKey是否相等

其中updateList就是用來存盤所有可能影響快取的因素,其它幾個則是根據該屬性中的物件計算出來的值,每次構造CacheKey物件時都會呼叫update方法:

  public void update(Object object) {
	//獲取object的hash值
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); 
    //更新count、checksum以及hashcode的值
    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    hashcode = multiplier * hashcode + baseHashCode;
    //將物件添加到updateList中
    updateList.add(object);
  }

而判斷兩個CacheKey物件是否相同則是通過equals方法:

  public boolean equals(Object object) {
    if (this == object) {//比較是不是同一個物件
      return true;
    }
    if (!(object instanceof CacheKey)) {//是否型別相同
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;

    if (hashcode != cacheKey.hashcode) {//hashcode是否相同
      return false;
    }
    if (checksum != cacheKey.checksum) {//checksum是否相同
      return false;
    }
    if (count != cacheKey.count) {//count是否相同
      return false;
    }

    //以上都不相同,才按順序比較updateList中元素的hash值是否一致
    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
  }

可以看到這里比較相等的方法是非常嚴格的,并且效率極高,我們在專案中重寫equals方法時也可以參照該方法的實作,

反射

反射是Mybatis的重中之重,通過反射Mybatis才能實作物件的實體化和屬性的賦值,并且Mybatis的反射是對JDK的封裝和增強,使其更易于使用,性能更高,其中關鍵的幾個類如下:

  • ObjectFactory:通過該物件創建POJO類的實體,
  • ReflectorFactory:創建Reflector的工廠類,
  • Reflector:MyBatis反射模塊的基礎,每個Reflector物件都對應一個類,在其中快取了反射操作所需要的類元資訊,
  • ObjectWrapper:物件的包裝,抽象了物件的屬性資訊,他定義了一系列查詢物件屬性資訊的方法,以及更新屬性的方法,
  • ObjectWrapperFactory:創建ObjectWrapper的工廠類,
  • MetaObject:包含了原始物件、ObjectWrapper、ObjectFactory、ObjectWrapperFactory、ReflectorFactory的參考,通過該類可以進行核心反射類的所有操作,也是門面模式的實作,

由于該模塊只是對JDK的封裝,雖然代碼和類非常多,但并不是很復雜,這里就不詳細闡述了,

總結

本篇講解了Mybatis最核心的四大模塊,可以看到使用了大量的設計模式使得代碼優雅簡潔,可讀性高,同時便于擴展,這也是我們在做專案時首先需要考慮的,代碼都是給人讀的,如何降低閱讀代碼的成本,提高代碼的質量,減少BUG的數量,只有多學習優秀代碼的設計思想才能提高我們自身的水平,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/150247.html

標籤:Java

上一篇:日志系統新貴 Loki,真香!!

下一篇:【HDFS篇05】HDFS客戶端操作 --- IO流操作

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more