Spring中,同一个类中的方法调用被@Transaction注解修饰的方法会失效?

201

我对Spring事务很陌生。我发现了一些奇怪的事情,可能是我没有理解清楚。

我想在方法级别上使用事务,并且我有一个调用方方法在同一类中,但似乎它不喜欢那样做,必须从单独的类中调用。我不明白为什么会这样。

如果有人知道如何解决这个问题,我将不胜感激。我想使用相同的类来调用注释的事务方法。

以下是代码:

public class UserService {

    @Transactional
    public boolean addUser(String userName, String password) {
        try {
            // call DAO layer and adds to database.
        } catch (Throwable e) {
            TransactionAspectSupport.currentTransactionStatus()
                    .setRollbackOnly();

        }
    }

    public boolean addUsers(List<User> users) {
        for (User user : users) {
            addUser(user.getUserName, user.getPassword);
        }
    } 
}

3
看看“TransactionTemplate”方法:https://dev59.com/fm855IYBdhLWcg3wXC19#52989925 - Ilya Serbis
3
关于为什么自我调用不起作用,请参阅8.6代理机制 - Jason Law
@Mike,你是怎么发现这个交易没有生效的?是抛出了异常吗? - Marco Sulla
10个回答

137

这是 Spring AOP(动态对象和 cglib)的限制。

如果您配置 Spring 使用 AspectJ 来处理交易,您的代码将正常工作。

最简单且可能最好的替代方法是重构您的代码。例如,一个处理用户的类和一个处理每个用户的类。然后使用 Spring AOP 的 默认 事务处理即可。


处理事务的AspectJ配置技巧

为了让Spring使用AspectJ来处理事务,您必须将模式设置为AspectJ:

<tx:annotation-driven mode="aspectj"/>

如果您正在使用Spring的3.0以下版本,则还必须将此内容添加到您的Spring配置中:
<bean class="org.springframework.transaction.aspectj
        .AnnotationTransactionAspect" factory-method="aspectOf">
    <property name="transactionManager" ref="transactionManager" />
</bean>

谢谢您提供的信息。目前我已经重构了代码,但您能否请给我提供一个使用AspectJ的示例或一些有用的链接呢?先谢谢了。Mike。 - Mike
在我的答案中添加了特定于事务的AspectJ配置。希望能有所帮助。 - Espen
15
好的!顺便说一句:如果您能将我的问题标记为最佳答案并给我一些点数,那就太好了(绿色勾号)。 - Espen
11
Spring Boot配置:@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)意思是启用Spring Boot事务管理,使用AspectJ作为通知模式。 - VinyJones
1
为什么AspectJ不是处理事务的默认选择? - Alex78191
@Alex78191 我知道这不是你问题的完整答案,但在我添加了VinyJones提到的那行代码后,我的代码库中出现了一些非常神秘的LazyInitializationExceptions,因此它似乎不是一个可以毫无思考地应用的纯升级。 - julaine

103
在Java 8+中,有一种可能性,出于以下原因,我更喜欢这种可能性:
@Service
public class UserService {

    @Autowired
    private TransactionHandler transactionHandler;

    public boolean addUsers(List<User> users) {
        for (User user : users) {
            transactionHandler.runInTransaction(() -> addUser(user.getUsername, user.getPassword));
        }
    }

    private boolean addUser(String username, String password) {
        // TODO call userRepository
    }
}

@Service
public class TransactionHandler {

    @Transactional(propagation = Propagation.REQUIRED)
    public <T> T runInTransaction(Supplier<T> supplier) {
        return supplier.get();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public <T> T runInNewTransaction(Supplier<T> supplier) {
        return supplier.get();
    }
}

这种方法具有以下优点:
  1. 它可以应用于私有方法。因此,您无需将方法公开以满足Spring的限制,从而不必破坏封装性。

  2. 同一个方法可以在不同的事务传播中被调用,由调用者决定选择合适的传播方式。比较以下两行代码:

     transactionHandler.runInTransaction(() -> userService.addUser(user.getUserName, user.getPassword));
    
     transactionHandler.runInNewTransaction(() -> userService.addUser(user.getUserName, user.getPassword));
    
  3. 这样更明确,因此更易读。


5
太棒了!它避免了Spring注释带来的所有陷阱。我喜欢! - Frank Hopkins
2
听起来很棒!我想知道是否有一些注意事项? - ch271828n
1
非常好。我也使用了这个解决方案,只是有一个小区别:我在TransactionHandler中的方法命名为runInTransactionSupplierrunInNewTransactionSupplier。这样可以留下添加类似但返回void的方法的可能性。 - burebista
3
@burebista,你做错了,可以定义两个同名的方法,其中一个接受供应商并返回T,另一个接受可运行对象并返回void。 - roma2341
1
transactionTemplate 是为了使事务更加手动化,这本身并不是坏事,而且更加灵活,但它仍然比只使用 @Transactional 注解更冗长。 - ses
显示剩余7条评论

76
这里的问题在于,Spring的AOP代理不是扩展而是包装您的服务实例以拦截调用。这会导致从您的服务实例内部对 "this" 的任何调用直接在该实例上调用且无法被包装代理拦截(代理甚至不知道任何这样的调用)。已经提到了一种解决方案。另一种巧妙的方法是简单地让Spring将服务实例注入到服务本身中,并在注入的实例上调用您的方法,该实例将是处理事务的代理。但请注意,如果您的服务bean不是单例,则可能会产生负面影响:
<bean id="userService" class="your.package.UserService">
  <property name="self" ref="userService" />
    ...
</bean>

public class UserService {
    private UserService self;

    public void setSelf(UserService self) {
        this.self = self;
    }

    @Transactional
    public boolean addUser(String userName, String password) {
        try {
        // call DAO layer and adds to database.
        } catch (Throwable e) {
            TransactionAspectSupport.currentTransactionStatus()
                .setRollbackOnly();

        }
    }

    public boolean addUsers(List<User> users) {
        for (User user : users) {
            self.addUser(user.getUserName, user.getPassword);
        }
    } 
}

3
如果你决定选择这种方式(无论这是否是良好的设计),并且不使用构造函数注入,请确保参考 这个问题 - Jeshurun
如果 UserService 具有单例作用域怎么办?如果它是同一个对象呢? - Yan Khonski

40

使用Spring 4,可以自动注入

@Service
@Transactional
public class UserServiceImpl implements UserService{
    @Autowired
    private  UserRepository repository;

    @Autowired
    private UserService userService;

    @Override
    public void update(int id){
       repository.findOne(id).setName("ddd");
    }

    @Override
    public void save(Users user) {
        repository.save(user);
        userService.update(1);
    }
}

2
最佳答案!!谢谢 - mjassani
7
请您核对一下,我可能错了,但这种模式真的容易出错,虽然它能够工作。它更像是展示Spring框架的能力,对吗?不熟悉“这个Bean调用”行为的人可能会意外删除自动装配的Bean(毕竟方法可通过“this.”获得),这可能会导致难以一眼发现的问题。甚至在问题被发现之前,它可能已经运行到了生产环境中。 - pidabrow
6
@pidabrow,你说得对,这是一个很严重的反模式,而且一开始并不明显。因此,如果可以的话,应该尽量避免使用它。如果必须要使用同一类中的方法,那么就尝试使用更强大的AOP库,比如AspectJ。 - Almas Abdrazak
对我来说,仍然看起来很生硬。 - ses
将一个类定义为纯粹的 @Transactional 可能会导致一些不可预测的错误,因此应该小心使用。 - fatih

7
这是我对“自调用”问题的解决方案:
public class SBMWSBL {
    private SBMWSBL self;

    @Autowired
    private ApplicationContext applicationContext;

    @PostConstruct
    public void postContruct(){
        self = applicationContext.getBean(SBMWSBL.class);
    }

    // ...
}

给我 org.springframework.beans.factory.BeanCurrentlyInCreationException: 创建名为 'xy' 的bean时出错:请求的bean当前正在创建中:是否存在无法解决的循环引用? - JRA_TLL

1
你可以在同一类中自动装配BeanFactory并执行:

getBean(YourClazz.class)

它将自动代理您的类,并考虑到您的@Transactional或其他aop注释。

3
这被认为是一种不好的做法。即使将bean递归注入到自身中也更好。在代码内部使用getBean(clazz)会产生紧密耦合和对Spring ApplicationContext类的强依赖。此外,通过类获取bean可能在Spring包装bean的情况下无法正常工作(类可能已更改)。 - Vadim Kirilchuk

1
以下是需要翻译的内容:

对于仅在同一类中使用方法调用的小型项目,我会这样做。强烈建议进行内部代码文档编写,以免让同事感到奇怪。但它适用于单例模式,易于测试,简单快捷,并且避免了全面的AspectJ插装。但是,对于更重度使用,我建议采用Espens答案中描述的AspectJ解决方案。

@Service
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
class PersonDao {

    private final PersonDao _personDao;

    @Autowired
    public PersonDao(PersonDao personDao) {
        _personDao = personDao;
    }

    @Transactional
    public void addUser(String username, String password) {
        // call database layer
    }

    public void addUsers(List<User> users) {
        for (User user : users) {
            _personDao.addUser(user.getUserName, user.getPassword);
        }
    }
}

0

这个问题与Spring如何加载类和代理有关。除非你将内部方法/事务编写到另一个类中,或者进入其他类然后再返回你的类并编写嵌套的事务方法,否则它将无法工作。

总之,Spring代理不允许你面临的情况。你必须在其他类中编写第二个事务方法。


0
使用Spring 2.5.15进行测试,你可以使用@Resource进行自我调用。请参考下面的示例。
@Service
@RequiredArgsConstructor
public class Example {
   private final EntityManager entityManager;
   
   @Resource
   private Example self;


   @Transactional(propagation = Propagation.REQUIRES_NEW)
   public void testSelf(){
      self.transaction();
   }


   @Transactional(propagation = Propagation.SUPPORTS)
   public void transaction(){
       entityManager.persist(new MyEntity(28L));
   }

测试类:
@SpringBootTest
class ExampleTest {
    @Autowired
    private Example example;
    @Autowired
    private EntityManager entityManager;

    @Test
    void testSelf(){
        transactionHandler.testSelf();
        assertNotNull(entityManager.find(Estado.class, 28L));
    }
}

-1

使用AspectJ或其他方式没有意义,只需使用AOP即可。因此,我们可以在addUsers(List<User> users)上添加@Transactional来解决当前问题。

public class UserService {
    
    private boolean addUser(String userName, String password) {
        try {
            // call DAO layer and adds to database.
        } catch (Throwable e) {
            TransactionAspectSupport.currentTransactionStatus()
                    .setRollbackOnly();

        }
    }

    @Transactional
    public boolean addUsers(List<User> users) {
        for (User user : users) {
            addUser(user.getUserName, user.getPassword);
        }
    } 
}

这改变了原始逻辑!现在添加整个用户列表在单个事务中运行,这不是OP想要的! - Honza Zidek

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