Spring @Transactional属性在私有方法上是否起作用?

266
如果我在Spring bean的私有方法上有一个@Transactional注解,这个注解会有任何效果吗?
如果@Transactional注解在公共方法上,它会起作用并开启一个事务。
public class Bean {
  public void doStuff() {
     doPrivateStuff();
  }
  @Transactional
  private void doPrivateStuff() {

  }
}

...

Bean bean = (Bean)appContext.getBean("bean");
bean.doStuff();
8个回答

263
你的问题的答案是否定的 - 如果用来注释私有方法,@Transactional将不起作用。代理生成器将忽略它们。
这在Spring手册第10.5.6章中有记录:
块引用: 方法的可见性和@Transactional 当使用代理时,您应该仅将@Transactional注释应用于具有公共可见性的方法。如果您使用@Transactional注释保护、私有或包可见方法,则不会引发错误,但是注释的方法不会展示配置的事务设置。如果需要注释非公共方法,请考虑使用AspectJ(请参见下文)。

你确定吗?我不认为这会有任何影响。 - willcodejavaforfood
1
如果代理方式是Cglib呢? - lily
1
我使用了以下正则表达式 @Transactional([^{](?!public))+ \{ 来查找在我们的代码库中不会产生任何影响(因为它们位于私有、受保护或包私有方法上)的可能注释。当然,它不能找到对公共方法的“无代理自引用”调用 - 是否有插件或其他工具可以发现这些问题? - icyerasor
我的父表有时不会更新:https://dev59.com/BncPtIcB2Jgan1znN7bz - Hacke

239
问题并不是私有或公共,而是:它如何被调用以及你使用哪种AOP实现!如果您使用(默认的)Spring代理AOP,则只有在调用经过代理时,Spring提供的所有AOP功能(如@Transactional)才会被考虑。 - 如果注释的方法是从另一个bean调用的话,通常情况下就是这样。
这有两个含义:
- 私有方法不能从另一个bean中调用(例外是反射),它们的@Transactional注释不会被考虑。
- 如果该方法是公共的,但是它是从同一个bean中调用的,则也不会被考虑(只有在使用(默认的)Spring代理AOP的情况下才正确)。
请参阅Spring参考文档第9.6章“代理机制”http://docs.spring.io/spring/docs/3.2.4.RELEASE/spring-framework-reference/html/aop.html#aop-proxying
我认为你应该使用AspectJ模式,而不是Spring代理,这将解决这个问题。并且AspectJ事务方面甚至编织到私有方法中(针对Spring 3.0进行了检查)。

5
两个观点并非必然正确。第一个观点是不正确的——私有方法可以通过反射调用,但代理发现逻辑选择不这样做。第二个观点仅适用于基于接口的 JDK 代理,而不适用于基于 CGLIB 子类的代理。 - skaffman
3
这取决于目标是否使用接口。如果没有,将使用CGLIB。 - skaffman
1
请不要打我,但我认为如果您在私有方法上放置@Transactional,则说明您不知道自己在做什么。您不应该这样做。 - Lawrence
1
如果您想使用Spring代理[默认环境],请参考答案块中的链接,在doStuff()上放置注释,并使用((Bean) AopContext.currentProxy()).doPrivateStuff()调用doPrivateStuff()。如果需要传播[默认环境],它将在同一个事务中执行两种方法。 - Michael Ouyang
1
@MichaelOuyang:感谢您指出这个解决方案。但在引用的Spring文档中,这种方法是这样描述的:“下一种方法绝对是可怕的,我几乎不想指出它,因为它太可怕了。你可以(呛!)通过这样做完全将你类内的逻辑与Spring AOP联系起来。” - Ralph
显示剩余5条评论

37

默认情况下,@Transactional 属性仅在从 applicationContext 获取的引用上调用带有注解的方法时才起作用。

public class Bean {
  public void doStuff() {
    doTransactionStuff();
  }
  @Transactional
  public void doTransactionStuff() {

  }
}

这将打开一个事务:
Bean bean = (Bean)appContext.getBean("bean");
bean.doTransactionStuff();

这不会:

Bean bean = (Bean)appContext.getBean("bean");
bean.doStuff();

Spring参考文档:使用@Transactional

注意:在代理模式下(默认情况下),只有通过代理传入的“外部”方法调用将被拦截。这意味着,即使被调用的方法标记有@Transactional,在运行时,目标对象内的一个方法调用另一个方法不会导致实际事务!

如果您希望自调用也被包装在事务中,请考虑使用AspectJ模式(请参见下文)。在这种情况下,首先不会有代理;相反,目标类将被“编织”(即其字节码将被修改),以便在任何类型的方法上将@Transactional转换为运行时行为。


你的意思是 bean = new Bean(); 吗? - willcodejavaforfood
不行。如果我使用 new Bean() 创建 bean,那么注解将无法工作,除非使用 Aspect-J。 - Juha Syrjälä
2
谢谢!这解释了我观察到的奇怪行为。这种内部方法调用限制相当违反直觉... - manuel aldana

31
如果你需要在事务中包装一个私有方法而不想使用AspectJ,你可以使用TransactionTemplate
@Service
public class MyService {
    @Autowired
    private TransactionTemplate transactionTemplate;

    private void process() {
        transactionTemplate.executeWithoutResult(status -> processInTransaction());
    }

    private void processInTransaction(){
        //...
    }
}

1
很好展示了TransactionTemplate的用法,但请将第二个方法命名为“..RequiresTransaction”,而非“..InTransaction”。始终按你自己一年后想要阅读的方式来命名。另外我认为应该考虑是否真的需要第二个私有方法:要么直接将其内容放在匿名的“execute”实现中,要么如果变得混乱,这可能是将实现拆分为另一个服务的迹象,然后您可以对其进行注释“@Transactional”。 - Stuck
@Stuck,第二种方法确实不是必需的,但它回答了最初的问题,即如何在私有方法上应用Spring事务。 - loonis
是的,我已经为答案投过赞了,但我想分享一些关于如何应用它的上下文和想法,因为我认为从架构的角度来看,这种情况可能意味着设计存在缺陷。 - Stuck

19

是的,可以在私有方法上使用@Transactional,但正如其他人所提到的,这并不能直接奏效。您需要使用AspectJ。我花了一些时间才弄清楚如何让它起作用。我将分享我的结果。

我选择使用编译时织入而不是加载时织入,因为我认为这是一个更好的选择。此外,我正在使用Java 8,因此您可能需要调整一些参数。

首先,添加aspectjrt的依赖项。

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.8</version>
</dependency>

然后在Maven中添加AspectJ插件进行实际的字节码织入(这可能不是最小示例)。

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.8</version>
    <configuration>
        <complianceLevel>1.8</complianceLevel>
        <source>1.8</source>
        <target>1.8</target>
        <aspectLibraries>
            <aspectLibrary>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aspects</artifactId>
            </aspectLibrary>
        </aspectLibraries>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

最后将此添加到您的配置类中

@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)

现在你应该能够在私有方法上使用@Transactional。

对于这种方法有一个警告:您需要配置IDE以了解AspectJ,否则如果例如通过Eclipse运行应用程序,则可能无法正常工作。确保您针对直接的Maven构建进行测试以进行合理检查。


如果代理方法是cglib,就不需要实现一个公共方法的接口,那么私有方法上能否使用@Transactional注解呢? - lily
是的,它可以在私有方法上工作,而且不需要接口!只要AspectJ配置正确,它基本上保证了工作方法装饰器。正如user536161在他的答案中指出的那样,它甚至可以在自我调用上工作。这真的很酷,但也有点可怕。 - James Watkins

6

Spring Docs中解释说:

在代理模式下(默认情况下),只有通过代理传入的外部方法调用会被拦截。这意味着自调用实际上是指目标对象内的一个方法调用另一个目标对象的方法,即使所调用的方法标记有@Transactional,在运行时也不会导致实际事务。

如果您希望自调用也被包装成事务,请考虑使用AspectJ模式(请参见下表中的mode属性)。在这种情况下,首先不会有代理; 相反,目标类将被编织(即其字节码将被修改),以便在任何类型的方法上将@Transactional转化为运行时行为。

另一种方法是使用BeanSelfAware


你能否添加一个对BeanSelfAware的引用?它似乎不是Spring框架的类。 - asgs
假设,这是关于自我注入(提供一个被包装成代理的实例)的问题。您可以在https://dev59.com/e3A75IYBdhLWcg3wMmCo中看到示例。 - Ilya Serbis

4

3

@loonis建议使用TransactionTemplate相同,可以使用此辅助组件(Kotlin):

@Component
class TransactionalUtils {
    /**
     * Execute any [block] of code (even private methods)
     * as if it was effectively [Transactional]
     */
    @Transactional
    fun <R> executeAsTransactional(block: () -> R): R {
        return block()
    }
}

使用方法:

@Service
class SomeService(private val transactionalUtils: TransactionalUtils) {

    fun foo() {
        transactionalUtils.executeAsTransactional { transactionalFoo() }
    }

    private fun transactionalFoo() {
        println("This method is executed within transaction")
    }
}

不知道 TransactionTemplate 是否重用现有的事务,但这段代码肯定会这样做。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接