JavaRush /Java блог /Java Developer /Что такое АОП? Основы аспектно-ориентированного программи...
Константин
36 уровень

Что такое АОП? Основы аспектно-ориентированного программирования

Статья из группы Java Developer
Hello, guys! Без понимания основных концепций довольно сложно вникнуть во фреймворки и подходы к построению функционала. Так что сегодня поговорим об одной из таких концепций — АОП, или аспектно-ориентированное программирование.Что такое АОП? Основы аспектно-ориентированного программирования - 1Это тема не из легких и нечасто применяется напрямую, но во многих фреймворках и технологиях она используется под капотом. Ну и конечно, иногда на собеседованиях вас могут попросить рассказать в общих чертах, что это за зверь такой и где его можно применить. Поэтому давайте рассмотрим основные концепции и несколько несложных примеров AOП на Java.Что такое АОП? Основы аспектно-ориентированного программирования - 2Итак, АОПаспектно-ориентированное программирование — это парадигма, направленная на повышение модульности различных частей приложения за счет разделения сквозных задач. Для этого к уже существующему коду добавляется дополнительного поведение, без изменений в изначальном коде. Иными словами, мы как бы навешиваем сверху на методы и классы дополнительную функциональность, не внося поправки в модифицируемый код. Зачем это нужно? Рано или поздно мы приходим к тому, что обычный объектно-ориентированный подход не всегда может эффективно решить те или иные задачи. В такой момент на помощь приходит АОП и дает нам дополнительные инструменты для постройки приложения. А дополнительные инструменты — это увеличение гибкости при разработке, благодаря которой появляется больше вариантов решения той или иной задачи.

Применение АОП

Аспектно-ориентированное программирование предназначено для решения сквозных задач, которые могут представлять собой любой код, многократно повторяющийся разными методами, который нельзя полностью структурировать в отдельный модуль. Соответственно, с помощью АОП мы можем оставить это за пределами основного кода и определить его по вертикали. В качестве примера можно привести применение политики безопасности в каком-либо приложении. Как правило безопасность проходит сквозь многие элементы приложения. Тем более, политика безопасности приложения должна применяться одинаково ко всем существующим и новым частям приложения. При этом используемая политика безопасности может и сама развиваться. Вот тут нам отлично может пригодится использование АОП. Также в качестве еще одного примера можно привести логирование. У использования АОП подхода к логированию есть несколько преимуществ по сравнению с ручной вставкой логирования:
  1. Код для логирования легко внедрять и удалять: всего-то нужно добавить или удалить пару конфигураций некоторого аспекта.
  2. Весь исходный код для логирования хранится в одном месте и не нужно находить вручную все места использования.
  3. Код, предназначенный для логирования, можно добавить в любое место, будь то уже написанные методы и классы или же новый функционал. Это уменьшает количество ошибок разработчика.
    Также при удалении аспекта из конфигурации конструкции можно быть абсолютно уверенным, что весь код трассировки удален и ничего не пропущено.
  4. Аспекты — это вынесенный отдельно код, который можно многократно переиспользовать и улучшать.
Что такое АОП? Основы аспектно-ориентированного программирования - 3Также АОП используется для обработки исключений, кеширования, выноса некоторого функционала, чтобы сделать его переиспользуемым.

Основные понятия АОП

Чтобы продвинуться дальше в разборе темы, для начала познакомимся с главными понятиями АОП. Совет (advice) — это дополнительная логика, код, который вызывается из точки соединения. Совет может быть выполнен до, после или вместо точки соединения (о них — ниже). Возможные виды советов:
  1. Перед (Before) — советы данного типа запускаются перед выполнением целевых методов — точек соединения. При использовании аспектов в виде классов мы берем @Before аннотацию, чтобы пометить тип совета как идущий перед. При использовании аспектов в виде файлов .aj это будет метод before().
  2. После (After) — советы, которые выполняются после завершения выполнения методов — точек соединения, как в обычных случаях, так и при бросании исключения.
    При использовании аспектов в виде классов мы можем использовать @After аннотацию для указания, что это совет, идущий после.
    При использовании аспектов в виде файлов .aj это будет метод after().
  3. После возврата (After Returning) — данные советы выполняются только в том случае, когда целевой метод отрабатывает нормально, без ошибок.
    Когда аспекты представлены в виде классов, мы можем использовать аннотацию @AfterReturning, чтобы пометить совет как выполняемый после успешного завершения.
    При использовании аспектов в виде файлов .aj это будет метод after() returning (Object obj).
  4. После бросания (After Throwing) — данный вид советов предназначен для тех случаев, когда метод, то есть точка соединения выдает исключение. Мы можем использовать этот совет для некой обработки неудачного выполнения (к примеру, для отката всей транзакции или логирования с необходимым уровнем трассировки).
    Для аспектов-классов аннотация @AfterThrowing используется, чтобы указать, что этот совет используется при после броска исключения.
    При использовании аспектов в виде файлов .aj это будет метод — after() throwing (Exception e).
  5. Вокруг (Around) — пожалуй, один из самых важных видов советов, который окружает метод, то есть — точку соединения, с помощью которого мы можем, к примеру, выбрать, выполнять данный метод точки соединения или нет.
    Можно написать код совета, который будет выполняться до и после выполнения метода точки соединения.
    В обязанности around advice входит вызов метода точки соединения и возвращение значений, если метод что-то возвращает. То есть в этом совете можно попросту сымитировать работу целевого метода, не вызывая его, и в качестве результата вернуть что-то свое.
    При аспектах в виде классов используем @Around аннотацию для создания советов, оборачивающих точку соединения. При использовании аспектов в виде файлов .aj это будет метод around().
Точка соединения (join point) — точка в выполняемой программе (вызов метода, создание объекта, обращение к переменной), где следует применить совет. Иначе говоря, это некоторое регулярное выражение, с помощью которого и находятся места для внедрения кода (места для применения советов). Срез (pointcut) — набор точек соединения. Срез определяет, подходит ли данная точка соединения к данному совету. Аспект (aspect) — модуль или класс, реализующий сквозную функциональность. Аспект изменяет поведение остального кода, применяя совет в точках соединения, определенных некоторым срезом. Иными словами, это комбинация советов и точек соединения. Внедрение (introduction) — изменение структуры класса и/или изменение иерархии наследования для добавления функциональности аспекта в инородный код. Цель (target) — объект, к которому будут применяться советы. Плетение (weaving) — это процесс связывания аспектов с другими объектами для создания рекомендуемых прокси-объектов. Это можно сделать во время компиляции, загрузки или во время выполнения. Есть три вида плетения:
  • плетение во время компиляции — если у вас есть исходный код аспекта и код, в котором вы используете аспекты, вы можете скомпилировать исходный код и аспект напрямую с помощью компилятора AspectJ;
  • посткомпиляционное плетение (бинарное плетение) — если вы не можете или не хотите использовать преобразования исходного кода для вплетения аспектов в код, вы можете взять уже скомпилированные классы или jar-файлы и внедрить аспекты;
  • плетение во время загрузки — это просто бинарное плетение, отложенное до момента, когда загрузчик классов загрузит файл класса и определит класс для JVM.
    Для поддержки этого требуется один или несколько «загрузчиков классов плетения». Они либо явно предоставляются средой выполнения, либо активируются с помощью «агента плетения.
AspectJ — конкретная реализация парадигм АОП, реализующая возможности решения сквозных задач. Документацию можно найти вот тут.

Примеры в Java

Далее для большего понимания АОП мы рассмотрим небольшие примеры уровня Hello World.Что такое АОП? Основы аспектно-ориентированного программирования - 4Сразу отмечу, что в наших примерах будем использовать плетение во время компиляции. Сперва нам нужно прописать следующую зависимость в нашем pom.xml:

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.9.5</version>
</dependency>
Как правило для использования аспектов применяют особый компилятор Ajs. В IntelliJ IDEA по умолчанию его нет, поэтому при выборе его как компилятора приложения нужно указать путь к дистрибутиву AspectJ. Подробнее о способе выбора Ajs как компилятора можно почитать на этой странице. Это был первый способ, а второй (которым я и воспользовался) — прописать следующий плагин в pom.xml:

<build>
  <plugins>
     <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>aspectj-maven-plugin</artifactId>
        <version>1.7</version>
        <configuration>
           <complianceLevel>1.8</complianceLevel>
           <source>1.8</source>
           <target>1.8</target>
           <showWeaveInfo>true</showWeaveInfo>
           <verbose>true</verbose>
           <Xlint>ignore</Xlint>
           <encoding>UTF-8</encoding>
        </configuration>
        <executions>
           <execution>
              <goals>
                 <goal>compile</goal>
                 <goal>test-compile</goal>
              </goals>
           </execution>
        </executions>
     </plugin>
  </plugins>
</build>
После этого желательно сделать реимпорт у Мавена и запустить mvn clean compile. А теперь перейдём непосредственно к примерам.

Пример №1

Давайте создадим класс Main. В нем у нас будет точка запуска и метод, который печатает в консоли переданные ему имена:

public class Main {
 
  public static void main(String[] args) {
  printName("Толя");
  printName("Вова");
  printName("Саша");
  }
 
  public static void printName(String name) {
     System.out.println(name);
  }
}
Ничего сложного: передали имя — вывели его в консоли. Если мы сейчас запустим, в консоли будет выведено:
Толя Вова Саша
Что ж, пришло время воспользоваться возможностями АОП. Сейчас нам нужно создать файл — аспект. Они бывают двух видов: первый — файл с расширением .aj, второй — обычный класс, который реализует возможности АОП при помощи аннотаций. Давайте сперва рассмотрим файл с расширением .aj:

public aspect GreetingAspect {
 
  pointcut greeting() : execution(* Main.printName(..));
 
  before() : greeting() {
     System.out.print("Привет ");
  }
}
Данный файл чем-то похож на класс. Разберемся, что здесь происходит: pointcut — срез или набор точек соединения; greeting() — название данного среза; : execution — при выполнении * — всех, вызов — Main.printName(..) — данного метода. Далее идёт конкретный совет — before() — который выполняется до вызова целевого метода, : greeting() — срез, на который данный совет реагирует, ну а ниже мы видим само тело метода, которое написано на понятном нам языке Java. При запуске main с наличием данного аспекта мы получим вывод в консоль:
Привет Толя Привет Вова Привет Саша
Мы видим, что каждый вызов метода printName был модифицирован при помощи аспекта. А теперь давайте взглянем, как будет выглядеть аспект, но уже как класс Java с аннотациями:

@Aspect
public class GreetingAspect{
 
  @Pointcut("execution(* Main.printName(String))")
  public void greeting() {
  }
 
  @Before("greeting()")
  public void beforeAdvice() {
     System.out.print("Привет ");
  }
}
После файла аспекта .aj тут всё более очевидно:
  • @Aspect обозначает, что данный класс является аспектом;
  • @Pointcut("execution(* Main.printName(String))") — точка среза, которая срабатывает на все вызовы Main.printName с входящим аргументом типа String;
  • @Before("greeting()") — совет, который применяется до вызова кода описанного в точке среза greeting().
При запуске main с этим аспектом вывод в консоли не изменится:
Привет Толя Привет Вова Привет Саша

Пример №2

Допустим, у нас есть некоторый метод который осуществляет некоторые операции для клиентов и вызов этого метода из main:

public class Main {
 
  public static void main(String[] args) {
  makeSomeOperation("Толя");
  }
 
  public static void makeSomeOperation(String clientName) {
     System.out.println("Выполнение некоторых операций для клиента - " + clientName);
  }
}
С помощью аннотации @Around сделаем что-то типа “псевдотранзакции”:

@Aspect
public class TransactionAspect{
 
  @Pointcut("execution(* Main.makeSomeOperation(String))")
  public void executeOperation() {
  }

  @Around(value = "executeOperation()")
  public void beforeAdvice(ProceedingJoinPoint joinPoint) {
     System.out.println("Открытие транзакции...");
     try {
        joinPoint.proceed();
        System.out.println("Закрытие транзакции....");
     }
     catch (Throwable throwable) {
        System.out.println("Операция не удалась, откат транзакции...");
     }
  }
  }
С помощью метода proceed объекта ProceedingJoinPoint мы вызываем оборачиваемый метод, чтобы определить его место в совете и, соответственно, код в методе, который выше joinPoint.proceed(); — это Before, который ниже — After. Если мы запустим main, в консоли мы получим:
Открытие транзакции... Выполнение некоторых операций для клиента - Толя Закрытие транзакции....
Если же мы добавим бросок исключения в наш метод (вдруг выполнение операции дало сбой):

public static void makeSomeOperation(String clientName)throws Exception {
  System.out.println("Выполнение некоторых операций для клиента - " + clientName);
  throw new Exception();
}
То мы получим вывод в консоли:
Открытие транзакции... Выполнение некоторых операций для клиента - Толя Операция не удалась, откат транзакции...
Получилась такая себе псевдообработка неудачи.

Пример №3

В качестве следующего примера сделаем что-то типа логирования в консоли. Для начала посмотрим в Main, где у нас происходит псевдо бизнес-логика:

public class Main {
  private String value;
 
  public static void main(String[] args) throws Exception {
     Main main = new Main();
     main.setValue("<некоторое значение>");
     String valueForCheck = main.getValue();
     main.checkValue(valueForCheck);
  }
 
  public void setValue(String value) {
     this.value = value;
  }
 
  public String getValue() {
     return this.value;
  }
 
  public void checkValue(String value) throws Exception {
     if (value.length() > 10) {
        throw new Exception();
     }
  }
}
В main с помощью setValue мы зададим значение внутренней переменной — value, далее с помощью getValue возьмём это значение и в checkValue проверим, длиннее ли это значение 10 символов. Если да, будет брошено исключение. Теперь посмотрим на аспект, с помощью которого мы будем логировать работу методов:

@Aspect
public class LogAspect {
 
  @Pointcut("execution(* *(..))")
  public void methodExecuting() {
  }
 
  @AfterReturning(value = "methodExecuting()", returning = "returningValue")
  public void recordSuccessfulExecution(JoinPoint joinPoint, Object returningValue) {
     if (returningValue != null) {
        System.out.printf("Успешно выполнен метод - %s, класса- %s, с результатом выполнения - %s\n",
              joinPoint.getSignature().getName(),
              joinPoint.getSourceLocation().getWithinType().getName(),
              returningValue);
     }
     else {
        System.out.printf("Успешно выполнен метод - %s, класса- %s\n",
              joinPoint.getSignature().getName(),
              joinPoint.getSourceLocation().getWithinType().getName());
     }
  }
 
  @AfterThrowing(value = "methodExecuting()", throwing = "exception")
  public void recordFailedExecution(JoinPoint joinPoint, Exception exception) {
     System.out.printf("Метод - %s, класса- %s, был аварийно завершен с исключением - %s\n",
           joinPoint.getSignature().getName(),
           joinPoint.getSourceLocation().getWithinType().getName(),
           exception);
  }
}
Что тут происходит? @Pointcut("execution(* *(..))") — будет соединяться со всеми вызовами всех методов; @AfterReturning(value = "methodExecuting()", returning = "returningValue") — совет, который будет выполнен после успешного выполнения целевого метода. У нас тут есть два случая:
  1. Когда у метода есть возвращаемое значение if (returningValue != null) {
  2. Когда возвращаемого значения нет else {
@AfterThrowing(value = "methodExecuting()", throwing = "exception") — совет, который будет срабатывать при ошибке, то есть при падении исключения из метода. И соответственно, запустив main, мы получим своеобразное логирование в консоли:
Успешно выполнен метод - setValue, класса- Main Успешно выполнен метод - getValue, класса- Main, с результатом выполнения - <некоторое значение> Метод - checkValue, класса- Main, был аварийно завершен с исключением - java.lang.Exception Метод - main, класса- Main, был аварийно завершен с исключением - java.lang.Exception
Ну и так как мы не обработали исключения, еще получим его стектрейс:Что такое АОП? Основы аспектно-ориентированного программирования - 5Почитать об исключениях и их обработке можно в этих статьях: Исключения в Java и Исключения и их обработка. На этом у меня сегодня всё. Сегодня мы познакомились с АОП, и вы смогли увидеть, что сей зверь не так страшен, как его рисуют. Goodbye everyone!Что такое АОП? Основы аспектно-ориентированного программирования - 6
Комментарии (7)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Дмитрий Уровень 99 Expert
10 сентября 2023
Маловато примеров с функциями. А если функция не статическая? А если нужно создать точку соединения, скажем, после создания какого нибудь объекта?
1 февраля 2023
Advice — набор инструкций выполняемых на точках среза (Pointcut). Наверное это будет более точный перевод
24 мая 2022
Возможно глупый вопрос, но, можно ли использовать класс с аннотациями вне спринг проекта?
Евгений Уровень 41 Expert
31 января 2022
Очень классно. Где в реальности это можно применять? Пока с трудом представляю... Ну разве что спринг уже это всё использует для тех же самых транзакций например)
Валерий Уровень 1
30 апреля 2021
Спасибо за статью! Но зачем переводить на русский термины и использовать их ( особенно "совет")? Это очень затрудняет восприятие. В коде же все на английском. Программист должен знать английский и, соответственно, оперировать терминами на английском.