聊聊Spring集成mybatis用到的SqlSessionTemplate

一、SqlSessionTemplate的入場

spring在集成mybatis 的時候,並沒有使用DefaultSqlSession來一個個getmapper。而是通過@Autowired來直接獲取mapper接口,調用mapper方法。那麼spring幫助自動注入的mapper到底是什麼呢?

其實是一種名爲MapperFactoryBean的類,這個類繼承了SqlSessionDaoSupport,可以直接獲取到SqlSessionTemplate。使用SqlSessionTemplate來代理DefaultSqlSession進行db交互。

那麼這個SqlSessionTemplate到底是何方神聖?

SqlSessionTemplate其實是Spring 爲了接入mybatis提供的bean,它其實是SqlSession的一個實現類,用於替代DefaultSqlSession的存在,保存線程的SqlSession來保證線程安全性。

既然SqlSessionTemplate是一個Bean,那它默認就是單利的,裏面有一個sqlSessionProxy的類變量,他其實是SqlSession的代理類:SqlSessionInterceptor。每次SqlSessionTemplate在執行方法的時候,最後都給交給SqlSessionInterceptor來代理執行,SqlSessionInterceptor每次在獲取SqlSession的時候,都會使用事務管理器從threadlocal中獲取,所以必然是線程安全的!對於Spring而言,每個事務都會使用同一個SqlSession,其實也就是DefaultSqlSession,用於操作相應的executor來進行db交互。

是不是看到這裏有點懵逼?接下來結合源碼來進一步分析。

從你依賴注入一個mapper接口到完成一次sql查詢的全過程

二、結合源碼分析

例:TestMapper.java TestMapper.xml

通過@Autowire來獲取一個TestMapper,但其實spring在啓動的時候注入的是一個MapperFactoryBean(具體位置在org.mybatis.spring.mapper.ClassPathMapperScanner#processBeanDefinitions中)

下面看看MapperFactoryBean的源碼

// 繼承了SqlSessionDaoSupport,繼承了SqlSessionDaoSupport可以用於直接獲取SqlsessionTemplate
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {

  // 真正的mapper文件
  private Class<T> mapperInterface;

  public MapperFactoryBean() {
    //intentionally empty 
  }
  
  public MapperFactoryBean(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }
  ...
}

再來看看通過SqlSessionDaoSupport獲取到的SqlSessionTemplate裏面是什麼:

  // 其實爲defaultSqlSession的代理類SqlSessionInterceptor
  private final SqlSession sqlSessionProxy;
  
  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;
    // jdk動態代理
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());    
  }
  
 ...
   
   // 內部類 SqlSessionInterceptor 
   private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // SqlSessionUtils.getSession(...) 是工具類, 裏面使用事務同步管理器從threadlocal中獲取到SqlsessionHolder, sqlsessionholder可以用於獲取緩存了的DefaultSqlSession
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
        // 用DefaultSqlSession來完成真正的操作
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        ...
        throw unwrapped;
      } finally {
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }
// SqlSessionUtils
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

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

  // 使用事務同步管理器從threadlocal中獲取到SqlsessionHolder
  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

  // 獲取到DefaultSqlSession
  SqlSession session = sessionHolder(executorType, holder);
  if (session != null) {
    return session;
  }

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

  // 如果threadlocal中沒有會生成一個
  session = sessionFactory.openSession(executorType);

  registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

  return session;
}
// DefaultSqlSession 來完成真正的操作
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    // 在全局配置類Configuration中,有一個map爲: Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");  這是在Configuration初始化的時候,掃描到所有xml中的語句,init到這個map中去,每個sql語句都是一個 MappedStatement,map的key爲mapper文件的路徑+id(類似com.xxx.entity.mapper.TestMapper.selectByPrimaryKey)
    MappedStatement ms = configuration.getMappedStatement(statement);
    // 獲取到MapperStatement之後,交給executor來進行執行具體操作
    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();
  }
}

三、簡單介紹下一級緩存、二級緩存

Executor是在sqlsession生成的時候生成的,mybatis也給插件們(interceptor)留下了口子,用於代理executor。

如果不開啓二級緩存,默認的executor爲SimpleExecutor,否則爲CachingExecutor。CachingExecutor爲SimpleExecutor的裝飾器,二級緩存是基於namespace(MappedStatement)的,cache在MappedStatement中,所以所有的sqlsession共享二級緩存,而一級緩存就存在於executor下。

二級緩存 > 一級緩存

關於緩存這塊的具體內容推薦看下這篇美團技術團隊的文章,寫的特好(羨慕):

傳送門

四、使用#{}和${}的不同

網上資料大多都說明了,使用$ {} 的時候會存在sql注入的問題,所以已經不推薦使用這種方式,那麼到底兩者在什麼地方導致了不同的結果呢?結合源碼來進行說明。

// org.apache.ibatis.executor.BaseExecutor#query 方法 可以看到具體執行的sql是從MappedStatement中獲得
@Override
 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
   BoundSql boundSql = ms.getBoundSql(parameter);
   CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
   return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

最後進入到org.apache.ibatis.mapping.SqlSource#getBoundSql這個接口方法中

SqlSource類圖

可以看到該接口有多個實現類,大膽猜測不同的實現類最終獲取到的執行sql是不同的。

很容易可以看到MappedStatement 中的 SqlSource是在org.apache.ibatis.scripting.xmltags.XMLLanguageDriver#createSqlSource中生成的:

@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
  // issue #3
  if (script.startsWith("<script>")) {
    XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
    return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
  } else {
    // issue #127
    script = PropertyParser.parse(script, configuration.getVariables());
    TextSqlNode textSqlNode = new TextSqlNode(script);
    // 根據isDynamic 來生成不同的SqlSource 那這裏面到底做了什麼?
    if (textSqlNode.isDynamic()) {
      return new DynamicSqlSource(configuration, textSqlNode);
    } else {
      return new RawSqlSource(configuration, script, parameterType);
    }
  }
}
// TextSqlNode 類

// 構造方法,傳入代碼中編寫的sql語句
public TextSqlNode(String text) {
    this(text, null);
  }
  
public TextSqlNode(String text, Pattern injectionFilter) {
  this.text = text;
  this.injectionFilter = injectionFilter;
}

// 是否爲動態語句
public boolean isDynamic() {
  DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
  GenericTokenParser parser = createParser(checker);
  parser.parse(text);
  return checker.isDynamic();
}

...

  // 到這裏可以明顯看出SqlSource的實現類到底是 DynamicSqlSource 還是 RawSqlSource 是根據語句中是否有${}來判斷的
private GenericTokenParser createParser(TokenHandler handler) {
  return new GenericTokenParser("${", "}", handler);
}

知道了SqlSource 的來源,那麼再看看這兩種不同的實現類在getBoundSql方法,也就是獲取最終執行sql的時候到底有什麼不同。

RawSqlSource

// 不帶有${} 的sql語句使用的sqlsource實現類
public class RawSqlSource implements SqlSource {

  private final SqlSource sqlSource;

  public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
  }

  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    // 這個sqlSource 其實是StaticSqlSource 生成過程中會將sql中的#{...} 格式化爲 ? 的
    // 例:select * from test where id = #{id}
    // ====> select * from test where id = ?
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
  }

  private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    DynamicContext context = new DynamicContext(configuration, null);
    rootSqlNode.apply(context);
    return context.getSql();
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    // 裝飾器模式 交給StaticSqlSource.getBoundSql 
    // 直接返回 new BoundSql(configuration, sql, parameterMappings, parameterObject);
    return sqlSource.getBoundSql(parameterObject);
  }

}

DynamicSqlSource

// 帶有${} 的sql語句使用的sqlsource實現類 
public class DynamicSqlSource implements SqlSource {

  private final Configuration configuration;
  private final SqlNode rootSqlNode;

  public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
    this.configuration = configuration;
    this.rootSqlNode = rootSqlNode;
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 在這裏會將初始sql和param屬性替換,組成一個完整的執行sql語句。並放到context的sqlBuilder屬性中。
    // 例:select * from test where id = ${id}
    // ====> select * from test where id = 1
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    // 將上面處理完成後的sql語句再進行StaticSqlSource的處理,依然對#{}的屬性進行處理
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }

}

無論是DynamicSqlSource還是RawSqlSource ,在最後都會交給PreparedStatement來處理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章