
Удивительна работа в Spring Boot аннотаций вроде @Transactional, @Cacheable, @Async. Но что происходит, когда они «отказываются» работать? Чтобы ответить на этот вопрос, изучим прокси-систему Spring изнутри.
А заодно узнаем нюансы прокси Spring Boot, разницу между динамическими прокси JDK и прокси CGLIB и сценарии их использования.
Тайна игнорируемых аннотаций
Сначала создадим службу уведомлений по электронной почте, для их обработки спроектируем класс NotificationService, а дорогостоящую логику генерирования email-шаблонов кэшируем. Вот код:
@Service
public class NotificationService {
@Cacheable("emailTemplates")
public String generateEmailTemplate(String type) {
// Моделируется дорогостоящая операция
return "Template for: " + type;
}
public void sendNotification(String email, String type) {
String template = generateEmailTemplate(type);
// Логика для отправки уведомления
}
}
Чтобы результат в Spring кэшировался, метод generateEmailTemplate аннотировали как @Cacheable. Но, когда запустили тесты, кэширование не выполнилось. При каждом вызове generateEmailTemplate метод выполнялся заново, а кэш игноририровался.
Открываем возможности прокси
Пытаясь понять, что не так с @Cacheable, обратились к документации Spring. И узнали, что функционал вроде @Cacheable, @Transactional и @Async поддерживается в Spring при помощи прокси. Прокси — это специальные объекты, в которые обертывается класс для перехвата вызовов методов и применения дополнительного поведения: кэширования или управления транзакциями.
Кроме того, выяснилась важная деталь: этими прокси внутренние вызовы методов не перехватываются. В примере выше generateEmailTemplate вызван методом sendNotification напрямую, в обход прокси. Поэтому кэширование @Cacheable и не выполнилось.
Решение: воспользоваться прокси
Проблема решается вынесением метода generateEmailTemplate в отдельную службу:
@Service
public class TemplateService {
@Cacheable("emailTemplates")
public String generateEmailTemplate(String type) {
// Моделируется дорогостоящая операция
return "Template for: " + type;
}
}
@Service
public class NotificationService {
private final TemplateService templateService;
public NotificationService(TemplateService templateService) {
this.templateService = templateService;
}
public void sendNotification(String email, String type) {
String template = templateService.generateEmailTemplate(type);
// Логика для отправки уведомления
}
}
Теперь generateEmailTemplate вызывается через прокси и кэширование выполняется, как ожидалось. Первый секрет этих прокси Spring раскрыт.
Прокси JDK и прокси CGLIB
Но это еще не все. Вскоре обнаружилось, что в Spring используются два вида прокси: динамические прокси JDK и прокси CGLIB. Тем, какой из них выбрать, определяется поведение кода.
Динамические прокси JDK
- Посредством прокси JDK реализуется тот же интерфейс, что и у целевого класса, и делегируются вызовы методов.
- Если классом реализуется интерфейс, в Spring по умолчанию используются прокси JDK.
Прокси CGLIB
- А этими прокси создается подкласс из целевого класса, методы которого переопределяются для применения дополнительного поведения.
- В Spring используются CGLIB, если классом не реализуется никаких интерфейсов или если он конфигурирован явно.
Корректный выбор прокси
Оказывается, выбором используемого в Spring прокси можно управлять. Например, так прокси CGLIB задается в Spring даже для интерфейсных классов:
@EnableTransactionManagement(proxyTargetClass = true)
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
// Детали конфигурации
}
Но какой прокси предпочтительнее? Сравним их:

В итоге решили использовать прокси JDK, а к CGLIB обращаться только для проксирования конкретных классов.
Другие аннотации
При изучении других аннотаций Spring обнаружились новые сценарии, в которых важны прокси.
Пример 1: @Async
Чтобы отправлять уведомления асинхронно, мы аннотировали метод при помощи @Async:
@Service
public class NotificationService {
@Async
public void sendNotification(String email, String type) {
// Логика для отправки уведомления
}
}
Однако обнаружилось, что метод не выполняется асинхронно при вызове напрямую из другого метода в том же классе. И снова внутренний вызов выполнен в обход прокси.
Решение прежнее: вынести асинхронный метод в отдельную службу.
Пример 2: @Retryable
Затем для автоматического повтора неудачных попыток доставки сообщений воспользовались аннотацией Spring @Retryable, благодаря механизму прокси обернули метод и четко обработали повторы:
@Service
public class EmailService {
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 2000))
public void sendEmail(String email, String content) {
// Логика для отправки сообщения
}
}
Здесь проблем не обнаружилось: пока метод вызывался через прокси, @Retryable обходилась без сбоев.
Золотые правила прокси Spring
Выводы мы оформили в виде этих золотых правил:
1. Выявляем вид прокси:
• Для интерфейсных классов в Spring по умолчанию используются прокси JDK.
• Для конкретных классов или при применении аннотаций к классу реализации используются прокси CGLIB.
2. Работаем с аннотациями через прокси:
• Функционал вроде @Transactional, @Cacheable, @Async и @Retryable поддерживается при помощи прокси.
3. Избегаем внутренних вызовов:
• Этими прокси внутренние вызовы не перехватываются. Для их перехвата такие методы перемещают в отдельные компоненты.
4. Только открытые методы:
• Этими прокси перехватываются только открытые методы. Закрытые или защищенные методы не проксируются.
5. Явное конфигурирование:
• При необходимости прокси CGLIB используются принудительно, для этого в конфигурации задается proxyTargetClass = true.
Заключение
Механизм прокси Spring Boot — мощный инструмент, но и он не лишен нюансов. Изучив работу прокси JDK и CGLIB, вы сэкономите время на отладке — кэшируете ли дорогостоящие операции, управляете транзакциями или отправляете асинхронные уведомления.
Читайте также:
- Механизм повторных попыток в Spring Boot: @Retryable и @Recover
- Как сделать интернет-магазин из Spring Boot, Angular, MySQL и Jasper Reports
- Упрощаем интеграцию Kafka со Spring Boot
Читайте нас в Telegram, VK и Дзен
Перевод статьи Ahmed Safwat: Spring Boot Proxies In Nutshell





