Питання 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. Що таке proxy в Spring]]
  • [[17. Що робить анотація @Transactional]]
  • [[19. Як вирішити проблему з self-invocation]]