Урок по треку Spring и Spring Boot 8 мин

AOP и прокси в Spring

Аспекты, advice и pointcut, JDK Proxy и CGLIB, self-invocation и что не перехватывается.

Половина «магии» Spring (@Transactional, @Async, @Cacheable, @PreAuthorize, @Retryable) работает через прокси. Кто понимает прокси, тот сразу видит, почему аннотация «не сработала». Это любимая тема middle и senior собесов.

AOP за минуту

  • Aspect: класс со сквозной логикой (логирование, метрики, транзакции).
  • Advice: что делать и когда: @Before, @AfterReturning, @AfterThrowing, @After, @Around.
  • Pointcut: где применять, выражение вроде execution(* com.shop.service.*.*(..)) или @annotation(com.shop.Audited).
  • Join point: точка выполнения, в Spring AOP это всегда вызов метода.
@Aspect
@Component
public class TimingAspect {
    @Around("@annotation(com.shop.Timed)")
    public Object time(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        try {
            return pjp.proceed();
        } finally {
            log.info("{} took {} µs", pjp.getSignature(), (System.nanoTime() - start) / 1000);
        }
    }
}

В @Around обязательно вызвать proceed() и вернуть результат, иначе метод не выполнится или вернёт null.

Как устроен прокси

Spring AOP работает в рантайме: вместо бина в контекст кладётся прокси-объект, который перехватывает вызовы и вызывает настоящий бин (target).

  • JDK dynamic proxy: реализует интерфейсы бина. Внедрить можно только по интерфейсу; приведение к классу упадёт.
  • CGLIB: генерирует подкласс бина и переопределяет методы. Spring Boot по умолчанию использует CGLIB (spring.aop.proxy-target-class=true), даже если интерфейсы есть.

Из механики подкласса следуют ограничения CGLIB: final-методы не переопределить, значит не перехватить; final-класс вообще не проксируется (ошибка при старте); private-методы не видны подклассу. static-методы не перехватывают ни JDK proxy, ни CGLIB.

Self-invocation: главная ловушка

@Service
public class OrderService {
    public void placeAll(List<Order> orders) {
        orders.forEach(this::place);     // вызов через this, мимо прокси
    }

    @Transactional
    public void place(Order o) { ... }
}

Внешний код вызывает прокси, прокси вызывает placeAll у настоящего объекта, а внутри него this.place() это прямой вызов. Прокси его не видит, транзакции нет. То же самое с @Async, @Cacheable, @Retryable, @PreAuthorize.

Что делать:

  • вынести метод в другой бин (лучший вариант);
  • внедрить себя через @Lazy или ObjectProvider и вызывать через прокси;
  • TransactionTemplate для программного управления транзакцией;
  • AspectJ с compile-time или load-time weaving, который вплетает код прямо в класс (редко нужно).

Что перехватывается, а что нет

СитуацияПерехватится?
вызов public-метода снаружи через внедрённый бинда
вызов из того же класса через thisнет
private-методнет
final-метод при CGLIBнет
protected или package-private при CGLIBс Spring 6 да для @Transactional (если вызов через прокси), но лучше не полагаться
объект создан через newнет: это не бин
вызов в конструкторе или @PostConstructнет: прокси ещё не создан

Порядок аспектов

Если на методе несколько аспектов (транзакция, кеш, ретраи), порядок задаётся @Order или Ordered. Это важно: ретрай снаружи транзакции повторит всю транзакцию, ретрай внутри будет пытаться внутри уже помеченной на откат транзакции.

Частые ошибки

  • @Transactional на private-методе или вызов через this.
  • Класс сервиса final (в Kotlin классы final по умолчанию, поэтому там нужен плагин kotlin-spring).
  • @Around без return pjp.proceed().
  • Приведение бина к конкретному классу при JDK-прокси.

Что спросят на собесе

  • Что такое AOP, advice, pointcut, join point?
  • Чем JDK dynamic proxy отличается от CGLIB, что выбирает Spring Boot?
  • Почему @Transactional не работает при вызове из того же класса? Как исправить?
  • Что не перехватывается прокси?
  • Как задать порядок нескольких аспектов?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.