Перейти к основному содержимому

Реализация пользовательской аннотации Spring AOP

· 5 мин. чтения

1. Введение

В этой статье мы реализуем пользовательскую аннотацию AOP, используя поддержку AOP в Spring.

Во-первых, мы дадим общий обзор АОП, объяснив, что это такое и его преимущества. После этого мы шаг за шагом будем реализовывать нашу аннотацию, постепенно создавая более глубокое понимание концепций АОП по мере продвижения.

Результатом станет лучшее понимание АОП и возможность создавать собственные аннотации Spring в будущем.

2. Что такое аннотация АОП?

Подводя итог, АОП означает аспектно-ориентированное программирование. По сути, это способ добавления поведения в существующий код без изменения этого кода .

Для подробного ознакомления с АОП есть статьи о точках и советах по АОП . В этой статье предполагается, что у нас уже есть базовые знания.

Тип АОП, который мы будем реализовывать в этой статье, основан на аннотациях. Возможно, мы уже знакомы с этим, если использовали аннотацию Spring @Transactional :

@Transactional
public void orderGoods(Order order) {
// A series of database calls to be performed in a transaction
}

Здесь главное неинвазивность. Используя метаданные аннотации, наша основная бизнес-логика не загрязняется кодом транзакции. Это упрощает анализ, рефакторинг и изолированное тестирование.

Иногда люди, разрабатывающие приложения Spring, могут рассматривать это как « Магию Spring», не задумываясь в деталях о том, как это работает. На самом деле в происходящем нет ничего особенно сложного. Однако, выполнив шаги, описанные в этой статье, мы сможем создать собственную пользовательскую аннотацию, чтобы понять и использовать АОП.

3. Зависимость от Maven

Во-первых, давайте добавим наши зависимости Maven .

В этом примере мы будем использовать Spring Boot, так как его подход к настройке позволяет нам как можно быстрее приступить к работе:

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>

Обратите внимание, что мы включили стартер АОП, который извлекает библиотеки, необходимые для начала реализации аспектов.

4. Создание нашей пользовательской аннотации

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

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

}

Несмотря на относительно простую реализацию, стоит отметить, для чего используются две метааннотации.

Аннотация @Target сообщает нам, где наша аннотация будет применима. Здесь мы используем ElementType.Method, что означает, что он будет работать только с методами. Если бы мы попытались использовать аннотацию в другом месте, наш код не смог бы скомпилироваться. Такое поведение имеет смысл, так как наша аннотация будет использоваться для регистрации времени выполнения метода.

И @Retention просто указывает, будет ли аннотация доступна для JVM во время выполнения или нет. По умолчанию это не так, поэтому Spring AOP не сможет увидеть аннотацию. Вот почему он был переконфигурирован.

5. Создание нашего аспекта

Теперь у нас есть аннотация, давайте создадим наш аспект. Это всего лишь модуль, который будет инкапсулировать нашу сквозную задачу, в нашем случае это регистрация времени выполнения метода. Все это класс, аннотированный @Aspect :

@Aspect
@Component
public class ExampleAspect {

}

Мы также включили аннотацию @Component , так как наш класс также должен быть bean-компонентом Spring, чтобы его можно было обнаружить. По сути, это класс, в котором мы будем реализовывать логику, которую мы хотим внедрить в нашу пользовательскую аннотацию.

6. Создание нашего Pointcut и совета

Теперь давайте создадим наш pointcut и совет. Это будет аннотированный метод, живущий в нашем аспекте:

@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}

Технически это пока ничего не меняет в поведении, но еще многое предстоит сделать, что требует анализа.

Во- первых, мы аннотировали наш метод с помощью @Around . Это наш совет, а совет означает, что мы добавляем дополнительный код как до, так и после выполнения метода. Существуют и другие типы советов, такие как « до » и « после », но они не будут рассматриваться в этой статье.

Далее, наша аннотация @Around имеет аргумент point cut. Наш pointcut просто говорит: «Примените этот совет к любому методу, аннотированному @LogExecutionTime ». Есть много других типов точечных сокращений, но они снова будут опущены в область видимости.

Сам метод logExecutionTime() является нашим советом. Существует единственный аргумент ProceedingJoinPoint . В нашем случае это будет исполняемый метод с аннотацией @LogExecutionTime.

Наконец, когда наш аннотированный метод будет вызван, то сначала будет вызван наш совет. Тогда это зависит от нашего совета, чтобы решить, что делать дальше. В нашем случае мы советуем не делать ничего, кроме вызова continue(), который просто вызывает исходный аннотированный метод.

7. Регистрация времени выполнения

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

@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();

Object proceed = joinPoint.proceed();

long executionTime = System.currentTimeMillis() - start;

System.out.println(joinPoint.getSignature() + " executed in " + executionTime + "ms");
return proceed;
}

Опять же, мы не сделали здесь ничего особенно сложного. Мы только что записали текущее время, выполнили метод, а затем вывели на консоль количество времени, которое потребовалось. Мы также регистрируем сигнатуру метода, которая предоставляется для использования экземпляра точки соединения . Мы также могли бы получить доступ к другим битам информации, если бы захотели, например к аргументам метода.

Теперь давайте попробуем аннотировать метод с помощью @LogExecutionTime, а затем выполнить его, чтобы посмотреть, что произойдет. Обратите внимание, что для правильной работы это должен быть Spring Bean:

@LogExecutionTime
public void serve() throws InterruptedException {
Thread.sleep(2000);
}

После выполнения мы должны увидеть в консоли следующее:

void org.foreach.Service.serve() executed in 2030ms

8. Заключение

В этой статье мы использовали Spring Boot AOP для создания нашей пользовательской аннотации, которую мы можем применить к компонентам Spring, чтобы внедрить в них дополнительное поведение во время выполнения.

Исходный код нашего приложения доступен на GitHub ; это проект Maven, который должен работать как есть.