Mybatis-Spring:SqlSessionTemplate

环境:mybatis-spring 2.0.3

Mybatis提供了一个工具类SqlSessionTemplate,通过它可以操作所有Mapper配置文件的SQL语句

@Test
public void testInsert() {
    Product product = (Product) super.context.getBean("product");
    // Mapper接口全限定名称+Sql语句标签的Id
    String sql = "com.hyc.dao.ProductMapper.insertProduct";
    int add = super.template.insert(sql, product);
    System.out.println(add > 0 ? "插入成功" : "插入失败");
}

打开SqlSessionTemplate源码,根据注释内容,它是由Spring管理的线程安全类,与Spring的事务管理器协作确保真实的SqlSession被关联到当前的Spring事务。更进一步,它基于Spring事务配置管理session的生命周期,包括关闭、提交、回滚

同时,因为是线程安全的,所以可以作为单例对象被所有的Dao使用

public class SqlSessionTemplate implements SqlSession, DisposableBean {

    private final SqlSessionFactory sqlSessionFactory;
    private final ExecutorType executorType;
    // SqlSession代理,通过它执行数据库操作
    // 通过这个代理对象,可以获取到专属于当前线程的SqlSession对象
    private final SqlSession sqlSessionProxy;  

    // 构造器
    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, 
            ExecutorType executorType, 
            PersistenceExceptionTranslator exceptionTranslator) {

        // ...

        this.sqlSessionFactory = sqlSessionFactory;
        this.executorType = executorType;
        this.exceptionTranslator = exceptionTranslator;
        // 使用JDK动态代理创建一个SqlSession代理对象
        this.sqlSessionProxy = (SqlSession) newProxyInstance(
                                                SqlSessionFactory.class.getClassLoader(),
                                                new Class[] { SqlSession.class },
                                                new SqlSessionInterceptor());
    }

    // 原生mybatis采用DefaultSqlSession发起数据库操作
    // mybatis-spring则通过一个代理对象,它代理的是原生DefaultSqlSession
    // 之所以使用代理,是为了获取当前线程专属的DefaultSqlSession
    public <T> T selectOne(String statement, Object parameter) {
        return this.sqlSessionProxy.selectOne(statement, parameter);
    }
}

根据上述代码段,可以知道SqlSessionTemplate也实现了SqlSession接口,同时组合了另一个SqlSession接口代理对象sqlSessionProxy执行具体的数据库操作,代理对象的增强逻辑如下

 private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) 
            throws Throwable {
        // 获取一个SqlSession
        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)) {
                sqlSession.commit(true);
            }
            return result;
        } catch (Throwable t) {
            Throwable unwrapped = unwrapThrowable(t);
            if (SqlSessionTemplate.this.exceptionTranslator != null 
                    && unwrapped instanceof PersistenceException) {
                // 关闭SqlSession
                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) {
                // 关闭SqlSession
                closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
            }
        }
    }
  }

SqlSessionInterceptor中可以看到,代理对象还是先获取了一个SqlSession对象

SqlSession sqlSession = getSqlSession(
                SqlSessionTemplate.this.sqlSessionFactory,
                SqlSessionTemplate.this.executorType,
                SqlSessionTemplate.this.exceptionTranslator);

SqlSessionTemplate作为一个全局单例的Bean,必然在多个Dao层中共同使用,那么不可避免会遇到Servelt容器的多线程环境所导致的线程安全问题,对此,它通过SqlSessionUtils解决了线程安全的问题

public final class SqlSessionUtils {

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

        // ...

        // SqlSessionTemplate的线程安全性由此保证
        // TransactionSynchronizationManager保证了线程安全
        // 追根揭底,这个类采用的技术还是ThreadLocal线程隔离技术
        SqlSessionHolder holder = (SqlSessionHolder) 
                TransactionSynchronizationManager.getResource(sessionFactory);

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

        // ...
        // 如果当前线程没有session,则通过SessionFactory创建
        // 这里就又回到了mybatis的DefaultSessionFactory,
        // 创建Mybatis原生的DefaultSqlSession
        session = sessionFactory.openSession(executorType);
        // 注册上面创建的session对象
        registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
        return session;
    }

    // 使用SqlSessionHolder封装SqlSession,保存到ThreadLocal对象中
    private static void registerSessionHolder(SqlSessionFactory sessionFactory, 
            ExecutorType executorType, 
            PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
        SqlSessionHolder holder;
        // ...
        holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
        // 将当前的SqlSession保存到ThreadLocal
        TransactionSynchronizationManager.bindResource(sessionFactory, holder);
        TransactionSynchronizationManager.registerSynchronization(
                new SqlSessionSynchronization(holder, sessionFactory));
        holder.setSynchronizedWithTransaction(true);
        holder.requested();
        // ...
    }
}

Spring提供了TransactionSynchronizationManager管理每个线程的资源和事务的同步状态,它采用了ThreadLocal线程隔离保证线程安全性

public abstract class TransactionSynchronizationManager {

    // mybatis使用它保存当前线程信息 
    // Key为SqlSessionFactory value为SqlSessionHolder
    private static final ThreadLocal<Map<Object, Object>> resources =
            new NamedThreadLocal<>("Transactional resources");

    // 获取当前多线程专属的SqlSessionHolder
    public static Object getResource(Object key) {
        Object actualKey  = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
        Object value = doGetResource(actualKey);
        // ...
        return value;
    }

    // 从ThreadLocal获取SqlSessionHolder
    private static Object doGetResource(Object actualKey) {
        Map<Object, Object> map = resources.get();
        if (map == null) {
            return null;
        }
        Object value = map.get(actualKey);
        if (value instanceof ResourceHolder  && ((ResourceHolder) value).isVoid()) {
            map.remove(actualKey);
            // Remove entire ThreadLocal if empty...
            if (map.isEmpty()) {
                resources.remove();
            }
            value = null;
        }
        return value;
    }
    
    // mybatis应用这个方法将SqlSessionHolder保存到ThreadLocal
    public static void bindResource(Object key, Object value) 
            throws IllegalStateException {
        Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
        Map<Object, Object> map = resources.get();
        // set ThreadLocal Map if none found
        if (map == null) {
            map = new HashMap<>();
            // 保存到ThreadLocal
            resources.set(map);
        }
        // ...
    }
}

继续回到SqlSessionInterceptor,获取到当前线程专属的SqlSession之后,通过它处理完毕之后会关闭当前SqlSession,这也是Mybatis集成Spring之后一级缓存失效的原因

// 通过反射执行DefaultSqlSession对象的同名方法
Object result = method.invoke(sqlSession, args);
// 关闭当前SqlSession
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);

关闭··SqlSesion··会判断当前这个对象是否被Spring管理,没有直接关闭,有则引用减一,交给Spring关闭

public final class SqlSessionUtils {
    
    public static void closeSqlSession(SqlSession session, 
            SqlSessionFactory sessionFactory) {
        //...

        SqlSessionHolder holder = (SqlSessionHolder) 
                TransactionSynchronizationManager.getResource(sessionFactory);
        if ((holder != null) && (holder.getSqlSession() == session)) {
            // ...
            // SqlSession引用减一
            holder.released();
        } else {
            // ...
            session.close();  // 直接关闭
        }
    }

    // 一个事务结束前会判断当前SqlSession的引用是否为0
    // 为0则表示可以回收该SqlSession
    public void beforeCompletion() {
        // Issue #18 Close SqlSession and deregister it now
        // because afterCompletion may be called from a different thread
        if (!this.holder.isOpen()) {
            // ...
            // 从ThreadLocal中移除
            TransactionSynchronizationManager.unbindResource(sessionFactory);
            this.holderActive = false;
            // ...
            this.holder.getSqlSession().close();
        }
    }
}

SqlSession的引用变为0,那么就会从ThreadLocal中移除

public abstract class TransactionSynchronizationManager {

    // mybatis使用它保存当前线程信息 
    // Key为SqlSessionFactory value为SqlSessionHolder
    private static final ThreadLocal<Map<Object, Object>> resources =
            new NamedThreadLocal<>("Transactional resources");

    public static Object unbindResource(Object key) 
            throws IllegalStateException {
        Object actualKey = TransactionSynchronizationUtils
                                    .unwrapResourceIfNecessary(key);
        Object value = doUnbindResource(actualKey);
        // ...
        return value;
    }

    public static Object unbindResourceIfPossible(Object key) {
        Object actualKey = TransactionSynchronizationUtils
                                    .unwrapResourceIfNecessary(key);
        return doUnbindResource(actualKey);
    }

    private static Object doUnbindResource(Object actualKey) {
        Map<Object, Object> map = resources.get();
        if (map == null) {
            return null;
        }
        Object value = map.remove(actualKey);
        // Remove entire ThreadLocal if empty...
        if (map.isEmpty()) {
            resources.remove();
        }
    // ...
        return value;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,451评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,172评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,782评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,709评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,733评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,578评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,320评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,241评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,686评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,878评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,992评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,715评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,336评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,912评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,040评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,173评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,947评论 2 355