Половина «магии» 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не работает при вызове из того же класса? Как исправить? - Что не перехватывается прокси?
- Как задать порядок нескольких аспектов?