Spring单例Bean后台线程报错:No EntityManager with actual transaction available
首先来看你遇到的错误日志:
ERROR c.r.r.c.uploader.RWDataUploader - Exception at ObjectUploaderMonitor.wait No EntityManager with actual transaction available for current thread - cannot reliably process 'merge' call javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'merge' call at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:282) at com.sun.proxy.$Proxy230.merge(Unknown Source) at com.retailwave.rwos.compartment.uploader.RWDataUploader.uploadTxnToServer(RWDataUploader.java:106) at com.retailwave.rwos.compartment.uploader.RWDataUploader.startUpload(RWDataUploader.java:82) at com.retailwave.rwos.compartment.uploader.RWDataUploader$1.run(RWDataUploader.java:57)
问题根源分析
这个异常的核心原因是Spring的@Transactional注解依赖线程上下文来管理事务,而你用java.util.Timer创建的后台线程并不在Spring的事务上下文范围内。也就是说,uploadTxnToServer方法上的@Transactional注解根本没有生效,当你调用em.merge(openDevnt)时,当前线程没有绑定任何事务,自然就抛出了这个异常。
另外还有个小问题:你原代码里的TimerTask执行一次后就调用了bgProcessTimer.cancel(),这会导致定时器直接停止,任务只跑一次,这应该不是你想要的持续上传逻辑吧?
解决方案1:改用Spring自带的任务调度框架(推荐)
Spring的@Scheduled注解是专门用来处理定时任务的,它会自动管理线程上下文,让事务注解正常生效,步骤如下:
- 先在你的Spring配置类上开启调度支持:
@Configuration @EnableScheduling public class AppConfig { // 其他配置内容 }
- 修改
RWDataUploader,去掉所有Timer相关代码,改用@Scheduled:
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) @Singleton @Component public class RWDataUploader { protected org.slf4j.Logger log = LoggerFactory.getLogger(RWDataUploader.class); @PersistenceContext(type = PersistenceContextType.TRANSACTION) private EntityManager em; protected EntityManager getEntityManager() { return em; } private boolean isActive = true; private int errorCount = 0; private long sleepTime = 20000l; @PostConstruct public void init() { log.info("RWDataUploadClient initialized {} ", this); } @PreDestroy public void close() { stopUpload(); log.info("RWDataUploadClient closed {} ", this); } public void stopUpload() { isActive = false; } // 初始延迟10秒启动,之后每3秒执行一次 @Scheduled(initialDelay = 10000, fixedRate = 3000) public void startUpload() { if (!isActive) { log.info("Uploader client is stopped"); return; } try { uploadTxnToServer(); log.debug("UploadTxnToServer Done, waiting for next cycle"); } catch (Exception ex) { errorCount++; log.error("Exception during upload process: {}", ex.getMessage(), ex); if (errorCount == 1) { isActive = false; } } } @Transactional private void uploadTxnToServer() { List<RWDataEvent> openDevnts = getPendingDataEvents(); for (RWDataEvent openDevnt : openDevnts) { log.info("Uploaded DE {} ", openDevnt); // 这里写上传到远程服务器的逻辑 // 上传完成后标记状态 openDevnt.setProcessedStatusCode("UPLOADED"); em.merge(openDevnt); } } private List<RWDataEvent> getPendingDataEvents() { Query nq = em.createNamedQuery("RWDataEvent.findByProcessedStatusCode"); nq.setParameter("processedStatusCode", "WAIT_UPLOAD"); return nq.getResultList(); } }
这样修改后,Spring会负责管理任务的线程,@Transactional注解就能正常生效,因为Spring会在调度任务时把事务上下文绑定到当前线程。而且也不用自己手动处理wait和notify的逻辑,@Scheduled已经帮我们搞定了执行间隔。
解决方案2:手动管理事务(如果必须保留Timer)
如果因为某些原因你必须使用java.util.Timer,那可以手动获取事务管理器来控制事务:
- 先注入Spring的事务管理器:
@Autowired private PlatformTransactionManager transactionManager;
- 修改
uploadTxnToServer方法,去掉@Transactional,手动开启和提交事务:
private void uploadTxnToServer() { // 定义事务属性,设置传播行为为REQUIRED(和@Transactional默认一致) DefaultTransactionDefinition transactionDef = new DefaultTransactionDefinition(); transactionDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status = transactionManager.getTransaction(transactionDef); try { List<RWDataEvent> openDevnts = getPendingDataEvents(); for (RWDataEvent openDevnt : openDevnts) { log.info("Uploaded DE {} ", openDevnt); // 上传到远程服务器的逻辑 openDevnt.setProcessedStatusCode("UPLOADED"); em.merge(openDevnt); } // 提交事务 transactionManager.commit(status); } catch (Exception ex) { // 发生异常回滚事务 transactionManager.rollback(status); throw ex; // 抛出异常让上层处理 } }
同时记得把原TimerTask里的bgProcessTimer.cancel()去掉,否则任务只执行一次就停了。
额外提示
java.util.Timer是JDK自带的基础定时任务工具,功能比较有限,而且异常处理、线程管理都不如Spring的调度框架灵活。如果没有特殊需求,优先用Spring的@Scheduled或者更强大的@Async结合调度来实现。- 你原代码里的
objectUploaderMonitor锁和wait逻辑,在改用Spring调度后就不需要了,因为@Scheduled已经帮我们控制了任务的执行频率。
内容的提问来源于stack exchange,提问作者vels4j

