Pointcut — это выражение, которое отвечает на вопрос "где именно в коде должен сработать аспект?". По сути, это фильтр, который выбирает нужные методы из всего приложения. Например, "все методы в сервисном слое" или "методы с определённой аннотацией".
В Spring AOP для описания поинткатов используется синтаксис AspectJ. Да, поначалу он может выглядеть как код с RSA-шифрованием, но на самом деле всё довольно логично. Давайте разберём.
Встроенные выражения Pointcut
Вот самые базовые выражения, которые вам понадобятся:
| Выражение | Описание |
|---|---|
execution |
Подходит для методов. Используется в 99% случаев |
within |
Указывает область (например, конкретный класс или пакет) |
args |
Подходит для методов с определёнными аргументами |
this и target |
Указывает на объект прокси (this) или реальный целевой объект (target) |
bean |
Подходит для бинов с определённым именем |
Пример простого Pointcut-выражения:
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayerMethods() {
// Этот метод просто маркер для Pointcut
}
Давайте разберём это "заклинание":
execution— мы нацеливаемся на методы.*— любой возвращаемый тип (void,String,int, etc).com.example.service.*.*— любой класс в пакетеcom.example.serviceи любой метод.(..)— любой набор параметров.
Немного о Wildcards
Wildcards (подстановочные символы) в выражениях Pointcut позволяют нам быть гибкими. Вот короткое руководство:
*— означает "любой". Пример:execution(* com.example..*Controller.*(..))— любой метод в любом классе, содержащемControllerв имени...— означает "любой уровень вложенности". Например,com.example..serviceвключает все подпакеты отcom.example.
Пример: Углубляемся в Pointcut
Давайте попробуем что-нибудь посложнее. Представим, что мы хотим добавить логирование для всех методов класса, которые начинаются с save.
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(* com.example..*Service.save*(..))")
public void saveMethodsPointcut() {
// Pointcut для методов, начинающихся с save
}
@Before("saveMethodsPointcut()")
public void beforeSaveAdvice(JoinPoint joinPoint) {
System.out.println("Вызов метода: " + joinPoint.getSignature().getName());
}
}
Здесь мы:
- определяем точку соединения для всех методов, начинающихся с
saveв любом классе из подпакетовcom.example, название которых заканчивается наService. - Перед вызовом таких методов мы логируем имя метода.
Композиция Pointcut-выражений
Мы уже упоминали, что иногда нужно объединить несколько Pointcut’ов. Например, мы хотим воздействовать только на методы из определённого пакета, и чтобы они ещё начинались с определённого слова.
В AspectJ есть логические операторы для композиций:
&&— логическое "И".||— логическое "ИЛИ".!— логическое "НЕ".
Вот пример:
@Pointcut("within(com.example..*)")
public void withinExamplePackage() {}
@Pointcut("execution(* save*(..))")
public void saveMethods() {}
@Pointcut("withinExamplePackage() && saveMethods()")
public void saveMethodsInExamplePackage() {}
Таким образом, saveMethodsInExamplePackage() сработает только на методах, которые начинаются с save, и лежат в пакетах com.example.
Создание кастомного аспекта
Теперь применим полученные знания на практике. Допустим, в нашем приложении есть сервис, который отвечает за управление пользователями (UserService). Мы хотим:
- Логировать вызовы методов, начинающихся с
find. - Вызывать дополнительную логику только тогда, когда метод возвращает
String.
Реализация
@Aspect
@Component
public class CustomAspect {
// Логирование всех методов, начинающихся с find
@Pointcut("execution(* com.example.service.UserService.find*(..))")
public void findMethods() {}
// Выполнение действия перед вызовом метода
@Before("findMethods()")
public void logBeforeFind(JoinPoint joinPoint) {
System.out.println("Вызов метода: " + joinPoint.getSignature().getName());
}
// Реакция на успешное выполнение метода, возвращающего String
@AfterReturning(
pointcut = "findMethods()",
returning = "result"
)
public void afterReturningFind(JoinPoint joinPoint, Object result) {
if (result instanceof String) {
System.out.println("Метод " + joinPoint.getSignature().getName() +
" успешно вернул значение: " + result);
}
}
}
Что происходит:
- С помощью
@Pointcutмы выбираем только методы в классеUserService, чьи названия начинаются сfind. - Используем
@Beforeдля логирования вызова. - Используем
@AfterReturning, чтобы дополнительно обработать возвращаемое значение, если оно является строкой.
Практика: создание сложного кастомного аспекта
Попробуем усложнить задачу. Допустим, нам нужно логировать все методы контроллеров, которые аннотированы @GetMapping.
@Aspect
@Component
public class ControllerLoggingAspect {
// Pointcut для методов с аннотацией @GetMapping
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void getMappingMethods() {}
// Логирование до выполнения метода
@Before("getMappingMethods()")
public void logBeforeGetMapping(JoinPoint joinPoint) {
System.out.println("Вызов метода контроллера: " + joinPoint.getSignature().getName());
}
}
Мы используем аннотацию @annotation, чтобы чётко определить методы, на которые нацеливаемся (в данном случае методы с @GetMapping). Вы видите, насколько мощным становится ваш инструмент?
Отладка и типичные ошибки
AOP — не без своего набора головоломок. Вот несколько вещей, о которых стоит помнить:
- Если ваш аспект не работает, убедитесь, что класс аспекта помечен аннотацией
@Componentи зарегистрирован в контексте Spring. - Проверьте, что вы правильно указываете путь пакетов в
execution. Пропущенный уровень вложенности или опечатка могут свести все усилия на нет. - Обратите внимание на прокси: если вы вызываете метод класса внутри самого же класса, аспект может не сработать.
Ручная проверка Pointcut-выражений
Если вам интересно, какие именно методы попадают под ваше Pointcut-выражение, вы всегда можете это проверить вручную:
@Before("execution(* com.example.service.*.*(..))")
public void debugPointcut(JoinPoint joinPoint) {
System.out.println("Попал в Pointcut: " + joinPoint.getSignature().getName());
}
Вы сможете увидеть в логах список всех методов, которые соответствуют выражению.
Вы готовы создавать аспекты, которые смогут управлять бизнес-логикой вашего приложения! На следующем этапе мы углубимся ещё больше в прокси-объекты и посмотрим на их скрытые секреты.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ