聊聊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 mappedStatements = new StrictMap("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这个接口方法中

聊聊Spring集成mybatis用到的SqlSessionTemplate_第1张图片

可以看到该接口有多个实现类,大胆猜测不同的实现类最终获取到的执行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("
                    
                    

你可能感兴趣的:(后端,mysql,springboot)