Spring AOP использует прокси. То есть вместо настоящего бина в контейнер помещается его обёртка. Эта обёртка перехватывает вызовы и применяет к ним различные действия, такие как логирование, управление транзакциями, проверка безопасности или другие, заданные пользователем.
Способ создания такой обёртки выбирается автоматически, но у разных способов есть свои особенности и ограничения.
🔵 JDK Dynamic Proxy vs CGLIB
JDK proxy работает только с интерфейсами. Он создаёт объект, который реализует тот же интерфейс, и все вызовы идут через специальный обработчик (InvocationHandler). Внутри есть оригинальный объект, но снаружи виден только прокси.
CGLIB работает по-другому: он создаёт подкласс вашего класса прямо во время работы программы и переопределяет методы, вставляя в них перехват. Поэтому CGLIB не может работать с final-методами и классами, т.к. подкласс не может переопределить final-метод.
Начиная с Spring Boot 2.0, CGLIB используется по умолчанию даже для классов с интерфейсами (
spring.aop.proxy-target-class=true). Раньше, если был интерфейс, использовался JDK proxy.🔵 Self-invocation
Тут всё становится не так очевидно. Разберём такой код:
@Service
public class OrderService {
public void placeOrder() {
// какая-то логика
sendNotification();
}
@Async
public void sendNotification() {
// должно выполняться асинхронно
}
}
sendNotification() помечен аннотацией @Async, но при вызове из placeOrder() он будет выполняться синхронно. Потому что placeOrder() вызывает this.sendNotification(), то есть напрямую метод оригинального объекта, минуя прокси. Перехватывать нечего, аспект не срабатывает.
То же самое с @Transactional, @Cacheable, любым AOP-аспектом.
🔵 Как это исправить
1. Самое простое и правильное решение — вынести метод в отдельный бин. Тогда вызов пойдёт через прокси другого бина, и перехват сработает.
2. Можно получить прокси самого себя через контекст:
@Service
public class OrderService {
@Autowired
private ApplicationContext context;
public void placeOrder() {
context.getBean(OrderService.class).sendNotification();
}
@Async
public void sendNotification() { ... }
}
3. Использовать AopContext.currentProxy():
((OrderService) AopContext.currentProxy()).sendNotification();
Для этого потребуется @EnableAspectJAutoProxy(exposeProxy = true). Выглядит не очень красиво, но иногда оправдано, если рефакторинг слишком сложен.
4. Перейти от Spring AOP к AspectJ с compile-time или load-time weaving. Тогда аспекты вшиваются прямо в байткод, прокси не нужен, и self-invocation работает как надо. Но это уже совсем другая история, требующая более сложной настройки.
🔵 Почему это важно
Ошибки, связанные с self-invocation, не всегда легко воспроизвести. В тестах, где бин заменяется моком, поведение может быть другим. А в интеграционных тестах с поднятым контекстом – проявляются.
Без понимания, как работает проксирование, сложно понять причину проблемы, потому что код выглядит правильно, аннотации на месте, и ошибок компиляции нет.
══════ Навигация ══════
Вакансии • Задачи • Собесы
🐸 Библиотека джависта
#CoreJava