Как решить проблему с 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. Что такое прокси в Spring]]
- [[12. В чём разница между singleton и prototype scope]]