mybatis-spring原理解析

前言

Mybatis是目前主流的Java ORM框架之一。
mybatis-spring包則是爲了讓Mybatis更好得整合進Spring的衍生產品。
本文就從Mybatis和mybatis-spring源碼着手,以目前較爲流行的用法,探究Mybatis的工做原理以及mybatis-spring是如何作到「迎合」Spring的。spring

一切都從配置開始

首先在pom.xml文件中引入Mybatis包和mybatis-spring包(若是是SpringBoot,引入mybatis-spring-boot-starter便可):sql

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.5.1</version>
</dependency>
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
  <version>2.0.1</version>
</dependency>

而後在Spring的配置xml文件中聲明如下bean:數據庫

<bean id="mySqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation" value="classpath:mybatis-config.xml" />
      <property name="mapperLocations" >
          <list>
              <value>classpath*:xxx/*.xml</value>
          </list>
      </property>
      <property name="dataSource" ref="myDataSource" />
  </bean>
  <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
      <property name="sqlSessionFactoryBeanName" value="mySqlSessionFactory" />
      <property name="basePackage" value="com.xxx.xxx.mapper" />
  </bean>

下面咱們研究每一個配置的做用,進而瞭解mybatis-spring的工做方式。緩存

SqlSessionFactoryBean

一個FactoryBean,負責建立SqlSessionFactory,而SqlSessionFactory是建立SqlSession的工廠類。安全

它在初始化時會解析基本配置和XML映射文件,而後所有封裝到一個Configuration對象中。建立出的SqlSessionFactory是DefaultSqlSessionFactory對象,持有這個Configuration對象。session

通常來講一個應用只須要建立一個SqlSessionFactory。mybatis

這裏重點關注下XML映射文件的解析,確切的說應該是解析的結果如何處理(畢竟解析的過程太複雜)。多線程

private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/sql"));
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
  }
}

經常使用的幾個節點:app

  • parameterMap節點解析成ParameterMap對象保存在Configuration的parameterMaps屬性中;
  • resultMap節點解析成ResultMap對象保存在Configuration的resultMaps屬性中;
  • select|insert|update|delete節點解析成MappedStatement保存在Configuration的mappedStatements屬性中。

光解析完還不夠,還得和映射接口關聯起來。XML文件的mapper節點會有namespace屬性,它的值就是映射接口的全類名。根據全類名獲取到Class對象,而後Configuration對象中的MapperRegistry屬性負責註冊該類,就是將類對象和由它初始化的MapperProxyFactory對象組成鍵值對放入knownMappers屬性。後面建立映射接口的實現類對象時會用到。框架

總結下SqlSessionFactoryBean的做用,就是建立一個SqlSessionFactory類型的單例,持有全部的配置信息和解析結果。

MapperScannerConfigurer

實現了BeanDefinitionRegistryPostProcessor,負責掃描指定包下的映射接口並向容器中註冊對應的bean。

註冊過程當中有一些細節須要提一下,註冊的bean的beanClass並非映射接口自己,而統一是MapperFactoryBean。同時MapperScannerConfigurer建立時傳入的sqlSessionFactoryBeanName所表明的SqlSessionFactory會設置到這些bean中去。

MapperFactoryBean

一個FactoryBean,負責建立對應映射接口的實現類對象,這個實現類負責完成映射接口的方法和XML定義的SQL語句的映射關係。

Mybatis經過SqlSession接口執行SQL語句,因此MapperFactoryBean會在初始化時經過持有的SqlSessionFactory對象建立一個SqlSessionTemplate(它實現了SqlSession)對象。這個SqlSessionTemplate是mybatis-spring的核心,它給常規的SqlSession賦予了更多的功能,特別是迎合Spring的功能,後面會詳細描述。

咱們來看一下MapperFactoryBean是如何建立映射接口的實現類對象的。
既然是FactoryBean,就是經過getObject建立須要的bean對象。跟蹤方法調用,發現最終委託給了Configuration對象中MapperRegistry屬性。上面簡述XML解析過程時已知,MapperRegistry對象的knownMappers屬性保存了映射接口的類對象和一個MapperProxyFactory對象組成的鍵值對。

MapperProxyFactory就是一個代理工廠類,它建立實現類對象的方式就是建立以映射接口爲實現接口、MapperProxy爲InvocationHandler的JDK動態代理。代理的邏輯都在MapperProxy#invoke方法中:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    } else if (isDefaultMethod(method)) {
      return invokeDefaultMethod(proxy, method, args);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  return mapperMethod.execute(sqlSession, args);
}

能夠看到,咱們想要實現的方法(即排除Object方法和接口的默認方法),都委託給了對應的MapperMethod去實現。方法第一次調用時,新建MapperMethod,而後放入緩存。MapperMethod包含了兩個內部類屬性:

  • SqlCommand:負責關聯SQL命令。根據接口名和方法名從Configuration對象的mappedStatements中檢查並獲取方法對應的SQL語句解析成的MappedStatement對象,保存它的id和SQL命令類型。
  • MethodSignature:負責解析和保存方法簽名信息。解析方法的參數和返回類型,保存解析後的信息。

獲取MapperMethod後就是調用它的execute方法:

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
    case INSERT: {
    Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT:
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {
        result = executeForCursor(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
      break;
    case FLUSH:
      result = sqlSession.flushStatements();
      break;
    default:
      throw new BindingException("Unknown execution method for: " + command.getName());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName() 
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}

方法根據SQL命令類型的不一樣進行不一樣的操做,同樣的地方是都會先把方法參數轉化爲SQL參數形式,而後執行傳進execute方法的SqlSession對象(即MapperFactoryBean對象持有的SqlSessionTemplate對象)的對應的方法。

總結下MapperScannerConfigurer和MapperFactoryBean的做用:MapperScannerConfigurer負責把配置路徑下的映射接口註冊爲Spring容器的MapperFactoryBean類型的bean。這個工廠bean經過代理方式建立對應映射接口的實現類對象。實現類攔截映射接口的自定義方法,讓SqlSessionTemplate去處理方法對應的SQL解析成的MappedStatement。

SqlSessionTemplate

實現了SqlSession,但和SqlSession默認實現類DefaultSqlSession不一樣的是,它是線程安全的,這意味着一個SqlSessionTemplate實例能夠在多個Dao之間共享;它和Spring的事務管理緊密關聯,能夠實現多線程下各個事務之間的相互隔離;另外,它會把Mybatis返回的異常轉化爲Spring的DataAccessException。下面咱們來探究它是如何作到這幾點的。

SqlSessionTemplate在初始化時會經過JDK動態代理的方式建立一個實現SqlSession、以SqlSessionInterceptor爲InvocationHandler的代理對象,SqlSessionTemplate的大多數方法調用都轉發給這個代理。攔截的邏輯在SqlSessionInterceptor#invoke中:

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;
    } catch (Throwable t) {
      Throwable unwrapped = unwrapThrowable(t);
      if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
        // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        sqlSession = null;
        Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
        if (translated != null) {
          unwrapped = translated;
        }
      }
      throw unwrapped;
    } finally {
      if (sqlSession != null) {
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
      }
    }
  }
}

首先獲取真正用來工做的SqlSession,SqlSessionUtils#getSqlSession:

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;
}

這裏包含了與Spring事務關聯的邏輯。先嚐試從事務同步管理類中獲取傳入的SqlSessionFactory對象在當前線程綁定的SqlSessionHolder對象,若是存在就直接返回SqlSessionHolder對象持有的SqlSession對象,不然就用SqlSessionFactory建立一個新的SqlSession,調用DefaultSqlSessionFactory#openSessionFromDataSource,level默認是null,autoCommit默認false:

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);
  } 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對象,這裏須要注意的一點是,這裏建立了Transaction和Executor,在繼續往底層探索時會再說起到。

建立完以後,會根據當前線程是否存在Spring事務而選擇是否封裝成SqlSessionHolder放入事務同步管理類,這樣以來,同線程同事務下對映射接口的調用,實際工做的都是同一個SqlSession。

咱們回到SqlSessionInterceptor,獲取到實際工做的DefaultSqlSession會去執行當前攔截的方法(具體咱們稍後探究),若是拋出Mybatis的PersistenceException異常,初始化時設置的PersistenceExceptionTranslator對象(默認是MyBatisExceptionTranslator對象)會對異常進行轉化爲DataAccessException。

總結下SqlSessionTemplate的做用,它經過動態代理對方法進行攔截,而後根據當前Spring事務狀態獲取或建立SqlSession來進行實際的工做。

DefaultSqlSession

咱們如今知道SqlSessionTemplate最終仍是依賴一個DefaultSqlSession對象去處理映射接口方法對應的MappedStatement。下面咱們以selectList方法爲例探究具體的處理過程:

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

首先從configuration中獲取到MappedStatement對象,而後讓Executor對象調用query方法。

Executor

Executor是Mybatis的執行器,負責SQL語句的生成和查詢緩存的維護。

前面在建立DefaultSqlSession的時候,會先讓configuration建立一個Executor,根據配置的ExecutorType選擇具體的Executor實現,默認是SimpleExecutor,而後若是配置緩存開啓(默認開啓),則還要封裝成CachingExecutor。

CachingExecutor的query方法會先從MappedStatement對象動態生成sql語句,和參數一塊兒封裝在BoundSql對象中;再根據sql、參數和返回映射等信息建立一個緩存鍵;而後檢查XML裏有沒有配置二級緩存,有的話就用緩存鍵去查找,不然就執行它代理的Executor對象的query方法,先用緩存鍵去一級緩存也叫本地緩存中去查找,若是沒有的話就執行doQuery方法。不一樣Executor實現的doQuery有所不一樣,但核心都是建立一個StatementHandler,而後經過它對底層JDBC Statement進行操做,最後對查詢的結果集進行轉化。

限於篇幅,就不繼續探究StatementHandler及更底層的操做了,就再看下Mybatis是怎麼管理數據庫鏈接的。

Transaction

先回顧下這個Transaction對象是怎麼來的:前面建立實際工做的DefaultSqlSession時會讓TransactionFactory對象建立一個Transactio對象做爲Executor對象的屬性。而這個TransactionFactory對象,如何沒有指定的話,默認是SpringManagedTransactionFactory對象。它接受一個DataSource建立SpringManagedTransaction,能夠看到這裏把事務隔離級別和是否自動提交兩個參數都忽略了,那是由於mybatis-spring把事務都交給Spring去管理了。

Executor在執行doQuery方法,建立JDBC Statement對象時須要先獲取到數據庫鏈接:

protected Connection getConnection(Log statementLog) throws SQLException {
  Connection connection = transaction.getConnection();
  if (statementLog.isDebugEnabled()) {
    return ConnectionLogger.newInstance(connection, statementLog, queryStack);
  } else {
    return connection;
  }
}

繼續看到SpringManagedTransaction,它的Connection是經過DataSourceUtils調用getConnection方法獲取的,核心邏輯在doGetConnection方法中:

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
   Assert.notNull(dataSource, "No DataSource specified");

   ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
   if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
      conHolder.requested();
      if (!conHolder.hasConnection()) {
         logger.debug("Fetching resumed JDBC Connection from DataSource");
         conHolder.setConnection(fetchConnection(dataSource));
      }
      return conHolder.getConnection();
   }
   // Else we either got no holder or an empty thread-bound holder here.

   logger.debug("Fetching JDBC Connection from DataSource");
   Connection con = fetchConnection(dataSource);

   if (TransactionSynchronizationManager.isSynchronizationActive()) {
      try {
         // Use same Connection for further JDBC actions within the transaction.
         // Thread-bound object will get removed by synchronization at transaction completion.
         ConnectionHolder holderToUse = conHolder;
         if (holderToUse == null) {
            holderToUse = new ConnectionHolder(con);
         }
         else {
            holderToUse.setConnection(con);
         }
         holderToUse.requested();
         TransactionSynchronizationManager.registerSynchronization(
               new ConnectionSynchronization(holderToUse, dataSource));
         holderToUse.setSynchronizedWithTransaction(true);
         if (holderToUse != conHolder) {
            TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
         }
      }
      catch (RuntimeException ex) {
         // Unexpected exception from external delegation call -> close Connection and rethrow.
         releaseConnection(con, dataSource);
         throw ex;
      }
   }

   return con;
}

能夠看到,Spring的事務管理器不只保存了事務環境下當前線程的SqlSession,還以dataSource爲鍵保存了Connection。若是從事務管理器沒有獲取到,就須要經過從SpringManagedTransaction傳遞過來的dataSource獲取Connection對象,獲取到以後判斷當前是否在事務環境,是的話就把Connection對象封裝成ConnectionHolder保存在事務管理器中,這樣的話就能保證一個事務中的數據庫鏈接是同一個。

相關文章
相關標籤/搜索