【问题标题】:Prevent transaction rollback when catching AccessDeniedException in inner service在内部服务中捕获 AccessDeniedException 时防止事务回滚
【发布时间】:2020-04-12 05:46:52
【问题描述】:

我有 2 个服务:RecentRecordServiceBookService

@Service
public class RecentRecordService {
    @Transactional
    public List<Book> getRecentReadBooks() {
        List<Long> recentReadBookIds = getRecentReadBookIds();
        List<Book> recentReadBooks = new ArrayList<>();

        for (Long bookId : recentReadBookIds) {
            try {
                Book book = bookService.getBook(bookId);
                recentReadBooks.add(book);
            } catch (AccessDeniedException e) {
                // skip
            }
        }

        return recentReadBooks;
    }
}
@Service
public class BookService {
    @Transactional
    public Book getBook(Long bookId) {
        Book book = bookDao.get(bookId);
        if (!hasReadPermission(book)) {
            throw new AccessDeniedException(); // spring-security exception
        }
        return book;
    }
}

假设getRecentReadBookIds() 返回[1, 2, 3]

当会话用户拥有从getRecentReadBookIds() 返回的所有图书 ID 的权限时,一切正常。 getRecentReadBooks() 将返回一个包含 3 个 Books 的列表。

突然,第 2 本书的所有者将其权限设置从“公共”更改为“私人”。因此,会话用户不能再阅读本书#2。

我期待getRecentReadBooks() 将返回一个包含 2 个Books 的列表,其中包含第 1 本书和第 3 本书的信息。但是,该方法失败并出现以下异常:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

经过一番研究,我发现它与事务传播有关。即使我使用 try-catch 处理 getRecentReadBooks() 中的 AccessDeniedException,事务也会被标记为“仅回滚”。

这个问题似乎可以通过把getBook()@Transactional注解改成:

    @Transactional(propagation = Propagation.NESTED)
    public Book getBook(Long bookId) {
        // ...
    }

    @Transactional(noRollbackFor = AccessDeniedException.class)
    public Book getBook(Long bookId) {
        // ...
    }

但是,我想知道是否可以仅通过修改RecentRecordService 来解决问题。毕竟RecentRecordService 是想自己处理AccessDeniedException 的人。

任何建议都会有所帮助。谢谢。

【问题讨论】:

    标签: java spring hibernate spring-transactions


    【解决方案1】:

    我认为仅通过修改RecentRecordService 是不可能解决的,因为该事务已经在BookService 中标记为回滚,因为其中发生了RuntimeException。而一旦TransactionStatus 被标记为回滚,就无法将其恢复为当前不回滚。

    因此,您必须使BookService#getBook() 不会将事务标记为回滚:

    @Transactional(noRollbackFor = Throwable.class)
    public Book getBook(Long bookId) {
    
    }
    

    这意味着无论发生什么异常,事务都不会被标记为回滚。这样做是有意义的,因为这个方法应该是只读的,所以当一个方法不应该修改任何东西时,回滚是没有意义的。

    【讨论】:

    • 如果我想让BookService#getBook() 不被修改,如果我为它写一个包装类,比如BookServiceWrapper#getBook() 并将包装方法设为@Transactional(noRollbackFor = Throwable.class),是否有意义?
    • 如果您只使用包装器来包装BookService#getBook() 但保持BookService#getBook() 不变,它应该不起作用,因为如果其中发生异常,它仍会将事务标记为回滚。
    • 将包装方法设为@Transactional(Propagation.NESTED)怎么样?
    【解决方案2】:

    引用documentation

    默认情况下,事务将在 RuntimeException 和 错误但不在检查异常(业务异常)上

    AccessDeniedException 是 RuntimeException 的子类

    如果在您的情况下使用的 AccessDeniedException 是 spring frameworks ,并且根据要求,如果拒绝访问的异常可以成为自定义业务异常(不是 Runtime Exception 的子类),它应该可以在没有注释的情况下工作 getBook(Long bookId) 方法。

    更新

    OP 决定解决根本原因。这是为了为将来的任何参考提供我的答案背后的理由

    AccessDeniedException : 如果 Authentication 对象没有被抛出 拥有所需的权限。

    根据我的理解,这里对一本书的访问是基于业务逻辑的,而不是特定于用户角色的。在这种情况下,自定义业务异常很合适。

    【讨论】:

    • 谢谢,但我知道 AccessDeniedException 是 RuntimeException 的子类。不,我不能对已检查的异常进行异常处理。我想知道的是一种只修改RecentRecordService即可解决问题的方法。
    • 您可能需要找出根本原因并修复它:stackoverflow.com/a/2007165/4214241
    • 我想我知道根本原因。我只需要对此进行适当的修复。
    猜你喜欢
    • 2014-07-17
    • 1970-01-01
    • 2010-12-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-02
    • 2019-01-21
    相关资源
    最近更新 更多