仅修改RecentRecordService解决捕获AccessDeniedException时事务回滚问题
Great question! The root problem here is that with default transaction propagation (Propagation.REQUIRED), BookService.getBook() runs within the same transaction as RecentRecordService.getRecentReadBooks(). When getBook() throws an AccessDeniedException, Spring marks the entire transaction as rollback-only. Even if you catch the exception in getRecentReadBooks(), the transaction can't commit later, leading to the UnexpectedRollbackException.
Here are two reliable ways to resolve this by only modifying RecentRecordService:
1. Wrap BookService Calls in a New Transaction
Create a helper method in RecentRecordService that calls BookService.getBook() with Propagation.REQUIRES_NEW—this forces each book lookup to run in its own independent transaction. If one fails, only that sub-transaction rolls back, leaving the main transaction intact.
@Service public class RecentRecordService { private final BookService bookService; // Constructor injection public RecentRecordService(BookService bookService) { this.bookService = bookService; } @Transactional public List<Book> getRecentReadBooks() { List<Long> recentReadBookIds = getRecentReadBookIds(); List<Book> recentReadBooks = new ArrayList<>(); for (Long bookId : recentReadBookIds) { try { // Call the helper method with its own transaction Book book = getBookInIsolatedTransaction(bookId); recentReadBooks.add(book); } catch (AccessDeniedException e) { // Skip unauthorized books as intended } } return recentReadBooks; } // This method runs in a new, independent transaction @Transactional(propagation = Propagation.REQUIRES_NEW) protected Book getBookInIsolatedTransaction(Long bookId) { return bookService.getBook(bookId); } }
Why this works:
Propagation.REQUIRES_NEWstarts a brand new transaction for each call togetBookInIsolatedTransaction().- When
BookService.getBook()throws an exception, only this new transaction is rolled back. The main transaction (forgetRecentReadBooks()) remains unmarked for rollback and can commit successfully with the authorized books.
2. Use TransactionTemplate for Isolated Calls
If you prefer not to add a helper method, you can use TransactionTemplate to execute each BookService.getBook() call in a separate transaction. This achieves the same isolation as the first approach but uses programmatic transaction management instead of declarative.
@Service public class RecentRecordService { private final BookService bookService; private final TransactionTemplate transactionTemplate; // Constructor injection public RecentRecordService(BookService bookService, TransactionTemplate transactionTemplate) { this.bookService = bookService; // Configure the template to use REQUIRES_NEW propagation this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); } @Transactional public List<Book> getRecentReadBooks() { List<Long> recentReadBookIds = getRecentReadBookIds(); List<Book> recentReadBooks = new ArrayList<>(); for (Long bookId : recentReadBookIds) { try { Book book = transactionTemplate.execute(status -> { return bookService.getBook(bookId); }); recentReadBooks.add(book); } catch (AccessDeniedException e) { // Skip unauthorized books } } return recentReadBooks; } }
Why this works:
- The
TransactionTemplateis configured to useREQUIRES_NEW, so eachexecute()call runs in a separate transaction. - Exceptions from
getBook()only affect their own transaction, leaving the main transaction able to commit with valid books.
Key Takeaway
Both approaches isolate each book lookup into its own transaction, preventing a single unauthorized book from marking the entire main transaction as rollback-only. This lets you keep BookService unchanged while handling the exception gracefully in RecentRecordService.
内容的提问来源于stack exchange,提问作者johnlinp

