聊聊Spring集成mybatis用到的SqlSessionTemplate

    技术2022-07-10  91

    一、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这个接口方法中

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

    Processed: 0.009, SQL: 9