@Transactional на методе, который вызывает сам себя рекурсивно
Ловушка похожа на классическое self-invocation через this, но чуть тоньше: код работает годами, потому что рекурсия неглубокая, а проблема всплывает только когда меняют propagation.
Метод помечен @Transactional и вызывает себя рекурсивно для обработки дерева задач. Первый вызов идёт через proxy — Spring честно открывает транзакцию. Но каждый следующий рекурсивный вызов происходит через this, минуя proxy, то есть выполняется в контексте уже открытой транзакции без повторного прохода через транзакционную инфраструктуру.
Если propagation стоит по умолчанию, REQUIRED, разница незаметна — все вызовы просто присоединяются к одной физической транзакции, и итоговое поведение совпадает с ожидаемым. Проблема вскрывается, когда кто-то меняет propagation на REQUIRES_NEW, ожидая, что каждый рекурсивный шаг получит собственную транзакцию с независимым коммитом или роллбэком. Этого не происходит — вложенные вызовы продолжают жить в одной транзакции, потому что proxy для них не срабатывает.
@Transactional(propagation = Propagation.REQUIRES_NEW) public void processNode(TaskNode node) { save(node); for (TaskNode child : node.getChildren()) { processNode(child); } }
Что спросят следом: как исправить без изменения архитектуры класса. Правильный ответ — вынести рекурсивный вызов в отдельный бин и обращаться к нему через инжектированную зависимость, чтобы каждый вызов шёл через proxy. Альтернатива — AopContext.currentProxy(), но это требует exposeProxy=true и считается более хрупким решением.
Self-invocation ломает не только вызов метода из соседнего метода того же класса — рекурсия через this та же самая проблема, просто спрятанная глубже в логике.
Тренажёр: 600 вопросов, мок с таймером, план повторов
senior·base — что спрашивают на самом деле
В этом посте были ссылки, но мы их удалили по правилам Сетки