Mam 2 usługi: RecentRecordService i BookService.

@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;
    }
}

Załóżmy, że getRecentReadBookIds() zwraca [1, 2, 3].

Gdy użytkownik sesji ma uprawnienia do wszystkich identyfikatorów książek, które zwróciły z getRecentReadBookIds(), wszystko działa prawidłowo. getRecentReadBooks() zwróci listę 3 Book s.

Nagle właściciel książki nr 2 zmienił ustawienie jej uprawnień z „Publicznego” na „Prywatne”. Dlatego użytkownik sesji nie może już czytać książki nr 2.

Spodziewałem się, że getRecentReadBooks() zwróci listę 2 Book s, która zawiera informacje o książce nr 1 i książce nr 3. Jednak metoda nie powiodła się z następującym wyjątkiem:

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

Po kilku badaniach stwierdziłem, że ma to coś wspólnego z propagacją transakcji. Transakcja zostanie oznaczona jako „tylko wycofywanie”, nawet jeśli użyję try-catch do obsługi AccessDeniedException w getRecentReadBooks().

Wydaje się, że ten problem można rozwiązać, zmieniając adnotację @Transactional dla getBook() na:

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

Lub

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

Jednak zastanawiałem się, czy mogę rozwiązać problem, modyfikując tylko RecentRecordService. W końcu RecentRecordService jest tym, który chce samodzielnie poradzić sobie z AccessDeniedException.

Każda sugestia pomoże. Dzięki.

3
johnlinp 19 grudzień 2019, 18:45

2 odpowiedzi

Cytat z dokumentacji

Domyślnie transakcja zostanie wycofana w przypadku wyjątków RuntimeException i Error, ale nie w przypadku zaznaczonych wyjątków (wyjątków biznesowych)

AccessDeniedException jest podklasą RuntimeException

Jeśli w twoim przypadku użyty wyjątek AccessDeniedException to ramy wiosenne i zgodnie z wymaganiem, jeśli wyjątek dotyczący odmowy dostępu może być niestandardowym wyjątkiem biznesowym (a nie podklasą wyjątku środowiska wykonawczego), powinien działać bez adnotacji metody getBook (Long bookId) .

Aktualizuj

OP zdecydował się naprawić główną przyczynę. Ma to na celu uzasadnienie mojej odpowiedzi na wszelkie przyszłe odniesienia

AccessDeniedException: zgłaszany, jeśli obiekt uwierzytelniania nie ma wymaganych uprawnień.

Jak rozumiem, tutaj dostęp do książki opiera się na logice biznesowej, a nie na roli użytkownika. W takim scenariuszu odpowiedni byłby niestandardowy wyjątek biznesowy.

0
R.G 21 grudzień 2019, 12:00
Dziękuję, ale wiedziałem, że AccessDeniedException jest podklasą RuntimeException. Nie, nie mogę zrobić wyjątku od zaznaczonego wyjątku. Chcę wiedzieć, jak rozwiązać problem, modyfikując tylko RecentRecordService.
 – 
johnlinp
20 grudzień 2019, 02:57
Być może trzeba będzie zidentyfikować główną przyczynę i prawdopodobnie ją naprawić: stackoverflow.com/a/2007165/4214241
 – 
R.G
20 grudzień 2019, 03:05
Myślę, że znam przyczynę. Potrzebuję tylko odpowiedniej naprawy.
 – 
johnlinp
20 grudzień 2019, 05:55

Nie sądzę, aby można było go rozwiązać, modyfikując tylko RecentRecordService, ponieważ transakcja jest już oznaczona jako wycofanie w BookService z powodu zdarzenia RuntimeException w nim. A gdy TransactionStatus zostanie oznaczony jako wycofywanie, nie ma możliwości przywrócenia go do stanu bez wycofywania.

Musisz więc sprawić, by BookService#getBook() nie oznaczał transakcji jako wycofania przez:

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

}

Co oznacza, że transakcja nie zostanie oznaczona jako wycofanie bez względu na wyjątek. Ma to sens, ponieważ ta metoda ma być tylko do odczytu, więc nie ma sensu wycofywać zmian, gdy metoda nie ma niczego modyfikować.

0
Ken Chan 20 grudzień 2019, 13:46
Jeśli chcę, aby BookService#getBook() był niezmodyfikowany, czy ma sens, jeśli napiszę dla niego klasę opakowującą, powiedz BookServiceWrapper#getBook() i ustawię metodę opakowującą jako @Transactional(noRollbackFor = Throwable.class)?
 – 
johnlinp
21 grudzień 2019, 16:29
Nie powinno działać, jeśli po prostu użyjesz opakowania do opakowania BookService#getBook(), ale zachowaj BookService#getBook() niezmieniony, ponieważ nadal będzie oznaczać transakcję jako wycofanie, jeśli wystąpi w niej wyjątek.
 – 
Ken Chan
21 grudzień 2019, 20:09
Co powiesz na zrobienie metody opakowującej jako @Transactional(Propagation.NESTED)?
 – 
johnlinp
22 grudzień 2019, 04:30