1. 程式人生 > >Mybatis與Spring整合時做了哪些事情

Mybatis與Spring整合時做了哪些事情

 這篇部落格主要是來分析MyBatis與Spring整合後Spring幫我們做了哪些事情,以及整合後使用MyBatis有什麼變化。

首先來看看整合包下有什麼東西吧。

第一個模組annotation:這裡做了一個註解(MapperScan),用於掃描mapper。以及mapper掃描註冊器(MapperScannerRegistrar),此掃描註冊器實現了ImportBeanDefinitionRegistrar介面,在Spring容器啟動時會執行所有實現了這個介面的實現類,而這個註冊器內部會註冊一系列MyBatis的資訊。

第二個模組batch:這裡沒有深入研究這個包,估計是為了批量操作使用的?

第三個模組config:主要是為了解析處理讀取到的配置資訊。

第四個模組mapper:這裡就是主要處理mapper的地方了,與程式設計式的MyBatis不同,與Spring整合後,每個mapper本來的生命週期為method級別,在整合後變成了application級別,主要原因是Spring將掃描到的每一個mapper都存入IOC容器,並且是單例的。

我們進入ClassPathMapperScanner的doScan方法DEBUG來看一下。

這裡可以看到,所有的mapper都會被放入IOC容器,並且scope=singleton,生命週期提升至容器級別,所以我們在用的時候只需要使用註解@Autowired即可使用Mapper。

第五個模組support:只有一個模板輔助類,是MapperFactoryBean的父類,用於方便建立一個SqlSession(整合後的SqlSession變成了SqlSessionTemplate,下面會介紹)

第六個模組transaction:在整合後,事務的管理交給了Spring來做。

最後一個就是SqlSession的變化了,先來DEBUG看看整合後的mapper是個什麼東西吧。

這裡我隨意啟動了一個Service層,debug了一個mapper的屬性,可以看到,它還是一個熟悉的MapperProxy代理的,但不同點在於MapperProxy中的sqlSession,這裡再也不是DefaultsqlSession了,而是一個SqlSessionTemplate,那這個到底是什麼呢?

  public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
  }

這個是SqlSessionTemplate主要的構造方法,可以看到,SqlSessionTemplate裡還有一個SqlSession。

  private final SqlSession sqlSessionProxy;

而這個SqlSession又用動態代理代理了一下,主要代理類為SqlSessionInterceptor,其為SqlSessionTemplate的內部類。

  private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } 
  }

這裡刪除了一些東西,保留了關鍵程式碼,可以看出來每次執行SqlSessionTemplate的方法都會新建立一個SqlSession,看看getSqlSession這個方法是如何建立SqlSession的。

  public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Creating a new SqlSession");
    }

    session = sessionFactory.openSession(executorType);

    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

這個方法在SqlSessionUtils中,用了靜態導包方法直接寫方法名呼叫。可以看到,這裡會先從TransactionSynchronizationManager中獲取SqlSession(下面會提到一級快取),如果取不到就用sessionFactory新開啟一個SqlSession,而開啟的這個SqlSession還是我們之前熟悉的DefaultSqlSession,回到上面內部類的invoke方法。

Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }

由method.invoke(sqlSession, args)可以看出,其實真正執行查詢方法的還是DefaultSqlSession,下面又判斷了一下事務,然後自動commit。

我們來總結一下,這個SqlSessionTemplate到底在整合後有了什麼作用?為什麼要一個這樣的SqlSession?

首先理一理大致的流程,Spring容器在啟動後會掃描所有的Mapper資訊,然後還是用MapperProxy給每一個Mapper介面做代理,不同的是這次的MapperProxy中的SqlSession是SqlSessionTemplate代理的,也就是說,每次執行mapper介面的方法,都會先執行MapperProxy的invoke方法,然後去執行SqlSessionTemplate的invoke方法,然後SqlSessionTemplate內部就會新new一個DefaultSqlSession,實際上底層還是使用的DefaultSqlSession的方法。

那為什麼要這樣一個SqlSession呢?

可以從invoke方法看出,在整合後,設計者將為每一次查詢都開啟一個新的SqlSession,在整合後的scope為method級別,也就是每呼叫一次mapper介面的方法,都會建立一個SqlSession,然後會將此SqlSession判斷事務,因為是Spring管理事務,所以這裡就可以用SqlSessionTemplate這樣一個類去很方便的可以管理事務,也方便了Spring管理事務。

介紹到這裡,可以丟擲一個問題了,我們知道,一級快取是SqlSession級別的,同一個SqlSession在查詢同一條語句時會使用快取,但整合後將SqlSession設計成每呼叫一次方法開啟一個新的SqlSession。

那麼一級快取是不是失效的呢?

其實也不一定,在一般情況下一級快取是沒有用了,但如果開啟了事務,就不一定是新建立SqlSession了,其實上面也有提到,在SqlSesionUtils類中建立SqlSession方法裡。

  public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Creating a new SqlSession");
    }

    session = sessionFactory.openSession(executorType);

    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

會嘗試先從TransactionSynchronizationManager取SqlSession,而這個類存放了一個ThreadLocal變數。

    private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations");

在上面的getSqlSession方法中,倒二行有一個registerSessionHolder,裡面會判斷是否有開啟事務,如有開啟事務則將當前的SqlSession儲存至一個holder到TransactionSynchronizationManager的ThreadLocal中,也就是儲存到執行緒級別的變數中,然後在同一個Transaction且同一個執行緒中如果有再次呼叫mapper的方法,第二次進入getSqlSession方法後會從TransactionSynchronizationManager中取出剛剛存放的執行緒變數ThreadLocal的holder,再從holder取出SqlSession,這樣,兩次查詢就還是用的同一個SqlSession,一級快取還是會生效的(一級快取預設開啟)。但在沒有事務的情況下,每一個mapper方法的呼叫都會建立一個新的mapper。

這裡寫一個測試類來驗證以上的想法。

@RestController
@RequestMapping("")
public class LoginControlller {

	@Autowired
	UserMapper mapper;

	@RequestMapping(value = "/test")
	@Transactional
	public String test(){
		long one = System.currentTimeMillis();
		long id = 3;
		mapper.selectByPrimaryKey(id);
		long two = System.currentTimeMillis();
		mapper.selectByPrimaryKey(id);
		long three = System.currentTimeMillis();
		mapper.selectByPrimaryKey(id);
		long four = System.currentTimeMillis();

		System.out.println("第1次查詢耗時:" + (two - one));
		System.out.println("第2次查詢耗時:" + (three - two));
		System.out.println("第3次查詢耗時:" + (four - three));
		return "test";
	}

	@RequestMapping(value = "/test2")
	public String test2(){
		long one = System.currentTimeMillis();
		long a = 3;
		mapper.selectByPrimaryKey(a);
		long two = System.currentTimeMillis();
		mapper.selectByPrimaryKey(a);
		long three = System.currentTimeMillis();
		mapper.selectByPrimaryKey(a);
		long four = System.currentTimeMillis();

		System.out.println("第1次查詢耗時:" + (two - one));
		System.out.println("第2次查詢耗時:" + (three - two));
		System.out.println("第3次查詢耗時:" + (four - three));
		return "test2";
	}
}

/test對映test方法,此方法開啟了事務,/test2對映test2方法,此方法沒有開啟事務。

首先訪問/test,控制檯列印。

從日誌可以看出來,這裡只建立了一次SqlSession,並且將此SqlSession註冊進了transaction synchronization中,也就是上面提到的那個地方,也可以從下面看出,只執行了一次SQL語句的查詢,以至於查詢耗時變快,感覺存在快取也許是因為此時三個查詢都是同一個SqlSession,一級快取生效了。所以可以得出結論,被事務管理的方法中,呼叫mapper的方法,不會new多次SqlSession,同一個執行緒同一個mapper都只會使用同一個SqlSession,所以此時的一級快取是生效的。

第二個測試,訪問/test2 (沒有註解上@Transactional)

第三次查詢耗時:0,控制檯有點短擷取不到下面的內容了。這裡關鍵資訊我框了出來,可以看出第一次查詢使用了SQL語句進行查詢,然後關閉一個沒有事務的SqlSession,歸還JDBC連線給資料庫,在第二次查詢之前,又建立了一個新的SqlSession,然後又進行相同的SQL語句的查詢,接著又關閉了沒有事務的SqlSession,並且歸還了JDBC連線給資料庫。以至於為什麼查詢耗時還是越來越短,感覺到快取的存在,這是因為資料庫上也是有快取的。所以到這裡可以得出結論,沒有被事務管理的方法例如/test2,在每次mapper查詢的時候,都會新建立SqlSession,以至於一級快取失效,但底層資料庫快取還是有在的。我在寫這篇文章時曾一度以為MyBatis與Spring整合後一級快取是失效的,在查閱資料以及動手驗證才知道底層還有這麼個原理,在測試的時候沒有開啟事務也能讓查詢耗時變短,曾一度以為是MapperProxy中的methodCache屬性在作怪,感興趣的讀者可以去這個類看一看,這裡的methodCache只是快取了Mapper的method資訊,例如command的type(INSERT、UPDATE等等),查詢時還是照樣要去資料庫查詢,誤解了以為是這裡使查詢耗時變快,後來仔細看了一下這裡並不快取查詢結果...