Как Spring создает прокси для классов с транзакциями
Что проверяют
Понимаете ли вы, что @Transactional работает через прокси, и знаете ли главное следствие: вызов внутри своего же класса транзакцию не открывает.
Как отвечать
Spring не переписывает ваш класс. Он оборачивает бин прокси-объектом, который перехватывает вызовы, открывает транзакцию перед методом и фиксирует или откатывает её после.
Два способа создания прокси: динамический прокси JDK, если класс реализует интерфейс, и подкласс через CGLIB в остальных случаях. В Spring Boot по умолчанию используется второй вариант даже при наличии интерфейсов.
Отсюда практические правила, и это самая ценная часть ответа:
- самовызов не работает: если метод класса вызывает другой метод того же класса напрямую, вызов идёт мимо прокси, и аннотация игнорируется. Лечится вынесением в отдельный бин или получением ссылки на себя через прокси;
privateиfinalметоды не перехватываются: у CGLIB нет способа их переопределить. По той же причинеfinalкласс нельзя обернуть;- аннотация на интерфейсе работает только с прокси JDK, поэтому её ставят на реализации;
- откат по умолчанию происходит только на непроверяемых исключениях. Проверяемое исключение транзакцию не откатывает, пока не указано
rollbackFor; - режим распространения (
REQUIRED,REQUIRES_NEWи другие) определяет, присоединиться к существующей транзакции или начать новую, иREQUIRES_NEWтребует именно отдельного прокси-вызова; - транзакция привязана к потоку: работа, унесённая в другой поток, к ней не относится.
Где спотыкаются
Вызывают транзакционный метод изнутри того же класса и часами ищут, почему нет отката.
Ставят @Transactional на приватный метод.
Ловят исключение внутри транзакционного метода и не пробрасывают его дальше, из-за чего откат не происходит.