Spring AOP

Аспектно-ориентированное программирование выносит сквозную техническую логику из бизнес-методов. Типичные применения: транзакции, метрики, аудит, проверка доступа и повторяемое логирование.

Термины

  • join point — точка исполнения, к которой можно присоединить поведение. В Spring AOP это выполнение метода Spring-бина;
  • pointcut — правило выбора методов;
  • advice — код, выполняемый до, после или вокруг выбранного вызова;
  • aspect — pointcut и связанные advice;
  • target — исходный объект;
  • proxy — объект-посредник, через который идёт перехваченный вызов.

Spring AOP не переписывает произвольные строки кода. Он ставит прокси перед Spring-бином:

sequenceDiagram
    participant C as Caller
    participant P as Proxy
    participant A as Advice
    participant T as Target
    C->>P: method()
    P->>A: before / around
    A->>T: proceed()
    T-->>A: result or exception
    A-->>P: result
    P-->>C: result

Пример аспекта

@Aspect
@Component
class TimingAspect {
    @Around("@annotation(Measured)")
    Object measure(ProceedingJoinPoint point) throws Throwable {
        long started = System.nanoTime();
        try {
            return point.proceed();
        } finally {
            long elapsed = System.nanoTime() - started;
            // передать имя операции и elapsed в систему метрик
        }
    }
}

В @Around обязательно решить, будет ли вызван proceed(). Advice может изменить аргументы, результат и исключение, поэтому такой код должен оставаться коротким и предсказуемым.

Основные типы advice:

  • @Before;
  • @AfterReturning;
  • @AfterThrowing;
  • @After — аналог finally;
  • @Around.

JDK proxy и class-based proxy

JDK dynamic proxy реализует интерфейсы target-объекта. Class-based proxy создаёт подкласс. Практические последствия:

  • вызов должен пройти через прокси;
  • final-класс и final-метод нельзя переопределить class-based proxy;
  • private-метод не является подходящей границей для proxy advice;
  • тип прокси важен при получении бина по конкретному классу.

Прокси — ещё один аргумент внедрять зависимости по интерфейсу, когда интерфейс действительно выражает контракт.

Self-invocation

@Service
class ReportService {
    public void rebuild() {
        savePart(); // обычный вызов this.savePart(), прокси не участвует
    }
 
    @Transactional
    public void savePart() {
        // ...
    }
}

Если rebuild() вызван через proxy, перехватится только внешний вызов. Внутренний вызов savePart() идёт напрямую к this, поэтому новая транзакционная граница не создаётся.

Рабочие решения:

  • вынести транзакционную операцию в отдельный бин;
  • перенести аннотацию на внешний метод;
  • использовать TransactionTemplate, когда граница должна быть внутри алгоритма;
  • AspectJ weaving применять только при осознанной необходимости.

Self-injection и получение собственного бина из контекста скрывают дизайн и обычно хуже явного разделения ответственности.

Pointcut без сюрпризов

Pointcut по своей аннотации обычно устойчивее выражения, зависящего от структуры пакетов:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Measured {}

Нельзя логировать все аргументы автоматически: среди них могут быть пароли, токены и персональные данные. Для метрик также важно ограничить cardinality тегов.

На собеседовании

Почему AOP-аннотация не сработала? Частые причины: объект создан не Spring, self-invocation, неподходящая видимость или final, pointcut не совпал, вызов произошёл слишком рано в жизненном цикле.

Чем Spring AOP отличается от AspectJ? Spring AOP перехватывает вызовы методов бинов через proxy; AspectJ может встраивать поведение в байткод и поддерживает больше join points.

Можно ли использовать AOP для бизнес-логики? Технически да, но скрытый порядок усложняет понимание. Аспекты лучше оставлять для действительно сквозного поведения.

Связанные заметки: Spring Proxy, Жизненный цикл бина, Transactional.