一、目的
我需要让 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:
这时,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