Детальний огляд AOP в Java. Weaving: CTW, LTW, RTW. Способи використання. Порівняння інструментів, пояснення анотацій, конфігурування, термінологія.
Що таке АОП?
Aspect-oriented Programming (AOP) complements Object-oriented Programming (OOP) by providing another way of thinking about program structure. The key unit of modularity in OOP is the class, whereas in AOP the unit of modularity is the aspect. Aspects enable the modularization of concerns (such as transaction management) that cut across multiple types and objects. (Such concerns are often termed "crosscutting" concerns in AOP literature.) (Spring Docs)
Аспект - це частина програми (модуль), який містить вказівки, які дозволяють визначати, які частини коду можна модифікувати окремо від основного коду програми. Аспект вказує що модифікувати і як модифікувати.
АОП не заміняє ООП, воно із ним тісно співіснує. Аспекти змінюють, або доповнюють поведінку об’єктів, дозволяють уникнути дублювання коду, інтегрувати функціонал у складних для модифікації місцях.
АОП відкриває велику силу, подібну до тої, що дає рефлексія, але із цією силою приходить велика відповідальність.
Філософія АОП
Основна ідея АОП полягає в розділені коду програми на виконавчі одиниці (методи, класи) та аспекти. Аспекти виконують функцію crosscutting (перерізних), або звичайних функціональних зв’язків між виконавчими одиницями.
Брутально кажучи, аспекти або доповнюють комінікацію між методами додатковим кодом, або повністю заміняють методи, або заміняють лише частину виконання методу.
Хороші аспекти зменшують дублювання коду, покращують модульність програми та зменшують загальну складність.
Не всі Java розробники стикаються з АОП, хоч це і старенький підхід, який зародився ще в 90-х, але останнім часом він набирає все більшої популярності.
Способи використання
Коротко кажучи, все що ви робите в ООП можна робити і через АОП, от тільки АОП дозволить вам роз’єднати надлишкову логіку від основної логіки програми, що зробить ваші застосунки більш SOLID.
Приклад - Логування і Метрики:
Код який виконує логування, запис метрик і відправлення даних в кафку.
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;
@Component
public class DataProcessor implements Processor {
@Override
public void processData(String data) {
logData(data);
writeMetric(data.length());
sendToKafka();
}
}
Нам необхідне і логування і запис метрик і відправлення в кафку. Це вимагає наш PM, а ми слухняно пишемо.
Проблема в тому, що цей код доведеться писати для кожного нового Processor. Було би добре, якби ця логіка стосувалась окремого логічного блоку, код для логування тільки в модулі для логування, код для метрик тільки в модулі для метрик. Це покращить модульність і узагальнить код.
public class ConsoleLogger {
public void logData(String message) {
System.out.println("Logging: " + message);
}
}
public class MetricConsoleWriter {
public void writeMetric(String metric) {
System.out.println("Size: " + metric);
}
}
Круто, модульність досягнута, але чи можна це ще покращити? Для нового Processor в нашому модулі потрібно буде писати нові виклики Logger/MetricWriter, добавляти залежності і повторювати цей процес багато раз.
Можна написати аспект який замінить Logger/MetricWriter для кожного нового Processor. Зберігаємо модульність і уникаємо зайвого коду.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class LoggingAspect {
@Before("execution(* com.example.processor.Processor.processData(String)) && args(data)")
public void beforeProcessing(String message) {
System.out.println("Logging: " + message);
}
}
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MetricAspect {
@Before("execution(* com.example.processor.Processor.processData(String)) && args(data)")
public void beforeProcessing(String metric) {
System.out.println("Size: " + metric.length());
}
}
Тепер для кожної імплементації інтерфейсу буде виконуватись логування і запис метрики. Інтерфейс забезпечує незмінність типу аргументу, а Аспекти гарантують логування і запис метрик.
Необхідно лише вказати шлях до методу інтерфейсу.
Короткий список можливого використання АОП:
Логування: Аспект логування може додавати записи перед викликами методів, після їх виклику або при виникненні певних подій.
Перехоплення помилок: Аспект може перехоплювати ексепшини, логувати їх та виконувати відповідні дії для їх обробки.
Транзакційне керування: Управління транзакціями баз даних, забезпечення консистентності даних та виконання змін у базі даних у межах транзакцій. Аспект транзакцій може автоматично розпочинати, фіксувати або відміняти транзакції за необхідності (Так реалізована анотація @Transactional)
Безпека: Забезпечення доступу до ресурсів лише авторизованим користувачам та захист від несанкціонованого доступу. Аспект безпеки може перевіряти права доступу до методів або ресурсів.
Аудит/Метрики: Запис дій, які виконуються користувачами або системою, для аналізу діяльності та виявлення потенційних проблем.
Кешування: Зберігання результатів обчислень або запитів для прискорення доступу до них в майбутньому.
Як Spring використовує AOP?
Spring використовує proxy для реалізації AOP всередині свого фреймворку.
@Transactional. Це завжди те, що мені перше приходить в голову. Spring створює проксі, огортає блок в try catch і робить ролбек, якщо відбуваються якісь проблеми під час комунікації з БД.
Spring Security - @Secured, @PreAuthorize, @PostAuthorize
Відловлювання ексепшинів - @ExceptionHandler, @ControllerAdvice
Валідація - @Valid, @Validated
Термінологія
Сoncern
Це поведінка, яку ми хочемо мати в певному модулі додатку. Це ціль, яку переслідує розробник.
Advice
Це дія, яку проводить аспект у певній точці з’єднання (joint point). Щоб легше це запам’ятати, то вважайте, що методи аспекту радять оригінальному коду, що робити.
(Така ось ввічлива Java, на словах радять, а на ділі змушують)
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class SimpleAspect {
@Before("execution(* com.example.*.*(..))")
public void beforeMethodExecution() {
System.out.println("Before method execution");
}
}
Тут Advice це System.out.println("Before method execution");
Join point
Це певна точка у виконанні програми, місце виклику методу, або обробки ексепшина.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class SimpleAspect {
@Before("execution(* com.example.*.*(..))")
public void beforeMethodExecution() {
System.out.println("Before method execution");
}
}
З цього самого прикладу Joint point це execution(* com.example.*.*(..))
Для написання правильних joint point може піти не одна година. Тому корисуйтесь документацією AspectJ при написанні.
У цьому прикладі значення зірочки (*) це вказівка програмі, яка проводитиме аспектування, що можна брати будь-який метод, будь-якого типу повернення, з будь-якими, але присутніми, аргументами, а ось позиція зірочки вказує на те, який саме опис методу брати, чи тип доступу, чи ім’я пакету.
Pointcut
Це набір з одного або декількох joint point де повинен бути виконаний Advice.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MultipleJoinPointsAspect {
@Pointcut("execution(* com.example.service.*.*(..)) || execution(* com.example.repository.*.*(..))")
public void pointcut() {}
@Before("pointcut()")
public void beforeServiceOrRepositoryMethodExecution() {
System.out.println("Before executing a method in the service or repository layer.");
}
}
Тут декілька join point об’єднані в один pointcut pointcut()
.
Щоб зробити використання аспектів більш явним можна використовувати користувацькі анотації в pointcuts.
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface PointcutForMyClass{
}
@PointcutForMyClass
public class MyClass {
@PointcutForMyClass
public void myMethod() {
// Явне використання поїнткату цього метода/класа
}
}
import org.aspectj.lang.annotation.After;
@Aspect
public class MyAspect {
@Pointcut("@annotation(com.example.PointcutForMyClass)")
public void pointCutForMyClassAnnotation() {}
@After("pointCutForMyClassAnnotation()")
public void afterMyAnnotation() {
// Advice
}
}
Так, pontcuts можна визначати в окремих методах і так, pointcuts можна використовувати для анотації, роблячи аспекти явними і код надійнішим.
Aspect
Це код, який вказує pointcut і advice.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class SimpleAspect {
@Before("execution(* com.example.*.*(..))")
public void beforeMethodExecution() {
System.out.println("Before method execution");
}
}
Тут pointcut містить один join point - execution(* com.example.*.*(..))
Та доволі простий advice - System.out.println("Before method execution");
Crosscutting Aspects
Це аспекти програми, які впливають на декілька модулів без можливості бути інкапсульованим в жодному із них.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(* com.example.service.*.*(..)) || execution(* com.example.repository.*.*(..))")
public void loggingPointcut() {}
@Before("loggingPointcut()")
public void beforeMethodExecution() {
System.out.println("Logging: Method is about to be executed.");
}
}
Цей Crosscutting Aspect перехоплюватиме код і для com.example.service.*.*(..))
і для execution(* com.example.repository.*.*(..))
Зверніть увагу, що у pointcut використовуються два різні модулі: service
та repository
.
Weaving. (CTW, LTW, RTW)
Weaving - (вплітання), це одна з методолій, яку можна використовувати для написання AOP. Ми обговорювали, як Spring використовує Proxy, огортаючи свої методи. Використання Proxy це доволі простий спосіб, який не вимагає маніпуляцій з байт кодом і він відноситься лише до одного з декількох способів “вплітання коду“, Runtime weaving.
Weaving вплітає advice в код програми, зазвичай використовуючи байт код напряму. Він це може робити під час компіляції (Compile-Time Weaving / Binary Weaving), під час загрузки коду в JVM (Load-Time Weaving) і під час виконання коду (Runtime Weaving).
Compile-time Weaving
Очевидно, що процес відбувається під час компіляції. AspectJ компілятор об’єднує необхідні файли і передає їх дальше.
Binary Weaving
Цей процес схожий на минулий, адже теж відбувається під час компіляції.
Різниця полягає в тому, що за допомогою Binary Weaving ми можемо вплітати аспекти у вихідний код іншої бібліотеки.
Load-time Weaving
Це той підхід з яким я мав найбільше справ. LTW відбувається в той момент, коли класи готуються до загрузки в JVM.
Алгоритм вплітання LTW:
Віртуальна машина ініціалізує Джава Агент, який буде використаний для вплітання. (Про Джава Агент нижче)
Агент шукає та підтягує aop.xml файли.
Агент завантажує необхідні аспекти з aop.xml файлів.
Система починає звичне виконання
Віртуальна машина завантажує класи протягом виконання
Віртуальна машина сповіщає Джава Агента кожного разу, коли завантажує клас.
Агент після того, як отримає повідомлення від віртуальної машини перевіряє клас, який потрібно завантажити у віртуальну машину, щоб визначити, чи необхідно в цей клас вплести один з аспектів.
Якщо необхідно, то Агент вплете аспект у клас.
“Вплетений“ байт код буде завантажений у VM.
Runtime Weaving
Аспекти вплітаються динамічно під час виконання програми. Той самий @Transactional і Proxy про який ми вже згадували. Нічого магічного.
Є наукове дослідження про швидкість виконання і утилізацію ресурсів різних weaving методів.
Spring AOP використовує лише Runtime Weaving.
Чим нижча абстракція тим більше можливостей дає weaving.
Інструменти
Є два основні інструменти у використанні AOP: Spring AOP та AspectJ. AspectJ появився раніше, але Spring AOP легше використовувати і він відразу інтегрований в середовище Spring.
Основна відмінність полягає в вплітанні коду. AspectJ працює на рівні байт-коду, що дозволяє безпосередньо вплітати аспекти в скомпільований код. Spring AOP, з іншого боку, використовує проксі. Він створює проксі-об’єкти навколо цільових об’єктів і перехоплює виклики методів через ці проксі обгортки. Цей підхід більш легкий у реалізації, але має обмеження щодо того, що можна перехопити.
Окрім Spring AOP та AspectJ ще є багато інших інструментів для написання AOP: Guice AOP, JBoss AOP, AspectWerkz, ProxyFactoryBean.
Spring AOP
Spring AOP використовує RTW, існує в середовищі Spring Framework, використовує AspectJ аннотації.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore() {
System.out.println("Logging method execution");
}
}
AspectJ
AspectJ не вимагає ніяких фреймворків, він існує в своєму світі, появився перший, має власну мову і здатен на всі види weaving.
public aspect LoggingAspect {
pointcut serviceMethods(): execution(* com.example.service.*.*(..));
before(): serviceMethods() {
System.out.println("Logging method execution: " + thisJoinPoint.getSignature().getName());
}
}
Приклад написання Aspect в AspectJ.
AspectJ можна інтегрувати в Spring Framework і таким чином використовувати стиль написання Spring AOP і одночасно з тим потужність aspectJ weaving агента.
Порівняння
Feature | Підтримка в Spring AOP | Підтримка в AspectJ |
---|---|---|
Joinpoint | + | + |
Joinpoint конструктора | - | + |
Joinpoint поля | - | + |
Jointpoint статичної ініціалізації | - | + |
Around Advice | + | + |
Before/After Advice | + | + |
Pointcut Expressions | Обмежено | Розширено |
Деталізація застосування аспектів | Обмежено | Розширено |
Інтеграція з Spring Framework | + | Через мануальну інтеграцію |
З порівння вище можна зробити висновок у очевидному домінуванні AspectJ по функціоналу. Крім того, AspectJ можна використовувати в Spring, що я і раджу.
Інтеграція
Інтеграція може бути доволі болючою, особливо, коли інтегрується AspectJ в Spring. Необхідно добавити потрібні залежності, сконфігурувати Maven плагін, визначити необхідний тип вплітання, створити aop.xml, написати аспект і ще добавити JVM конфігурацію. В цьому розділі я поширю певно корисну інформацію щодо цього.
Maven
Інтеграція AspectJ LTW в Spring через Maven може бути болем для новачка. Наступні кроки описують інтеграцію AspectJ LTW в Maven.w
1. Добавити aspectJ залежність
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
</dependency>
2. Добавити плагін
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>${aspectj-maven-plugin.version}</version>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
<configuration>
<outxml>true</outxml>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<properties>
<aspectj.version>1.9.7</aspectj.version>
<aspectj-maven-plugin.version>1.12.6</aspectj-maven-plugin.version>
</properties>
<outXml>true</outxml>
генерує aop.xml файл на базі об’явлених аспектів в коді, можна обійтись без нього, але тоді треба писати свій aop.xml.
LTW є вплітанням по замовчуванню в AspectJ
Іноді необхідно добавити aspectjrt або aspectjtools до плагіну, залежно від того, що вам потрібно.
3. Написати aop.xml
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
<aspects>
<aspect name="com.example.aspect.LoggingAspect"/>
</aspects>
<weaver options="-Xset:weaveJavaxPackages=true -verbose -showWeaveInfo">
<include within="com.example.*"/>
</weaver>
</aspectj>
Тут можна описати додаткові опції для вплітання, ті самі, які передаються через CLI.
4. Написати аспект
package com.example.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeServiceMethods() {
System.out.println("Logging before executing service methods...");
}
@Before("execution(* com.example.controller.*.*(..))")
public void beforeControllerMethods() {
System.out.println("Logging before executing controller methods...");
}
}
5. Зібрати проект і запустити jar через javaagent
java -javaagent:/path/to/aspectjweaver.jar -jar your-application.jar
Іноді треба добавляти --add-opens
, що не рекомендовано, але швидко усуне ваші проблеми.
Java Agent
Java Agent - це програма, яка запускається під час завантаження JVM і має доступ до всіх класів та об'єктів, що працюють в межах JVM. Java Agent використовують для моніторингу та, як ми вже знаємо, для вплітання аспектів.
Є два основних Java Agent для вплітання коду.
Від Spring: -javaagent:path/to/org.springframework.instrument-{version}.jar
Він дозволяє LTW.
Від AspectJ: -javaagent:path/to/aspectjweaver.jar
Він дозволяє і CTW і LTW.
Якщо у вас виникають проблеми з прописанням javaagent через параметр до JVM, то ви можете скористатись бібліотеками для динамічного добавляння Java Agent, наприклад ByteBuddy.
Lombok & AspectJ CTW
Lombok змінює код ваших класів під час компіляції, що може непередбачено вплинути на роботу СTW Java Agent, або взагалі зробити її неможливою.
Наразі є рішення. Треба сконфігурувати aspectjweaver використовувати in-place weaving feature. Якщо не використовувати in-place, то плагін ігноруватиме зміни, які робитиме Lombok і використовуватиме .java файли для weaving.
<forceAjcCompile>true</forceAjcCompile>
<sources/>
<weaveDirectories>
<weaveDirectory>${project.build.directory}/classes</weaveDirectory>
</weaveDirectories>
Анотації
@Aspect: Ця анотація позначає клас як аспект у системі AspectJ.
@Before: Вказує, що advice повинен виконатися перед викликом методу, відповідно до pointcut, який він аспектує.
@AfterReturning: Позначає advice, який виконується після успішного завершення методу.
@AfterThrowing: Вказує, що advice повинен виконатися після виклику методу, якщо метод виконався з ексепшином.
@After: Позначає advice, який виконується після виклику методу, незалежно від того, чи відбулася помилка чи ні.
@Around: Вказує, що advice повинен виконатися навколо методу, тобто до і після його виклику. Це найбільш гнучкий тип advice. Крім того, якщо зробити @Around і нічого не написати в тілі методу, то ви просто його проігноруєте, схоже на мокінг.
@Pointcut: Визначає pointcut, тобто місце в програмі, де може бути застосований аспект.
@DeclareParents: Дозволяє динамічно додавати методи та інтерфейси до більш широкого набору класів.
@DeclareError: Дозволяє аспекту викидати ексепшини під час виконання.
@DeclareWarning: Дозволяє аспекту генерувати warning під час виконання.
@DeclarePrecedence: Вказує порядок виконання аспектів, які можуть мати конфліктуючі pointcuts.
@DeclareSoft: Вказує, що аспект має м'яку залежність від іншого аспекту, тобто він використовується лише в разі, якщо інший аспект використовується.
Конфігурації
Тут описані VM конфігурації для використання aspectJweaver джава агенту.
-javaagent:/path/to/aspectjweaver.jar
- Визначає aspectjweaver як Java Agent.
-Daj.weaving.verbose=true
- Додає додаткові логи, які стосуватимуться процесу вплітання, дуже важлива конфігурація.
-Daj.weaving.loadtime=true
- Змушує джава агент працювати як LTW, не обов’язкова анотація, можна обійтись конфігурацією плагіна в Maven.
-Daj.aspectpath=/path/to/aspects
- Вказує шлях до файлу з аспектами (aop.xml)
Іноді необхідно добавляти додаткові дозволи, якщо ваш код збираєтсья вплітатись в стандартні класи JDK.
Плюси і мінуси в дизайні
Плюси доволі зрозумілі, це краща модульність коду, уникнення дублікацій, реверс інжиніринг бібліотек, насправді це чарівна паличка в руках.
Мінус це слон у кімнаті, який чекає поки я закінчу писати цей лонгрід. Очевидно, що Аспекти це про сильну залежність на виконавчий код. Є деякі розробники, які вважають, що аспекти це крайнє рішення, саме через те, що їх важко підтримувати і вони можуть посипатись при незначних змінах в коді.
Крім того при прочитанні первинного коду ви навіть не будете знати, що є якийсь аспект, якийсь десь там робить магію, це вплине на розуміння, дебагу. Ви запускаєте дебаг і отримуєте результат незрозуміло звідки. Це частково вирішується використанням користувацьких анотацій.
Зрештою як завжди, це питання до сумління розробника, як ви гарантуєте надійність вашого коду? Можна все покрити тестами, це не зробить код більш підтримуваним, але дозволить уникнути ризикованого деплойменту.
Я закінчую короктий огляд АОП в Джаві, використовуйте з розумом.