JavaRush /Java блог /Random UA /Що таке АОП? Основи аспектно-орієнтованого програмування
Константин
36 рівень

Що таке АОП? Основи аспектно-орієнтованого програмування

Стаття з групи Random UA
Hello, guys! Без розуміння основних концепцій досить складно вникнути у фреймворки та підходи до побудови функціоналу. Тому сьогодні поговоримо про одну з таких концепцій — АОП, або аспектно-орієнтоване програмування . Що таке АОП?  Основи аспектно-орієнтованого програмування.Ця тема не з легких і нечасто застосовується безпосередньо, але у багатьох фреймворках та технологіях вона використовується під капотом. Ну і звичайно, іноді на співбесідах вас можуть попросити розповісти загалом, що це за звір такий і де його можна застосувати. Тому давайте розглянемо основні концепції та кілька нескладних прикладів 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 з наявністю даного аспекту ми отримаємо висновок у консоль:
Привіт Толя Привіт Вова Привіт Саша
p align="justify"> Ми бачимо, що кожен виклик методу 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
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ