🍃 Раздел 5 · Вопрос #19

Как решить проблему с self-invocation

Проблема self-invocation (само-вызова) заключается в том, что при вызове метода того же класса через this вызов идет в обход AOP-прокси, поэтому аннотации @Transactional, @Async...


🟢 Junior Level

Проблема self-invocation (само-вызова) заключается в том, что при вызове метода того же класса через this вызов идет в обход AOP-прокси, поэтому аннотации @Transactional, @Async, @Cacheable игнорируются.

Самое простое и рекомендуемое решение: Вынос метода в отдельный сервис

Разделите ответственность классов: перенесите транзакционный метод во вспомогательный сервис.

// ❌ БЫЛО: self-invocation внутри одного сервиса (транзакция не работает)
@Service
public class OrderService {
    public void processOrder(Long orderId) {
        saveOrder(orderId); // Вызов через this минует прокси!
    }

    @Transactional
    public void saveOrder(Long orderId) { ... }
}

// ✅ СТАЛО: два независимых бина (транзакция работает через прокси)
@Service
public class OrderService {

    private final OrderDatabaseService orderDatabaseService;

    public OrderService(OrderDatabaseService orderDatabaseService) {
        this.orderDatabaseService = orderDatabaseService;
    }

    public void processOrder(Long orderId) {
        // Внешний вызов через прокси внедренного бина!
        orderDatabaseService.saveOrder(orderId); 
    }
}

@Service
public class OrderDatabaseService {

    @Transactional
    public void saveOrder(Long orderId) {
        // Выполняется в рамках открытой транзакции
    }
}

🟡 Middle Level

5 способов решения проблемы: сравнение

Способ решения Принцип работы Плюсы Минусы Рекомендация
1. Вынос в отдельный бин Разделение классов (SRP) Чистая архитектура, легкое модульное тестирование Увеличивает количество классов Рекомендуется (Best Practice)
2. TransactionTemplate Программные транзакции Не зависит от прокси, точечный контроль Шаблонный код Рекомендуется для коротких транзакций
3. Self-injection (@Lazy) Внедрение прокси самого себя Не требует создания новых классов Циклическая зависимость (хоть и ленивая) Допустимо при рефакторинге легаси
4. ObjectProvider<Self> Извлечение прокси из контекста Типобезопасно, явный вызов Зависимость от Spring API Альтернатива @Lazy
5. AopContext.currentProxy() Извлечение прокси из ThreadLocal Быстрый костыль без инъекций Жесткая связность, оверхед, легко сломать ❌ Не рекомендуется (Anti-pattern)

🔴 Senior Level

Способ 2: Программное управление через TransactionTemplate

Использование TransactionTemplate избавляет от необходимости генерировать AOP-прокси для вспомогательных методов и предотвращает удержание соединения с БД во время медленных операций (I/O, вызовы внешних REST API):

package com.example.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;

@Service
public class InvoiceService {

    private final TransactionTemplate transactionTemplate;
    private final ExternalPaymentGatewayClient paymentClient;
    private final InvoiceRepository invoiceRepository;

    public InvoiceService(TransactionTemplate transactionTemplate,
                          ExternalPaymentGatewayClient paymentClient,
                          InvoiceRepository invoiceRepository) {
        this.transactionTemplate = transactionTemplate;
        this.paymentClient = paymentClient;
        this.invoiceRepository = invoiceRepository;
    }

    public void processInvoice(Long invoiceId) {
        // 1. Медленный вызов к внешнему шлюзу БЕЗ удержания соединения с БД
        PaymentResult result = paymentClient.charge(invoiceId);

        // 2. Короткая изолированная транзакция строго для записи в БД
        transactionTemplate.executeWithoutResult(status -> {
            Invoice invoice = invoiceRepository.findById(invoiceId).orElseThrow();
            invoice.markAsPaid(result.transactionId());
            invoiceRepository.save(invoice);
        });
    }
}

Способ 3: Ленивое внедрение самого себя (Self-Injection через @Lazy)

Если рефакторинг с разделением класса невозможен из-за сжатых сроков или объема легаси-кода, можно внедрить прокси самого себя через @Lazy:

package com.example.service;

import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

    private final UserService self; // Ссылка на AOP-прокси этого же бина!

    public UserService(@Lazy UserService self) {
        this.self = self;
    }

    public void registerUser(UserDto dto) {
        // Вызов метода через прокси-ссылку self активирует TransactionInterceptor!
        self.saveUserInTransaction(dto);
    }

    @Transactional
    public void saveUserInTransaction(UserDto dto) {
        // Успешно выполняется под транзакцией
    }
}

Почему это работает под капотом:

  1. Без аннотации @Lazy инъекция самого себя привела бы к ошибке BeanCurrentlyInCreationException (циклическая зависимость в конструкторе).
  2. С аннотацией @Lazy Spring внедряет в конструктор синтетическую CGLIB-заглушку.
  3. При первом реальном вызове self.saveUserInTransaction() заглушка запрашивает у BeanFactory полностью инициализированный синглтон-прокси бина UserService.

Способ 4: ObjectProvider<Self>

Современная альтернатива ленивому полю:

@Service
public class NotificationService {

    private final ObjectProvider<NotificationService> selfProvider;

    public NotificationService(ObjectProvider<NotificationService> selfProvider) {
        this.selfProvider = selfProvider;
    }

    public void sendBulkNotifications() {
        // Запрос актуального прокси из контекста в момент вызова
        selfProvider.getObject().sendSingleInTransaction();
    }

    @Transactional
    public void sendSingleInTransaction() { ... }
}

Антипаттерн: AopContext.currentProxy()

Старый механизм, требующий включения свойства @EnableAspectJAutoProxy(exposeProxy = true):

// ❌ АНТИПАТТЕРН: жесткая привязка к Spring AOP API
public void outerMethod() {
    ((MyService) AopContext.currentProxy()).innerTransactionalMethod();
}

Почему это признано антипаттерном:

  1. Скрытая хрупкость: Если кто-то забудет выставить флаг exposeProxy = true в конфигурации, код выбросит фатальный IllegalStateException: Cannot find current proxy только в рантайме.
  2. Оверхед по памяти и CPU: Флаг exposeProxy = true заставляет прокси сохранять текущий объект в ThreadLocal при каждом вызове любого метода каждого бина в системе.
  3. Невозможность юнит-тестирования: Обычный new MyService() в JUnit-тестах упадет с ошибкой, так как AopContext не инициализирован.

🎯 Шпаргалка для интервью

30-секундный ответ

Проблема self-invocation решается 4 способами:

  1. Вынос метода в отдельный Spring-бин (Best Practice): соблюдает принцип единой ответственности (SRP) и обеспечивает чистый вызов через прокси внедренной зависимости.
  2. Использование TransactionTemplate: программное открытие транзакций, не зависящее от AOP-прокси (идеально для изоляции от долгих HTTP-запросов).
  3. Self-Injection через @Lazy конструктор или ObjectProvider: позволяет вызывать метод на прокси самого себя (self.inner()).
  4. AspectJ Weaving: замена прокси на модификацию байткода, где само-вызов работает из коробки. Использование AopContext.currentProxy() считается устаревшим антипаттерном.

4 каверзных вопроса с ответами

  1. Какое решение проблемы self-invocation считается архитектурно эталонным? Ответ: Вынесение транзакционного метода в отдельный сервис (класс). Это соответствует принципу Single Responsibility Principle (SRP): класс бизнес-оркестрации отделяется от класса, непосредственно работающего с персистентным хранилищем. Такой код легко покрывать юнит-тестами с обычными моками (Mockito).

  2. Чем TransactionTemplate лучше @Transactional при работе с внешними REST API? Ответ: @Transactional держит физическое соединение с базой данных на протяжении выполнения всего метода (включая ожидание сетевого ответа от REST API). TransactionTemplate позволяет выполнить медленный сетевой запрос до транзакции, затем точечно открыть транзакцию ровно на время сохранения данных и мгновенно вернуть соединение в пул HikariCP.

  3. Как @Lazy решает проблему циклической зависимости при self-injection в конструктор? Ответ: Аннотация @Lazy указывает Spring внедрить динамический прокси-заместитель вместо реального бина. Конструктор бина успешно завершается без блокировок. При первом вызове метода через этот прокси происходит реальное разрешение бина из контекста.

  4. Почему AopContext.currentProxy() является антипаттерном? Ответ: Он требует явного включения настройки exposeProxy = true, что создает постоянный оверхед на запись в ThreadLocal для всех вызовов в приложении. Кроме того, он намертво связывает бизнес-код со статическим API Spring AOP, делая невозможным выполнение простых модульных тестов без подъема контекста.

Красные флаги (чего категорически нельзя говорить)

  • ❌ «Лучший способ решить self-invocation — вызвать AopContext.currentProxy()» — грубый антипаттерн, приводящий к падению в модульных тестах и рантайм-ошибкам.
  • ❌ «Self-injection через @Autowired private MyService self без @Lazy работает отлично» — в конструкторе это приведет к ошибке циклической зависимости BeanCurrentlyInCreationException.
  • ❌ «TransactionTemplate работает через AOP-прокси» — нет, TransactionTemplate работает программно через прямой вызов PlatformTransactionManager, без генерации прокси.
  • ❌ «Для решения проблемы нужно переключить проект на JDK Dynamic Proxy» — JDK Dynamic Proxy подвержен проблеме self-invocation точно так же, как и CGLIB.

Связанные темы