You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为回调函数正确应用@Transactional注解?

Spring事务代理失效原因及同服务内回调的事务解决方案

问题场景代码

PersonService

@Service
class PersonService(
    private val personRepository: PersonRepository,
    private val schedulerService: SchedulerService,
) {

    fun someMethod() {
        schedulerService.scheduleTask(this::someTransactionalMethod)
    }

    @Transactional
    fun someTransactionalMethod(int: Int) {
        personRepository.streamAllBy().forEach {
            println(it)
        }
    }
}

SchedulerService

@Service
class SchedulerService {

    fun scheduleTask(task: Task) {
        task.execute(5)
    }
}

Task函数式接口

fun interface Task {
    fun execute(int: Int)
}

抛出异常

org.springframework.dao.InvalidDataAccessApiUsageException: You're trying to execute a streaming query method without a surrounding transaction that keeps the connection open so that the Stream can actually be consumed; Make sure the code consuming the stream uses @Transactional or any other way of declaring a (read-only) transaction

疑问

调用PersonService.someMethod()时触发上述异常,原以为调用方(SchedulerService)和被调用方法分属不同Bean,事务代理应该生效,但实际失效。将someTransactionalMethod移至第三个Service,通过thirdService::someTransactionalMethod传入时代理正常工作。


原因分析

Spring的事务代理基于Bean的代理实例实现,默认使用JDK动态代理(针对接口)或CGLIB代理(针对类)。当你通过this::someTransactionalMethod传递方法引用时,这里的this是PersonService的原始对象(而非Spring生成的代理对象)——Bean内部引用自身方法时,本质是直接调用原始类逻辑,不会经过代理层,因此@Transactional注解不会被Spring事务拦截器处理,自然无法开启事务。

而将方法移至第三个Service后,thirdService是Spring容器管理的代理对象,方法引用指向代理类的方法,调用时会经过事务拦截器,从而正确触发事务逻辑。

解决方案

方案1:注入自身代理实例

在PersonService中注入自身的代理对象,通过代理对象获取方法引用:

@Service
class PersonService(
    private val personRepository: PersonRepository,
    private val schedulerService: SchedulerService,
    // 注入自身代理实例
    private val self: PersonService
) {

    fun someMethod() {
        // 使用代理对象的方法引用
        schedulerService.scheduleTask(self::someTransactionalMethod)
    }

    @Transactional
    fun someTransactionalMethod(int: Int) {
        personRepository.streamAllBy().forEach {
            println(it)
        }
    }
}

Spring会自动注入代理实例,无需额外配置(需确保代理模式正常生效)。

方案2:手动获取代理对象

通过AopContext.currentProxy()获取当前代理对象,需先开启代理暴露:

  1. 在启动类/配置类添加注解:
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
class YourApplication

fun main(args: Array<String>) {
    runApplication<YourApplication>(*args)
}
  1. 修改PersonService逻辑:
@Service
class PersonService(
    private val personRepository: PersonRepository,
    private val schedulerService: SchedulerService,
) {

    fun someMethod() {
        val proxy = AopContext.currentProxy() as PersonService
        schedulerService.scheduleTask(proxy::someTransactionalMethod)
    }

    @Transactional
    fun someTransactionalMethod(int: Int) {
        personRepository.streamAllBy().forEach {
            println(it)
        }
    }
}

方案3:抽离事务方法到独立Bean

将带@Transactional的方法放到单独的Service类中,通过该Bean的方法引用传递,确保调用经过代理层(即你已验证有效的方式)。

关键总结

Spring事务生效的核心是必须通过代理对象调用被@Transactional标记的方法。Bean内部直接引用自身方法(包括方法引用、this调用)会绕过代理,导致事务失效,必须确保方法引用指向代理对象的方法,才能触发事务拦截逻辑。

内容的提问来源于stack exchange,提问作者szmozes

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 19:16:24