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