Spring Batch的ItemProcessor执行时出现Hibernate no session异常如何解决
我在运行Spring Batch作业时,在ItemProcessor中访问Hibernate实体关联关系时遇到了问题。该ItemProcessor属于基于chunk的步骤,按我理解ItemProcessor是在事务中运行的,因此应该可以懒加载实体关联关系。
问题详情
我在执行ItemProcessor逻辑时收到如下异常:
org.hibernate.LazyInitializationException: could not initialize proxy [org.powo.model.registry.Organisation#1] - no Session at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:169) at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:309) at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45) at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95) at org.powo.model.registry.Organisation$HibernateProxy$OFnEWoXa.getIdentifier(Unknown Source) at org.powo.model.solr.BaseSolrInputDocument.build(BaseSolrInputDocument.java:39) at org.powo.model.solr.TaxonSolrInputDocument.<init>(TaxonSolrInputDocument.java:67) at org.powo.model.Taxon.toSolrInputDocument(Taxon.java:1091) at org.powo.job.reindex.TaxonToSolrInputDocumentProcessor.process(TaxonToSolrInputDocumentProcessor.java:20) at org.powo.job.reindex.TaxonToSolrInputDocumentProcessor.process(TaxonToSolrInputDocumentProcessor.java:13) at org.springframework.batch.core.step.item.SimpleChunkProcessor.doProcess(SimpleChunkProcessor.java:126) at org.springframework.batch.core.step.item.SimpleChunkProcessor.transform(SimpleChunkProcessor.java:303) at org.springframework.batch.core.step.item.SimpleChunkProcessor.process(SimpleChunkProcessor.java:202) at org.springframework.batch.core.step.item.ChunkOrientedTasklet.execute(ChunkOrientedTasklet.java:75) at org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:406) at org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:330) at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140) at org.springframework.batch.core.step.tasklet.TaskletStep$2.doInChunkContext(TaskletStep.java:272) at org.springframework.batch.core.scope.context.StepContextRepeatCallback.doInIteration(StepContextRepeatCallback.java:81) at org.springframework.batch.repeat.support.RepeatTemplate.getNextResult(RepeatTemplate.java:375) at org.springframework.batch.repeat.support.RepeatTemplate.executeInternal(RepeatTemplate.java:215) at org.springframework.batch.repeat.support.RepeatTemplate.iterate(RepeatTemplate.java:145) at org.springframework.batch.core.step.tasklet.TaskletStep.doExecute(TaskletStep.java:257) at org.springframework.batch.core.step.AbstractStep.execute(AbstractStep.java:200) at org.springframework.batch.core.job.SimpleStepHandler.handleStep(SimpleStepHandler.java:148) at org.springframework.batch.core.job.flow.JobFlowExecutor.executeStep(JobFlowExecutor.java:66) at org.springframework.batch.core.job.flow.support.state.StepState.handle(StepState.java:67) at org.springframework.batch.core.job.flow.support.SimpleFlow.resume(SimpleFlow.java:169) at org.springframework.batch.core.job.flow.support.SimpleFlow.start(SimpleFlow.java:144) at org.springframework.batch.core.job.flow.FlowJob.doExecute(FlowJob.java:136) at org.springframework.batch.core.job.AbstractJob.execute(AbstractJob.java:308) at org.springframework.batch.core.launch.support.SimpleJobLauncher$1.run(SimpleJobLauncher.java:141) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base/java.lang.Thread.run(Thread.java:834)
以下是用到的ItemProcessor代码供参考(遍历实体关联关系的逻辑位于toSolrInputDocument方法内):
@Component public class TaxonToSolrInputDocumentProcessor implements ItemProcessor<Taxon, SolrInputDocument> { @Autowired private ApplicationContext context; @Override public SolrInputDocument process(Taxon item) throws Exception { return item.toSolrInputDocument(context); } }
我使用的读取器是org.springframework.batch.item.database.HibernatePagingItemReader。
已尝试的解决方案
我已经尝试过以下方案,但都没有解决上述报错:
- 改用
JpaPagingItemReader替代HibernatePagingItemReader,问题依旧存在 - 通过
@Autowired注入SessionFactory,在遍历实体关联关系的代码前后执行openSession/closeSession操作
受数据模型限制,我无法在单次查询中fetch所有关联关系,因此需要使用有状态的session(不过部分关联我是希望可以提前fetch的)。
问题根因
分页类ItemReader的默认实现中,每一页数据读取完成后就会关闭对应的Session/EntityManager,读取到的实体就变为脱管状态。虽然ItemProcessor确实运行在Chunk的事务中,但这个事务对应的是新的持久上下文,和读取实体时的上下文无关,因此访问未加载的代理对象就会抛出无Session的懒加载异常。
之前的尝试不生效的原因:
- 切换JpaPagingItemReader没用:它的分页逻辑和HibernatePagingItemReader一致,同样会在分页读取完成后关闭EntityManager
- 手动打开Session没用:新打开的Session中没有读取出来的实体的代理上下文,代理对象关联的是已经关闭的旧Session,新Session无法识别这个代理,因此还是会报错
解决方案
方案1(优先推荐,性能最优)
尽可能在Reader的查询语句中使用fetch join提前加载你确定会用到的关联,比如:
from Taxon t left join fetch t.organisation left join fetch t.otherCommonField
能提前加载的关联都提前加载,减少运行时懒加载的开销,剩余少量无法提前fetch的关联可以搭配方案2使用。
方案2(适配无法全量fetch的场景)
将Hibernate Session绑定到整个Chunk的执行生命周期,让读、处理、写全流程使用同一个Session。实现方式如下:
- 注册OpenSessionInterceptor Bean
import org.springframework.orm.hibernate5.support.OpenSessionInterceptor; @Bean public OpenSessionInterceptor openSessionInterceptor(SessionFactory sessionFactory) { OpenSessionInterceptor interceptor = new OpenSessionInterceptor(); interceptor.setSessionFactory(sessionFactory); return interceptor; }
- 将拦截器添加到对应Step的通知链中
@Bean public Step reindexStep(JobRepository jobRepository, PlatformTransactionManager transactionManager, HibernatePagingItemReader<Taxon> reader, TaxonToSolrInputDocumentProcessor processor, ItemWriter<SolrInputDocument> writer, OpenSessionInterceptor openSessionInterceptor) { return new StepBuilder("reindexStep", jobRepository) .<Taxon, SolrInputDocument>chunk(100, transactionManager) .reader(reader) .processor(processor) .writer(writer) .interceptor(openSessionInterceptor) .build(); }
如果使用JPA则对应替换为OpenEntityManagerInViewInterceptor,注入EntityManagerFactory即可,配置逻辑完全一致。
配置完成后整个Chunk执行过程中Session会保持打开状态,ItemProcessor中访问关联时就可以正常懒加载了。
内容的提问来源于stack exchange,提问作者Malcolm

