Hello, guys! Без розуміння основних концепцій досить складно вникнути у фреймворки та підходи до побудови функціоналу. Тому сьогодні поговоримо про одну з таких концепцій — АОП, або аспектно-орієнтоване програмування . Ця тема не з легких і нечасто застосовується безпосередньо, але у багатьох фреймворках та технологіях вона використовується під капотом. Ну і звичайно, іноді на співбесідах вас можуть попросити розповісти загалом, що це за звір такий і де його можна застосувати. Тому давайте розглянемо основні концепції та кілька нескладних прикладів AOП на Java . Отже, АОП - аспектно-орієнтоване програмування— це парадигма, спрямовану підвищення модульності різних частин програми з допомогою поділу наскрізних завдань. Для цього до вже існуючого коду додається додаткова поведінка, без змін у початковому коді. Іншими словами, ми ніби навішуємо зверху на методи і класи додаткову функціональність, не вносячи поправки в код, що модифікується. Навіщо це потрібно? Рано чи пізно ми приходимо до того, що звичайний об'єктно-орієнтований підхід не завжди може ефективно вирішити ті чи інші завдання. У такий момент на допомогу приходить АОП і дає нам додаткові інструменти для створення програми. А додаткові інструменти - це збільшення гнучкості при розробці, завдяки якій з'являється більше варіантів вирішення того чи іншого завдання.
Застосування АОП
Аспектно-орієнтоване програмування призначене для вирішення наскрізних завдань, які можуть являти собою будь-який код, що багаторазово повторюється різними методами, який не можна повністю структурувати в окремий модуль. Відповідно, за допомогою АОП ми можемо залишити це за межами основного коду та визначити його по вертикалі. Як приклад можна навести застосування політики безпеки в будь-якій програмі. Як правило, безпека проходить крізь багато елементів програми. Тим більше, політика безпеки програми повинна застосовуватись однаково до всіх існуючих та нових частин програми. При цьому політика безпеки може й сама розвиватися. Ось тут нам відмінно може стати в нагоді використання АОП. Також як ще один приклад можна навести логування . Використання АОП підходу до логування має кілька переваг у порівнянні з ручною вставкою логування:- Код для логування легко впроваджувати і видаляти: всього потрібно додати або видалити пару конфігурацій деякого аспекту.
- Весь вихідний код для логування зберігається в одному місці і не потрібно знаходити всі місця використання вручну.
- Код, призначений для логування, можна додати в будь-яке місце, будь то вже написані методи і класи або новий функціонал. Це зменшує кількість помилок розробника.
Також при видаленні аспекту конфігурації конструкції можна бути абсолютно впевненим, що весь код трасування видалений і нічого не пропущено. - Аспекти - це винесений окремо код, який можна багаторазово перевикористовувати та покращувати.
Основні поняття АОП
Щоб просунутися далі у розборі теми, спочатку познайомимося з головними поняттями АОП. Порада (advice) – це додаткова логіка, код, який викликається з точки з'єднання. Порада може бути виконана до, після або замість точки з'єднання (про них нижче). Можливі види порад :- Перед (Before) - поради цього запускаються перед виконанням цільових методів - точок з'єднання. При використанні аспектів у вигляді класів ми беремо @Before анотацію, щоб помітити тип поради як перед. При використанні аспектів як файлів .aj це буде метод before() .
- Після (After) — поради, які виконуються після завершення виконання методів — точок з'єднання як у звичайних випадках, так і при киданні виключення.
При використанні аспектів у вигляді класів ми можемо використовувати анотацію @After для вказівки, що це порада, що йде після.
При використанні аспектів як файлів .aj це буде метод after() . - Після повернення (After Returning) дані поради виконуються тільки в тому випадку, коли цільовий метод відпрацьовує нормально, без помилок.
Коли аспекти представлені у вигляді класів, ми можемо використовувати анотацію @AfterReturning , щоб помітити пораду як після успішного завершення.
При використанні аспектів як файлів .aj це буде метод after() returning (Object obj) . - Після кидання (After Throwing) - даний вид порад призначений для тих випадків, коли метод, тобто точка з'єднання видає виняток. Ми можемо використовувати цю пораду для обробки невдалого виконання (наприклад, для відкату всієї транзакції або логування з необхідним рівнем трасування).
Для аспектів-класів анотація @AfterThrowing використовується, щоб вказати, що ця порада використовується після кидка виключення.
При використанні аспектів як файлів .aj це буде метод — after() throwing (Exception e) . - Навколо (Around) — мабуть, один із найважливіших видів порад, що оточує метод, тобто точку з'єднання, за допомогою якого ми можемо, наприклад, вибрати, виконувати даний метод точки з'єднання чи ні.
Можна написати код поради, який виконуватиметься до та після виконання методу точки з'єднання.
До обов'язків around advice входить виклик методу точки з'єднання та повернення значень, якщо метод щось повертає. Тобто в цій раді можна просто зімітувати роботу цільового методу, не викликаючи його, і як результат повернути щось своє.
При аспектах у вигляді класів використовуємо анотацію @Around для створення порад, що обертають точку з'єднання. При використанні аспектів у вигляді файлів .ajце буде метод around() .
- плетіння під час компіляції - якщо у вас є вихідний код аспекту та код, в якому ви використовуєте аспекти, ви можете скомпілювати вихідний код та аспект безпосередньо за допомогою компілятора AspectJ;
- посткомпіляційне плетіння (бінарне плетіння) — якщо ви не можете або не хочете використати перетворення вихідного коду для вплетення аспектів у код, ви можете взяти вже скомпіловані класи або jar-файли та впровадити аспекти;
- Плетіння під час завантаження - це просто бінарне плетіння, відкладене до моменту, коли завантажувач класів завантажить файл класу та визначить клас для JVM.
Для підтримки цього потрібно один або кілька «завантажувачів класів плетіння». Вони або явно надаються середовищем виконання, або активуються за допомогою агента плетіння.
Приклади в Java
Для більшого розуміння АОП ми розглянемо невеликі приклади рівня Hello World. Відразу зазначу, що в наших прикладах використовуватимемо плетіння під час компіляції . Спершу нам потрібно прописати наступну залежність у нашому 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() .
Привіт Толя Привіт Вова Привіт Саша
Приклад №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") - порада, яка буде виконана після успішного виконання цільового методу. У нас тут є два випадки:
- Коли метод має повертається значення if (returningValue != null) {
- Коли значення, що повертається, немає else {
Успішно виконаний метод - setValue, класу- Main Успішно виконаний метод - getValue, класу- Main, з результатом виконання - < деяке значення> Метод - checkValue, класу- Main, був аварійно завершений з винятком - java.lang.Exception Метод - main, класу- Main, був аварійно завершений з винятком - java.lang.Exception
Ну і так як ми не обробабо винятки, ще отримаємо його стектрейс: Почитати про винятки та їх обробку можна в цих статтях: Винятки в Java та Винятки та їх обробка . На цьому сьогодні маю все. Сьогодні ми познайомабося з АОП , і ви змогли побачити, що цей звір не такий страшний, як його малюють. Goodbye everyone!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ