Какие архитектурные подходы решают проблему транзакций при вызове методов внутри одного класса в Java
Что проверяют
Знаете ли вы, что @Transactional в Spring работает через прокси, и почему вызов this.method() внутри класса транзакцию не открывает. Следом почти всегда просят назвать способы обхода и выбрать один для реального кода.
Как отвечать
Сначала механизм. Spring оборачивает бин прокси (JDK dynamic proxy по интерфейсу или CGLIB-подкласс), и транзакционная логика живёт в перехватчике на прокси. Внешний вызов идёт через прокси, а вызов из соседнего метода того же объекта идёт по this напрямую, мимо перехватчика. Поэтому @Transactional(propagation = REQUIRES_NEW) на внутреннем методе молча игнорируется, и запись аудита откатится вместе с основной операцией.
Способы решения, от предпочтительного:
- вынести метод в отдельный бин: самый чистый вариант, заодно проясняет ответственность. Например,
AuditWriterс собственной транзакцией; - программное управление через
TransactionTemplate:transactionTemplate.execute(status -> ...)задаёт границу транзакции явно, без прокси; - самовнедрение: бин получает ссылку на свой же прокси и вызывает
self.method(). Циклические ссылки в Spring Boot по умолчанию запрещены, поэтому обычно через@LazyилиObjectProvider; AopContext.currentProxy()при@EnableAspectJAutoProxy(exposeProxy = true): работает, но привязывает бизнес-код к Spring AOP;- режим AspectJ:
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)с compile-time или load-time weaving. Аспект вшивается в байт-код, и самовызов тоже становится транзакционным, но сборка усложняется.
Хорошо добавить про видимость: через прокси private-методы не перехватываются вовсе, а для CGLIB-прокси Spring 6 поддерживает ещё protected и package-private.
Где спотыкаются
Не упоминают прокси и перечисляют «@Transactional плюс программные транзакции». Главный вопрос остаётся без ответа.
Ловят исключение в catch и не пробрасывают его. Транзакция тогда фиксируется, а по умолчанию откат вообще идёт только на RuntimeException и Error: для проверяемых исключений нужен rollbackFor.
Предлагают самовнедрение как основной путь. Это рабочий костыль, а интервьюер ждёт, что вы сначала предложите разделить класс.