Вопрос 18 · Раздел 5

Почему @Transactional не работает при self-invocation?

4. AspectJ → байт-код weaving (сложно) 5. Избегайте self-invocation 6. Проверяйте через TransactionSynchronizationManager

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Self-invocation — когда метод вызывает другой метод ТОГО ЖЕ класса.

@Service
public class UserService {
    
    public void outer() {
        inner();  // ❌ @Transactional НЕ сработает!
    }
    
    @Transactional
    public void inner() { }
}

Почему: Spring создал Proxy вокруг объекта, но вызов inner() идёт напрямую через this, минуя Proxy!


🟡 Middle Level

Как работает Proxy

[ Клиент ] → [ Proxy ] → [ TransactionInterceptor ] → [ Target ]
                                                         ↓
                                                   [ outer() ]
                                                         ↓
                                                   [ inner() ] ← мимо Proxy!

Внешний вызов (работает):

Клиент → Proxy.inner() → TransactionInterceptor → Target.inner()

Self-invocation (НЕ работает):

Target.outer() → this.inner() → Target.inner() ← Proxy пропущен!

Почему CGLIB не помогает

CGLIB создаёт подкласс, но:
  → В CGLIB прокси — это подкласс. Когда внешний код вызывает метод, он идёт через MethodInterceptor. Но когда метод вызывает другой метод того же класса через `this`, вызов идёт напрямую к коду оригинального метода, минуя interceptor.

Какие ещё аннотации не работают

@Async        → не асинхронно
@Cacheable    → не кэширует
@Validated    → не валидирует
@Retryable    → не повторяет

→ Все на базе AOP Proxy!

🔴 Senior Level

Диагностика

// Проверка наличия транзакции:
TransactionSynchronizationManager.isActualTransactionActive();

// В дебаггере:
// Прокси: UserService$$EnhancerBySpringCGLIB$$abc123
// Target: UserService

Память: JDK vs CGLIB Proxy

JDK Proxy:
  → com.sun.proxy.$Proxy123
  → Реализует интерфейсы
  
CGLIB:
  → UserService$$EnhancerBySpringCGLIB$$abc123
  → Наследует класс

Production Experience

Реальный сценарий: частая причина багов

Разработчики пишут:
  @Transactional метод вызывает другой @Transactional метод

→ Внутренний @Transactional игнорируется
→ Данные сохраняются без транзакции
→ Баг обнаруживается только в production

Self-invocation — одна из самых частых причин, почему @Transactional «не работает» в реальных проектах.

Решение: Code Review на self-invocation

Best Practices

  1. Разделяйте на разные бины
  2. TransactionTemplate → программное управление
  3. Self-injection → @Lazy + ObjectProvider
  4. AspectJ → байт-код weaving (сложно)
  5. Избегайте self-invocation
  6. Проверяйте через TransactionSynchronizationManager

Резюме для Senior

  • Proxy перехватывает только ВНЕШНИЕ вызовы
  • this.method() → напрямую, мимо Proxy
  • CGLIB не помогает (this = Target)
  • Self-invocation — частая причина, почему @Transactional «не работает»
  • TransactionSynchronizationManager → для проверки
  • Решение: разные бины или TransactionTemplate

🎯 Шпаргалка для интервью

Обязательно знать:

  • Self-invocation = метод вызывает другой метод ТОГО ЖЕ класса
  • Spring AOP работает через прокси, но this.method() идёт напрямую, минуя прокси
  • Это касается ВСЕХ AOP-аннотаций: @Transactional, @Async, @Cacheable, @Validated, @Retryable
  • CGLIB тоже не помогает — внутри Target-объекта this ссылается на Target, а не на подкласс-прокси
  • Проверить наличие транзакции: TransactionSynchronizationManager.isActualTransactionActive()
  • Одна из самых частых причин «не работает @Transactional» в реальных проектах
  • Диагностика: в дебаггере прокси = $$EnhancerBySpringCGLIB$$, target = обычный класс

Частые уточняющие вопросы:

  • Почему CGLIB не решает проблему? → CGLIB перехватывает вызовы извне, но this.method() внутри Target идёт напрямую
  • Какие ещё аннотации не работают при self-invocation? → Все AOP-based: @Async, @Cacheable, @Validated, @Retryable
  • Как обнаружить self-invocation в коде? → Code Review, вызов метода того же класса внутри @Transactional-метода
  • Когда баг проявляется? → В production, когда данные сохраняются без ожидаемой транзакции

Красные флаги (НЕ говорить):

  • «CGLIB решает self-invocation» — не решает, this = Target
  • «@Transactional на private-методах работает» — Spring AOP только для public
  • «Это редкая проблема» — одна из самых частых причин багов
  • «Достаточно поставить @Transactional на класс» — self-invocation всё равно не работает

Связанные темы:

  • [[13. Что такое прокси в Spring]]
  • [[17. Что делает аннотация @Transactional]]
  • [[19. Как решить проблему с self-invocation]]