Як вирішити проблему з self-invocation?
4. Уникайте AopContext.currentProxy() 5. AspectJ — вимагає додаткового налаштування weaving (compile-time або load-time), ускладнює відладку. Використовуйте тільки якщо інші ріш...
🟢 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
- Розділення → найкраще рішення
- TransactionTemplate → точкове керування
- ObjectProvider + @Lazy → для легасі
- Уникайте AopContext.currentProxy()
- AspectJ — вимагає додаткового налаштування weaving (compile-time або load-time), ускладнює відладку. Використовуйте тільки якщо інші рішення не підходять.
- 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]]