Удивительна работа в 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, вы сэкономите время на отладке  —  кэшируете ли дорогостоящие операции, управляете транзакциями или отправляете асинхронные уведомления.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Ahmed Safwat: Spring Boot Proxies In Nutshell

Предыдущая статьяОт кода до APK: полный разбор задач Android-сборки
Следующая статьяПереход с VS Code на Neovim: повысьте свою продуктивность