在内部服务中捕获 AccessDeniedException 时防止事务回滚

joh*_*inp 5 java spring hibernate spring-transactions

我有 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;
    }
}
Run Code Online (Sandbox Code Playgroud)
@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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

我期望它getRecentReadBooks()会返回一个 2 的列表Book,其中包含书 #1 和书 #3 的信息。但是,该方法失败,但出现以下异常:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
Run Code Online (Sandbox Code Playgroud)

经过一番研究,我发现这和交易传播有关系。即使我使用 try-catch 来处理 in ,该转换也会被标记为“仅回滚AccessDeniedExceptiongetRecentReadBooks()

这个问题似乎可以通过将@Transactional注释更改为getBook()来解决:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
Run Code Online (Sandbox Code Playgroud)

或者

    @Transactional(noRollbackFor = AccessDeniedException.class)
    public Book getBook(Long bookId) {
        // ...
    }
Run Code Online (Sandbox Code Playgroud)

但是我想知道是否可以通过仅修改RecentRecordService. 毕竟是想要自己RecentRecordService处理的人。AccessDeniedException

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

R.G*_*R.G 0

引用自文档

默认情况下,事务将在 RuntimeException 和 Error 上回滚,但不会在已检查异常(业务异常)上回滚

AccessDeniedException是 RuntimeException 的子类

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

更新

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

AccessDeniedException :如果 Authentication 对象不拥有所需的权限,则抛出该异常。

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