Почему @Transactional не работает при self-invocation
В Spring при таком вызове аннотация @Transactional (а также @Async, @Cacheable и др.) полностью игнорируется, и транзакция не открывается.
🟢 Junior Level
Self-invocation (само-вызов) — это ситуация, когда один метод класса вызывает другой метод того же самого класса напрямую через ключевое слово this (явное или неявное).
В Spring при таком вызове аннотация @Transactional (а также @Async, @Cacheable и др.) полностью игнорируется, и транзакция не открывается.
Наглядный пример дефекта
package com.example.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
// Внешний метод БЕЗ транзакции
public void processOrder(Long orderId) {
System.out.println("Шаг 1: Подготовка заказа");
// Внутренний вызов (Self-invocation): равносильно this.saveToDatabase()
saveToDatabase(orderId); // ❌ ОШИБКА: @Transactional НЕ СРАБОТАЕТ!
}
// Метод, требующий транзакции
@Transactional
public void saveToDatabase(Long orderId) {
System.out.println("Шаг 2: Запись в базу данных БЕЗ транзакции!");
// SQL-запросы выполнятся в режиме autocommit=true
}
}
Почему это происходит: Простая аналогия
Представьте, что транзакционный прокси — это секретарь на входе в кабинет директора, который проверяет документы и регистрирует визитеров. Когда клиент обращается к сервису «снаружи», он неизбежно проходит через секретаря (прокси). Но когда директор внутри кабинета берет ручку со своего стола, секретарь в этом не участвует — вызов происходит напрямую внутри объекта.
🟡 Middle Level
Архитектурная причина: JVM invokevirtual и обход AOP-прокси
Spring AOP основан на паттерне Proxy (Заместитель). Внедряемый в другие компоненты бин — это не оригинальный экземпляр класса, а сгенерированный прокси-объект.
ВНЕШНИЙ ВЫЗОВ (Работает штатно):
[ Client ] ──> [ OrderService Proxy ] ──> [ TransactionInterceptor ] ──> [ Target OrderService ]
(Открывает транзакцию) (saveToDatabase)
САМО-ВЫЗОВ (Self-Invocation — НЕ работает):
[ Client ] ──> [ OrderService Proxy ]
│
▼
[ Target OrderService ]
│
└──> processOrder()
│
└── (this.saveToDatabase()) ──> минуя Proxy!
- Клиент вызывает
orderService.processOrder(). - Вызов перехватывается прокси. Поскольку на методе
processOrder()нет@Transactional, прокси просто передает вызов реальному объектуTarget. - Внутри метода
processOrder()выполняется вызовsaveToDatabase(). - На уровне байткода JVM выполняется инструкция
invokevirtualпо указателюthis. - Ссылка
thisуказывает на реальный целевой объект в памяти, а не на прокси-обертку. - Вызов попадает напрямую в vtable целевого объекта, минуя
TransactionInterceptor. Транзакция не открывается!
Почему CGLIB не спасает ситуацию
Частое заблуждение разработчиков: «CGLIB создает подкласс, значит, переопределяет метод и перехватывает все вызовы».
Это не так:
- CGLIB генерирует класс-потомок:
OrderService$$SpringCGLIB$$0 extends OrderService. - В прокси переопределен метод
saveToDatabase(), содержащий обращение кMethodInterceptor. - Но реальный бизнес-код метода
processOrder()исполняется в контексте оригинального экземпляра суперкласса. - Когда внутри
processOrder()вызываетсяthis.saveToDatabase(), ссылкаthisвнутри метода родителя указывает на самого себя, а не на интерфейс перехватчика CGLIB.
Какие еще аннотации не работают при Self-Invocation
Проблема само-вызова актуальна для абсолютно всех AOP-аннотаций Spring:
| Аннотация | Последствие при self-invocation |
|---|---|
@Transactional |
Данные сохраняются без транзакции; REQUIRES_NEW игнорируется |
@Async |
Метод выполняется синхронно в вызывающем потоке |
@Cacheable / @CacheEvict |
Кэш игнорируется, повторный вызов всегда идет в базу данных |
@PreAuthorize / @Secured |
Проверка прав доступа пропускается (критическая уязвимость безопасности!) |
@Retryable |
При исключении повторные попытки не выполняются |
@Validated |
Валидация аргументов метода не запускается |
🔴 Senior Level
Искажение параметров Propagation при Self-Invocation
Если внешний метод outer() уже аннотирован @Transactional, а внутренний inner() помечен специфической стратегией распространения:
@Service
public class PaymentService {
@Transactional // Транзакция 1 (REQUIRED)
public void processPayment() {
// ... списание средств ...
try {
logAuditInfo(); // Self-invocation!
} catch (Exception ex) {
// Пытаемся подавить ошибку аудита, чтобы платеж прошел
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAuditInfo() {
// ОШИБКА: REQUIRES_NEW НЕ СРАБОТАЕТ!
// Метод выполнится в рамках ТРАНЗАКЦИИ 1, а не в новой транзакции!
auditRepo.save(new AuditRecord());
}
}
Последствия в Production:
- Разработчик ожидает, что
logAuditInfo()выполнится в отдельной транзакции и сохранится, даже если основной платеж упадет, либо его падение не сломает платеж. - В реальности из-за self-invocation метод
logAuditInfo()выполняется в физическом контексте внешней транзакции. - Если
logAuditInfo()падает с ошибкой, сессия помечается какRollback Only. Внешняя транзакция откатывается, несмотря на блокtry-catch!
Программная диагностика в рантайме
Чтобы убедиться, открыта ли реальная транзакция Spring в текущей точке выполнения, используют статический класс TransactionSynchronizationManager:
import org.springframework.transaction.support.TransactionSynchronizationManager;
public void saveToDatabase(Long orderId) {
boolean isTxActive = TransactionSynchronizationManager.isActualTransactionActive();
String currentTxName = TransactionSynchronizationManager.getCurrentTransactionName();
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
System.out.println("Транзакция активна? " + isTxActive);
System.out.println("Имя транзакции: " + currentTxName);
}
Если метод вызван через self-invocation из не-транзакционного метода, isActualTransactionActive() вернет false.
🎯 Шпаргалка для интервью
30-секундный ответ
Self-invocation — это вызов метода того же самого класса через неявный или явный указатель
this. В Spring AOP сквозная функциональность (@Transactional,@Async,@Cacheable,@PreAuthorize) реализована через динамические прокси (CGLIB / JDK). Прокси перехватывает вызовы только из внешних объектов. При self-invocation инструкция байткодаinvokevirtualисполняется напрямую на объектеTarget, минуя цепочкуMethodInterceptor. В результате аннотация@Transactionalигнорируется, и код выполняется без транзакции (или параметрыREQUIRES_NEWне учитываются).
4 каверзных вопроса с ответами
-
Что произойдет, если метод без транзакции вызовет внутри своего класса метод с
@Transactional(propagation = Propagation.REQUIRES_NEW)? Ответ: Транзакция не откроется вообще. Вызов пойдет напрямую в оригинальный метод в обход прокси. Метод выполнится без транзакционного контекста, в режиме авто-коммита JDBC-соединения. -
Почему CGLIB-прокси не перехватывает вызовы внутри класса, если он наследует целевой класс? Ответ: CGLIB создает подкласс, переопределяющий методы с делегированием в интерцепторы. Однако бизнес-код вызывающего метода исполняется внутри целевого объекта. Указатель
thisссылается на экземпляр целевого класса, и вызов второго метода адресуется напрямую его таблице методов (vtable), не попадая в сгенерированные методы подкласса-прокси. -
Что произойдет с аннотацией
@Async, если вызвать помеченный ей метод через self-invocation? Ответ: Метод выполнится синхронно в том же самом потоке, из которого был вызван. Прокси-интерцепторAsyncExecutionInterceptor, отвечающий за отправку задачи в пулTaskExecutor, вызван не будет. -
Как в коде гарантированно проверить, работает ли метод внутри активной Spring-транзакции? Ответ: Вызвать
TransactionSynchronizationManager.isActualTransactionActive(). Если транзакция активна, метод вернетtrue.
Красные флаги (чего категорически нельзя говорить)
- ❌ «CGLIB решает проблему self-invocation, она есть только в JDK Dynamic Proxy» — грубое непонимание архитектуры:
thisуказывает на целевой объект в обеих реализациях. - ❌ «Если повесить
@Transactionalнад всем классом, self-invocation заработает» — аннотация над классом лишь помечает все public-методы для проксирования извне; внутренние вызовы черезthisвсе равно обходят прокси. - ❌ «При self-invocation транзакция откатится с ошибкой
TransactionRequiredException» — никакой ошибки не возникнет, код молча выполнится без транзакции (тихий дефект). - ❌ «Проблема касается исключительно аннотации
@Transactional» — ломаются абсолютно все AOP-механизмы: кэширование, асинхронность, безопасность, retry.