Питання 19 · Розділ 5

Як вирішити проблему з self-invocation?

4. Уникайте AopContext.currentProxy() 5. AspectJ — вимагає додаткового налаштування weaving (compile-time або load-time), ускладнює відладку. Використовуйте тільки якщо інші ріш...

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Рішення: Викличте метод через інший бін!

// ❌ Self-invocation
@Service
public class UserService {
    public void outer() {
        inner();  // Повз Proxy!
    }
    @Transactional public void inner() { }
}

// ✅ Розділення на два біни
@Service
public class UserService {
    @Autowired private InnerService innerService;

    public void outer() {
        innerService.inner();  // Через Proxy!
    }
}

@Service
public class InnerService {
    @Transactional public void inner() { }
}

🟡 Middle Level

Рішення 1: Розділення (найкраще)

// Винесіть логіку в окремий бін
@Service
public class OrderService {
    @Autowired private NotificationService notifier;

    public void createOrder() {
        repository.save(order);
        notifier.send(order);  // → Через Proxy іншого біна!
    }
}

Рішення 2: ObjectProvider

@Service
public class MyService {
    private final ObjectProvider<MyService> selfProvider;

    public void outer() {
        selfProvider.getObject().inner();  // → Через Proxy!
    }

    @Transactional
    public void inner() { }
}

Рішення 3: TransactionTemplate

@Service
public class MyService {
    private final TransactionTemplate transactionTemplate;

    public void outer() {
        transactionTemplate.execute(status -> {
            return inner();  // В транзакції без Proxy!
        });
    }
    // Якщо inner() повертає void, використовуйте:
    // transactionTemplate.executeWithoutResult(status -> { inner(); });

    public void inner() { }
}

Рішення 4: @Lazy self-injection

@Service
public class MyService {
    @Autowired @Lazy
    private MyService self;  // Проксі самого себе!

    public void outer() {
        self.inner();  // Через Proxy!
    }

    @Transactional
    public void inner() { }
}

🔴 Senior Level

Чому @Lazy працює

@Lazy → Spring створює Proxy-заглушку
→ При першому виклику Proxy ініціалізує реальний бін
→ self = Proxy → inner() йде через Proxy!

AopContext.currentProxy() (не рекомендується!)

// ❌ Крихкий код, прив'язаний до Spring
((MyService) AopContext.currentProxy()).inner();

// Потрібно: exposeProxy = true
// → Додатковий overhead
// → Не використовуйте!

// Проблеми AopContext.currentProxy():
// (1) вимагає exposeProxy = true в конфігурації,
// (2) додає ThreadLocal overhead на кожен виклик,
// (3) код жорстко прив'язаний до Spring AOP, складно тестувати.

AspectJ Weaving

LTW (Load-Time Weaving):
  → Java Agent впроваджує аспекти при завантаженні класу
  → Self-invocation працює!

CTW (Compile-Time Weaving):
  → Плагін Maven/Gradle впроваджує при компіляції

→ Складне налаштування
→ Ускладнює відладку

Production Experience

Реальний сценарій: ObjectProvider у легасі

Великий сервіс з self-invocation
→ Рефакторинг занадто дорогий
→ ObjectProvider вирішив проблему за 10 хвилин

Best Practices

  1. Розділення → найкраще рішення
  2. TransactionTemplate → точкове керування
  3. ObjectProvider + @Lazy → для легасі
  4. Уникайте AopContext.currentProxy()
  5. AspectJ — вимагає додаткового налаштування weaving (compile-time або load-time), ускладнює відладку. Використовуйте тільки якщо інші рішення не підходять.
  6. Code Review → перевіряйте self-invocation

Резюме для Senior

  • Розділення → чиста архітектура
  • ObjectProvider → сучасний спосіб
  • TransactionTemplate → без Proxy проблем
  • @Lazy self-injection → для легасі
  • AopContext → не використовуйте
  • AspectJ → складно, але вирішує

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • 4 основні рішення: (1) Розділення на різні біни, (2) ObjectProvider, (3) TransactionTemplate, (4) @Lazy self-injection
  • Розділення на біни — найкраще рішення з точки зору чистої архітектури
  • ObjectProvider.getObject() — отримує проксі самого себе, сучасний спосіб
  • TransactionTemplate — програмне керування транзакціями, без проксі-проблем
  • @Lazy self-injection — Spring створює Proxy-заглушку, при першому виклику ініціалізує реальний бін
  • AopContext.currentProxy() — НЕ РЕКОМЕНДУЄТЬСЯ: вимагає exposeProxy=true, ThreadLocal overhead, прив’язка до Spring
  • AspectJ Weaving (LTW/CTW) — вирішує проблему, але складне налаштування та відладка

Часті уточнюючі запитання:

  • Чому @Lazy працює? → Spring відкладає ініціалізацію, інжектить Proxy-заглушку, яка при першому виклику делегує реальному біну
  • Коли обрати TransactionTemplate? → Коли потрібна точкова транзакція всередині методу, без створення окремого біна
  • Чому AopContext.currentProxy() поганий? → (1) exposeProxy=true, (2) ThreadLocal overhead, (3) hardcoupling до Spring, (4) складно тестувати
  • Що краще: ObjectProvider чи @Lazy? → ObjectProvider — більш явний та тестований; @Lazy — коротший, але менш очевидний

Червоні прапорці (НЕ говорити):

  • «AopContext.currentProxy() — рекомендоване рішення» — не рекомендується
  • «AspectJ не потрібен, Spring AOP завжди вистачає» — для self-invocation без рефакторингу AspectJ — варіант
  • «TransactionTemplate = те саме, що @Transactional» — ні, це програмне керування, без проксі
  • «@Lazy створює новий бін» — створює проксі-заглушку до того ж біна

Пов’язані теми:

  • [[18. Чому @Transactional не працює при self-invocation]]
  • [[17. Що робить анотація @Transactional]]
  • [[13. Що таке proxy в Spring]]
  • [[12. В чому різниця між singleton та prototype scope]]