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.