Как создать свою аннотацию в Spring для выполнения кода при вызове метода
Что проверяют
Понимаете ли вы, что Spring выполняет код вокруг метода через прокси, и знаете ли ограничения этого подхода. Почти всегда следом спрашивают, почему аннотация не срабатывает при вызове метода из того же класса.
Как отвечать
Три шага:
- аннотация:
@Target(ElementType.METHOD)и обязательно@Retention(RetentionPolicy.RUNTIME), иначе в рантайме её не будет видно; - аспект: класс с
@Aspectи@Component, внутри совет@Around("@annotation(timed)")с параметромTimed timed. Привязка через параметр даёт доступ к атрибутам аннотации и избавляет от полного имени класса в выражении; - зависимость: Spring AOP и AspectJ weaver, в Spring Boot их приносит AOP-стартер, и прокси включаются автоматически.
Внутри @Around вызываете joinPoint.proceed(), вокруг него замеряете время или пишете лог и возвращаете результат дальше. Для простых случаев хватит @Before и @AfterReturning.
Как это работает: Spring оборачивает бин прокси, в Spring Boot по умолчанию через CGLIB. Отсюда ограничения:
- self-invocation: вызов
this.method()идёт мимо прокси, совет не срабатывает; - private и final методы: CGLIB не может их переопределить, перехвата нет;
- объект через
new: это не бин, прокси у него нет.
Если аспектов несколько, порядок задаёт @Order, и рядом с @Transactional это важно. Когда нужен перехват вызовов внутри класса, метод выносят в другой бин или берут AspectJ со вплетением на этапе компиляции.
Где спотыкаются
Пишут @Around и забывают вернуть результат proceed() или вовсе его не вызывают. Метод либо не выполняется, либо возвращает null, и ошибка всплывает далеко от аспекта.
Проверяют аннотацию в main через new MyService() и удивляются, что ничего не происходит. Аспекты работают только на бинах из контекста.
Глотают исключение в совете: ловят Throwable вокруг proceed(), пишут в лог и не пробрасывают. Вызывающий код считает, что всё прошло успешно, а если аспект выполняется внутри транзакции, @Transactional не увидит исключения и закоммитит изменения.