[我读'不懂'源码]:001因事务导致hibernate延迟加载出现no session异常

[TOC]

因事务导致hibernate延迟加载出现no session异常

文章来源: 临窗旋墨的博客

本人对hibernate 的使用不是特别的熟悉,这里只是记录一次帮助同事解决异常排查的过程.

项目中的spring版本为4.1.6

贴出的源码的spring版本为5.1.9

1 项目技术框架

spring + springmvc + hibernate + freemarker

2 异常的产生现象

controller中直接调用serviceA中的方法A 页面可正常渲染;

controller调用serviceB中的方法B, 方法B中调用serviceA中的方法A,渲染页面的时候报异常:no session

3 问题追踪

  1. 根据现象,最直观的原因是session已经关闭, 然后获取懒加载对象的属性的时候,没有获取到session;
  2. 直接调用无错, 但是经过另外一个service调用后就no session, 说明两个session可能并不是一个session
3.1 确认是否是一个session的调试:结果不是一个session;

本项目开启了OpenSessionInViewFilter过滤器,

  1. 断点跟踪OpenSessionInViewFilter中的open的Session, 记录sesson的hashcode-->code1;
  2. 进入serviceB, 通过获取sessionFactory.getCurrentSession(), 记录session的hashcode, 等于code1, 表示此处使用的session是filter中打开的session;
  3. 从serviceB进入serviceA,获取当前session,获得的hashcode值与code1不一致;
3.2 为什么不是一个session: 因为不在一个事物中
  1. 本项目的事务在配置文件中由切面配置;
  2. 由于命名方式的问题,
    1. 入口方法B不要求在事物中运行( propagation="SUPPORTS")
    2. 方法A需要在一个事物中运行((propagation="REQUIRED"))
    3. 导致进入方法B后不需要事务,但是B调用的A方法在事务中运行

关于事务的传播性,小伙伴可自行复习;

3.3 解决问题:修改方法名,保证在一个事务中

修改方法名后, 保证两个方法在一个事务中,再次验证, 两者session一致, 延迟加载的属性可正常加载, 在OpenSessionInViewFilter的finally代码块中亦可正常关闭;

4 再问:openSession和getCurrentSession的区别

  • 采用getCurrentSession()创建的session在commit或rollback时会自动关闭,
  • 采用openSession()创建的session必须手动关闭
  • 采用getCurrentSession()创建的session会绑定到当前线程中,
  • 采用openSession() 创建的session则不会.所以以上的session是一直都是一个session.

本问题中因为方法A中session (getCurrentSession())在事务中运行, 所以在commit之后被关闭掉了,造成延迟加载事务失败;

5 三问: 为什么开启新事物中的session和当前线程中的session不一致

5.1 spring如何管理hibernate的session

查看配置文件中关于sessionFactory的配置为LocalSessionFactoryBean

LocalSessionFactoryBean` implements FactoryBean.......

SessionFactory.getCurrentSession源码追踪
SessionFactoryImpl#getCurrentSession
public Session getCurrentSession() throws HibernateException {
    if ( currentSessionContext == null ) {
        throw new HibernateException( "No CurrentSessionContext configured!" );
    }
    return currentSessionContext.currentSession();
}
SpringSessionContext#currentSession
    public Session currentSession() throws HibernateException {
        /*
        *以sessionFactory为key去当前线程中获取session(此处不再展开源码), 可能是在 OpenSessionInViewFilter 中存入线程的, 参见 OpenSessionInViewFilter 的openSessIon代码
        */
        Object value = TransactionSynchronizationManager.getResource(this.sessionFactory);
        if (value instanceof Session) {
            return (Session) value;
        }
        else if (value instanceof SessionHolder) {
            // hibernate事务管理器
            SessionHolder sessionHolder = (SessionHolder) value;
            Session session = sessionHolder.getSession();
            if (!sessionHolder.isSynchronizedWithTransaction() &&
                    TransactionSynchronizationManager.isSynchronizationActive()) {
                TransactionSynchronizationManager.registerSynchronization(
                        new SpringSessionSynchronization(sessionHolder, this.sessionFactory, false));
                sessionHolder.setSynchronizedWithTransaction(true);
                // Switch to FlushMode.AUTO, as we have to assume a thread-bound Session
                // with FlushMode.MANUAL, which needs to allow flushing within the transaction.
                FlushMode flushMode = SessionFactoryUtils.getFlushMode(session);
                if (flushMode.equals(FlushMode.MANUAL) &&
                        !TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
                    session.setFlushMode(FlushMode.AUTO);
                    sessionHolder.setPreviousFlushMode(flushMode);
                }
            }
            return session;
        }
        else if (value instanceof EntityManagerHolder) {
            // JpaTransactionManager
            return ((EntityManagerHolder) value).getEntityManager().unwrap(Session.class);
        }

        if (this.transactionManager != null && this.jtaSessionContext != null) {
            try {
                if (this.transactionManager.getStatus() == Status.STATUS_ACTIVE) {
                    Session session = this.jtaSessionContext.currentSession();
                    if (TransactionSynchronizationManager.isSynchronizationActive()) {
                        TransactionSynchronizationManager.registerSynchronization(
                                new SpringFlushSynchronization(session));
                    }
                    return session;
                }
            }
            catch (SystemException ex) {
                throw new HibernateException("JTA TransactionManager found but status check failed", ex);
            }
        }

        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            Session session = this.sessionFactory.openSession();
            if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
                session.setFlushMode(FlushMode.MANUAL);
            }
            SessionHolder sessionHolder = new SessionHolder(session);
            TransactionSynchronizationManager.registerSynchronization(
                    new SpringSessionSynchronization(sessionHolder, this.sessionFactory, true));
            TransactionSynchronizationManager.bindResource(this.sessionFactory, sessionHolder);
            sessionHolder.setSynchronizedWithTransaction(true);
            return session;
        }
        else {
            throw new HibernateException("Could not obtain transaction-synchronized Session for current thread");
        }
    }
对比OpenSessionInViewFilter#doFilterInternal ,可以发现两者获取hibernate 的session的逻辑相似;
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        SessionFactory sessionFactory = lookupSessionFactory(request);
        boolean participate = false;

        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        String key = getAlreadyFilteredAttributeName();

        if (TransactionSynchronizationManager.hasResource(sessionFactory)) {
            // Do not modify the Session: just set the participate flag.
            participate = true;
        }
        else {
            boolean isFirstRequest = !isAsyncDispatch(request);
            if (isFirstRequest || !applySessionBindingInterceptor(asyncManager, key)) {
                logger.debug("Opening Hibernate Session in OpenSessionInViewFilter");
                Session session = openSession(sessionFactory);
                SessionHolder sessionHolder = new SessionHolder(session);
                TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);

                AsyncRequestInterceptor interceptor = new AsyncRequestInterceptor(sessionFactory, sessionHolder);
                asyncManager.registerCallableInterceptor(key, interceptor);
                asyncManager.registerDeferredResultInterceptor(key, interceptor);
            }
        }

        try {
            filterChain.doFilter(request, response);
        }

        finally {
            if (!participate) {
                SessionHolder sessionHolder =
                        (SessionHolder) TransactionSynchronizationManager.unbindResource(sessionFactory);
                if (!isAsyncStarted(request)) {
                    logger.debug("Closing Hibernate Session in OpenSessionInViewFilter");
                    SessionFactoryUtils.closeSession(sessionHolder.getSession());
                }
            }
        }
spring是如何管理hibernate的事务的?HibernateTransactionManager

本项目配置的hibernate事务管理器为HibernateTransactionManager

HibernateTransactionManager extends AbstractPlatformTransactionManager......

其中获取事务的源码参见:AbstractPlatformTransactionManager#getTransaction
    public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
        Object transaction = doGetTransaction();

        // Cache debug flag to avoid repeated checks.
        boolean debugEnabled = logger.isDebugEnabled();

        if (definition == null) {
            // Use defaults if no transaction definition given.
            definition = new DefaultTransactionDefinition();
        }

        if (isExistingTransaction(transaction)) {
            // Existing transaction found -> check propagation behavior to find out how to behave.
            return handleExistingTransaction(definition, transaction, debugEnabled);
        }

        // Check definition settings for new transaction.
        if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
            throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout());
        }

        // No existing transaction found -> check propagation behavior to find out how to proceed.
        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
            throw new IllegalTransactionStateException(
                    "No existing transaction found for transaction marked with propagation 'mandatory'");
        }
        else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
                definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
                definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
            SuspendedResourcesHolder suspendedResources = suspend(null);
            if (debugEnabled) {
                logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition);
            }
            try {
                boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
                DefaultTransactionStatus status = newTransactionStatus(
                        definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
                doBegin(transaction, definition);
                prepareSynchronization(status, definition);
                return status;
            }
            catch (RuntimeException | Error ex) {
                resume(null, suspendedResources);
                throw ex;
            }
        }
        else {
            // Create "empty" transaction: no actual transaction, but potentially synchronization.
            if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
                logger.warn("Custom isolation level specified but no actual transaction initiated; " +
                        "isolation level will effectively be ignored: " + definition);
            }
            boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
            return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
        }
    }
其中dobegin进入HibernateTransactionManager#doBegin
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        HibernateTransactionObject txObject = (HibernateTransactionObject) transaction;

        if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
            throw new IllegalTransactionStateException(
                    "Pre-bound JDBC Connection found! HibernateTransactionManager does not support " +
                    "running within DataSourceTransactionManager if told to manage the DataSource itself. " +
                    "It is recommended to use a single HibernateTransactionManager for all transactions " +
                    "on a single DataSource, no matter whether Hibernate or JDBC access.");
        }

        Session session = null;

        try {
            /*** 判断是否 open一个newSession 见下文说明
            txObject.hasSessionHolder() 一般应返回true,因为在filter中新建了
            txObject.getSessionHolder().isSynchronizedWithTransaction() 何时为true呢?
            在try代码块的末尾会设置其为true: txObject.getSessionHolder().setSynchronizedWithTransaction(true);
            有理由相信 可能是第1+n次进入此dobegin方法时候,如果是txObject中的SessionHolder属性为同一个,则这个属性为true.
            那么什么时候第1+n次进入dobegin,却携带了相同的txObject呢?
            参见:AbstractPlatformTransactionManager#
            DefaultTransactionStatus status = newTransactionStatus(
                        definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
                doBegin(transaction, definition);
            
            *************************************/
            if (!txObject.hasSessionHolder() || txObject.getSessionHolder().isSynchronizedWithTransaction()) {
                Interceptor entityInterceptor = getEntityInterceptor();
                Session newSession = (entityInterceptor != null ?
                        obtainSessionFactory().withOptions().interceptor(entityInterceptor).openSession() :
                        obtainSessionFactory().openSession());
                if (logger.isDebugEnabled()) {
                    logger.debug("Opened new Session [" + newSession + "] for Hibernate transaction");
                }
                txObject.setSession(newSession);
            }

            session = txObject.getSessionHolder().getSession();

            boolean holdabilityNeeded = this.allowResultAccessAfterCompletion && !txObject.isNewSession();
            boolean isolationLevelNeeded = (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT);
            if (holdabilityNeeded || isolationLevelNeeded || definition.isReadOnly()) {
                if (this.prepareConnection && isSameConnectionForEntireSession(session)) {
                    // We're allowed to change the transaction settings of the JDBC Connection.
                    if (logger.isDebugEnabled()) {
                        logger.debug("Preparing JDBC Connection of Hibernate Session [" + session + "]");
                    }
                    Connection con = ((SessionImplementor) session).connection();
                    Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
                    txObject.setPreviousIsolationLevel(previousIsolationLevel);
                    if (this.allowResultAccessAfterCompletion && !txObject.isNewSession()) {
                        int currentHoldability = con.getHoldability();
                        if (currentHoldability != ResultSet.HOLD_CURSORS_OVER_COMMIT) {
                            txObject.setPreviousHoldability(currentHoldability);
                            con.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT);
                        }
                    }
                }
                else {
                    // Not allowed to change the transaction settings of the JDBC Connection.
                    if (isolationLevelNeeded) {
                        // We should set a specific isolation level but are not allowed to...
                        throw new InvalidIsolationLevelException(
                                "HibernateTransactionManager is not allowed to support custom isolation levels: " +
                                "make sure that its 'prepareConnection' flag is on (the default) and that the " +
                                "Hibernate connection release mode is set to 'on_close' (the default for JDBC).");
                    }
                    if (logger.isDebugEnabled()) {
                        logger.debug("Not preparing JDBC Connection of Hibernate Session [" + session + "]");
                    }
                }
            }

            if (definition.isReadOnly() && txObject.isNewSession()) {
                // Just set to MANUAL in case of a new Session for this transaction.
                session.setFlushMode(FlushMode.MANUAL);
                // As of 5.1, we're also setting Hibernate's read-only entity mode by default.
                session.setDefaultReadOnly(true);
            }

            if (!definition.isReadOnly() && !txObject.isNewSession()) {
                // We need AUTO or COMMIT for a non-read-only transaction.
                FlushMode flushMode = SessionFactoryUtils.getFlushMode(session);
                if (FlushMode.MANUAL.equals(flushMode)) {
                    session.setFlushMode(FlushMode.AUTO);
                    txObject.getSessionHolder().setPreviousFlushMode(flushMode);
                }
            }

            Transaction hibTx;

            // Register transaction timeout.
            int timeout = determineTimeout(definition);
            if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
                // Use Hibernate's own transaction timeout mechanism on Hibernate 3.1+
                // Applies to all statements, also to inserts, updates and deletes!
                hibTx = session.getTransaction();
                hibTx.setTimeout(timeout);
                hibTx.begin();
            }
            else {
                // Open a plain Hibernate transaction without specified timeout.
                hibTx = session.beginTransaction();
            }

            // Add the Hibernate transaction to the session holder.
            txObject.getSessionHolder().setTransaction(hibTx);

            // Register the Hibernate Session's JDBC Connection for the DataSource, if set.
            if (getDataSource() != null) {
                SessionImplementor sessionImpl = (SessionImplementor) session;
                // The following needs to use a lambda expression instead of a method reference
                // for compatibility with Hibernate ORM <5.2 where connection() is defined on
                // SessionImplementor itself instead of on SharedSessionContractImplementor...
                ConnectionHolder conHolder = new ConnectionHolder(() -> sessionImpl.connection());
                if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
                    conHolder.setTimeoutInSeconds(timeout);
                }
                if (logger.isDebugEnabled()) {
                    logger.debug("Exposing Hibernate transaction as JDBC [" + conHolder.getConnectionHandle() + "]");
                }
                TransactionSynchronizationManager.bindResource(getDataSource(), conHolder);
                txObject.setConnectionHolder(conHolder);
            }

            // Bind the session holder to the thread.
            if (txObject.isNewSessionHolder()) {
                TransactionSynchronizationManager.bindResource(obtainSessionFactory(), txObject.getSessionHolder());
            }
            txObject.getSessionHolder().setSynchronizedWithTransaction(true);
        }

        catch (Throwable ex) {
            if (txObject.isNewSession()) {
                try {
                    if (session != null && session.getTransaction().getStatus() == TransactionStatus.ACTIVE) {
                        session.getTransaction().rollback();
                    }
                }
                catch (Throwable ex2) {
                    logger.debug("Could not rollback Session after failed transaction begin", ex);
                }
                finally {
                    SessionFactoryUtils.closeSession(session);
                    txObject.setSessionHolder(null);
                }
            }
            throw new CannotCreateTransactionException("Could not open Hibernate Session for transaction", ex);
        }
    }

如上 :若 txObject.hasSessionHolder() || txObject.getSessionHolder().isSynchronizedWithTransaction() 返回ture则会新建session

  1. 其中txObject.hasSessionHolder() 一般应返回true,因为多在OpenSessionInViewFilter中新建了

  2. 那txObject.getSessionHolder().isSynchronizedWithTransaction() 何时为true呢?

  • 在try代码块的末尾会设置其为true: txObject.getSessionHolder().setSynchronizedWithTransaction(true);

  • txObject(HibernateTransactionObject)对象来自doGetTransaction()方法

    • txObject的sessionFactory属性来自:(SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    • 可知在一个线程中确实是一个对象

HibernateTransactionManager#doGetTransaction
    protected Object doGetTransaction() {
        HibernateTransactionObject txObject = new HibernateTransactionObject();
        txObject.setSavepointAllowed(isNestedTransactionAllowed());
        //获取 sessionFactory, 来自当前HibernateTransactionManager实例的sessionFactory属性
        SessionFactory sessionFactory = obtainSessionFactory();
        //获取当前线程绑定的sessionHolder,来自TransactionSynchronizationManager 的ThreadLocal> resources
        SessionHolder sessionHolder, =
                (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
        if (sessionHolder != null) {
            if (logger.isDebugEnabled()) {
                logger.debug("Found thread-bound Session [" + sessionHolder.getSession() + "] for Hibernate transaction");
            }
            txObject.setSessionHolder(sessionHolder);
        }
        else if (this.hibernateManagedSession) {
            try {
                Session session = sessionFactory.getCurrentSession();
                if (logger.isDebugEnabled()) {
                    logger.debug("Found Hibernate-managed Session [" + session + "] for Spring-managed transaction");
                }
                txObject.setExistingSession(session);
            }
            catch (HibernateException ex) {
                throw new DataAccessResourceFailureException(
                        "Could not obtain Hibernate-managed Session for Spring-managed transaction", ex);
            }
        }

        if (getDataSource() != null) {
            ConnectionHolder conHolder = (ConnectionHolder)
                    TransactionSynchronizationManager.getResource(getDataSource());
            txObject.setConnectionHolder(conHolder);
        }

        return txObject;
    }

追踪代码而知, 在新建事务的时候,若当前线程之前已经新建了事务,且进入了doBegin方法,则hibernate的事务对象HibernateTransactionObject中持有的SessionHolder对象的synchronizedWithTransaction属性会被设置为true;在这种情况下,新事务下的session为新打开的session,造成和先前的session不一致的情况.

6 留下的疑问:

项目中的serviceA的A方法并没有开启事务(虽然进入事务切面, 但是传播级别是SUPPORTS)

根据源码只会新建一个空的事务(Create "empty" transaction: no actual transaction, but potentially synchronization.),

而不会进入doBegin,也就不会设置SessionHolder对象的synchronizedWithTransaction属性为true,

那么serviceB的B方法开启新事务的时候, 理应不会打开新的session才对......................

由于项目在内网环境,无法debug进入源码, 暂时未追踪运行时进入事务源码的代码,且留个疑问, 有暇在外网搭建个环境的时候,再做补充吧.

或者有哪些大侠直接告诉我答案也是好的.

文章来源: 临窗旋墨的博客

你可能感兴趣的:([我读'不懂'源码]:001因事务导致hibernate延迟加载出现no session异常)