调试 Spring + GORM + BeanDefinitionRegistryPostProcessor 遇到的问题

16 篇文章 0 订阅
14 篇文章 0 订阅

一、目的

我需要让 GORM 在独立的 Spring 应用中能自动查找到 Entity 类、Service类,类似在 grails 中,只要在指定的目录下放置 groovy Entity、Service 类就能被自动识别为 Gorm entity。

在实现自动扫描 service 类的时候,创建了一个 GormScanner 类,这个类实现了接口 BeanDefinitionRegistryPostProcessor 并且需要一个 HibernateDatastore 属性。问题是一旦添加了这个属性,就会导致报告错误:

No Session found for current thread
org.hibernate.HibernateException: No Session found for current thread
	at org.grails.orm.hibernate.GrailsSessionContext.currentSession(GrailsSessionContext.java:112)
	at org.hibernate.internal.SessionFactoryImpl.getCurrentSession(SessionFactoryImpl.java:514)
	at org.grails.orm.hibernate.HibernateSession.createQuery(HibernateSession.java:185)
	at org.grails.orm.hibernate.HibernateSession.createQuery(HibernateSession.java:178)
......

二、查找问题原因的过程:

查看离错误最近的代码:
org.grails.orm.hibernate.GrailsSessionContext#currentSession

发现是不能获取到用 SessionFactory 为 key 的线程资源 session holder:
Object value = TransactionSynchronizationManager.getResource(sessionFactory);

正常情况下,返回的 value 是一个 SessionHolder,错误情况下返回的是 null。

正常情况下,程序会在这里:

org.springframework.orm.hibernate5.HibernateTransactionManager#doBegin

调用 org.springframework.transaction.support.TransactionSynchronizationManager#bindResource 来注册 SessionHolder:
调用 bindResource() 的堆栈
这时,Service 对象的属性 $transactionTemplate 是这样:
在这里插入图片描述
这个属性是 Groovy Transform 注解动态生成的代码设置的。

但执行 save() 函数时,却用了另外的 GrailsHibernateTransactionManager 对象 和 SessionFactory 对象:
在这里插入图片描述

结论:错误情况下,bindResource 和 getResource() 使用的 SessionFactoryImpl object 不一样,导致取不到 value 。

GrailsSessionContext 是用 构造函数传入的 SessionFactoryImplementor 参数 来获取 session 资源的,如果设置和读取使用的 sessionFactory 对象不一样,就会导致失败。
在这里插入图片描述
最后发现是在不同的 spring context xml 文件中定义了两个 HiberanteDatastore 的bean造成的问题,这个两个bean使用了不同的 id,一个是 hibernateDatastore 一个是 hiberanteDataStore。这导致 HiberanteDatastore 构造了2次,执行了2次构造函数。

将 id 改为相同的即可解决问题,这样,只会创建一个 HiberanteDatastore bean。注意,xml 的装载顺序很重要,后装载的会覆盖前面的bean 定义。

错误过程分析

HiberanteDatastore bean A 是前一个 xml 中配置的。

假设 HiberanteDatastore bean B 是后一个xml中配置的,且设置给 GormScanner 的 hibernateDatastore 属性,GormScanner 实现BeanDefinitionRegistryPostProcessor接口。

那么,当创建程序会执行下列过程,导致错误:

  • HiberanteDatastore bean B 的构造函数执行
    • 创建 B 的 TransactionManager 和 SessionFactory
  • HiberanteDatastore bean A 的构造函数执行
    • 创建 A 的 TransactionManager 和 SessionFactory
  • 执行 @Transactional 函数,Gorm 自动调用 GrailsTransactionTemplate.execute()
    • 这时,使用了最后创建的 TransactionManager,即 A 的 TransactionManager 和 SessionFactory 来创建 Transaction。
    • 使用 A 的 SessionFactory 来绑定 SessionHolder 到线程资源 【绑定 SessionHolder 的步骤】
  • 执行 Entity.save() 函数,这时,会使用 service 中设置的 HibernateDatastore B 对象,及其 TransactionManager 和 SessionFactory。
    • org.grails.orm.hibernate.GrailsSessionContext#currentSession 用 B 的 sessionFactory 从线程获取 session,从而失败,返回null
    • 抛出异常
    • 事务回滚
绑定 SessionHoler 的步骤分析

绑定动作是通过 GrailsHibernateTransactionManager#getSessionFactory 来获取的 key 。
而这个属性是在构造函数中被设置的,因此 GrailsHibernateTransactionManager 对象就是关键。

查看 TransactionalTransform 源码,可以看到 @Transactional 注解,会生成下面的代码:

        // GrailsTransactionTemplate $transactionTemplate
        //           = new GrailsTransactionTemplate(getTransactionManager(), $transactionAttribute )

新创建的 HibernateDatastore 对象是下面这样,并且:
在这里插入图片描述
TransactionTemplate 的 sessionFactory 是下面这个,也是注册时用的 key:
在这里插入图片描述
Action 的 transactionFactory 是:
在这里插入图片描述
注册用的sessionFactory 是下面这个,也是 @Transactional 注解使用的:
在这里插入图片描述
而 绑定 是由下面的函数发起的:
在这里插入图片描述
GrailsTransactionTemplate object 的 transactionTemplate.transactionManager.sessionFactory 和 action参数的 thisObject.org_grails_datastore_mapping_services_Service__datastore.sessionFactory 不一样了。

读取用的是:
在这里插入图片描述
org.grails.datastore.mapping.core.DatastoreUtils#execute(org.grails.datastore.mapping.core.Datastore, org.grails.datastore.mapping.core.SessionCallback)
里面会调用:

    /**
     * Bind the session to the thread with a SessionHolder keyed by its Datastore.
     * @param session the session
     * @return the session (for method chaining)
     */
    public static Session bindSession(final Session session) {
        TransactionSynchronizationManager.bindResource(session.getDatastore(), new SessionHolder(session));
        return session;
    }

因此可以看到问题根源是 @Transactional 注解产生的代码所使用的 TransactionTemplate 与 entity 使用的 TransactionTemplate 不一致造成了错误! 具体来说是从线程资源中读取sessionHolder时,使用了不同的 key 对象,从而造成设置和读取的key不一样。

那么,为什么会使用不同的 TransactionTemplate 呢?
在 Service 中,entity 使用的是显式设置的 HibernateDatastore 以及 TransactionTemplate;
而 @Transactional 注解生成的代码使用了 另外一个 HibernateDatastore 。

是下面的Transformer代码获取的 transactionManager:

// $transactionManager = connection != null ? getTargetDatastore(connection).getTransactionManager() : getTransactionManager()

// Prepare the getTransactionManager() method body
// if($transactionManager != null)
// return $transactionManager
// else
// return GormEnhancer.findSingleTransactionManager()

问题终于清楚了!!!

是因为 GormEnhancer 的属性 DATASTORES_BY_TYPE 只能存放一个 class 对应的 value,比如 HibernateDatastore 只能存放一个 value ,如果初始化2个 HibernateDatastore bean ,那么,后者会覆盖前一个,导致 @Transactional 注解获得的 TransactionTemplate 和 Enitty 所使用的的 TransactionTemplate 不一致!

GormEnhancer 的代码如下:

    GormEnhancer(Datastore datastore, PlatformTransactionManager transactionManager, ConnectionSourceSettings settings) {
        this.datastore = datastore
        this.failOnError = settings.isFailOnError()
        Boolean markDirty = settings.getMarkDirty()
        this.markDirty = markDirty == null ? true : markDirty
        this.transactionManager = transactionManager
        this.dynamicEnhance = false
        if(datastore != null) {
            registerConstraints(datastore)
        }
        NAMED_QUERIES.clear()
        DATASTORES_BY_TYPE.put(datastore.getClass(), datastore)             // 就是这一句

        for(entity in datastore.mappingContext.persistentEntities) {
            registerEntity(entity)
        }
    }

因此,结论是一个JVM中不能初始化2个不同的 HibernateDatastore 对象。

问题二、从字符串获取对应 Package 对象

从字符串获取一个对应的 Package 对象需要 ClassLoader 已经装载过该包下的class,否则会返回 null。

尝试用 Package.getPackage(‘package.name’) 获取 Package 对象失败,原因是该方法会使用调用方的ClassLoader来获取包,但因为Groovy 会将调用者设置为一个叫 CallSiteClassLoader 的 class-loader,并且 CallSiteClassLoader 的 parent 是 null,即bootstrap classloader,导致获取不到 entity package。

只好用 ClassLoader.getPackage() 函数了,这个函数是一个 protected 方法,在groovy中可以使用,但会有访问警告。

最后,改用传递 Class[] 的方式来初始化 HibernateDatastore 了。

Spring XML 配置文件的装载顺序很重要

xml 的顺序很重要,因为后面的 xml bean 定义会覆盖之前相同id的bean定义。

context = new ClassPathXmlApplicationContext("classpath:/geo_utils/spring-context.xml",  "classpath:/spring-context.xml"); 

上面的代码,/spring-context.xml 会覆盖/geo_utils/spring-context.xml中有相同 bean id 的定义。

参考资料

  • https://www.iteye.com/blog/johnnyjian-1847151
  • http://www.2cto.com/kf/201403/284030.html
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值