@AspectJ передбачає стиль оголошення аспектів як звичайних класів Java, анотованих анотаціями. Стиль @AspectJ був введений проєктом AspectJ як частина релізу AspectJ 5. Spring інтерпретує ті ж анотації, що й AspectJ 5, використовуючи бібліотеку, що поставляється AspectJ для синтаксичного аналізу та зіставлення зрізів. Однак середовище виконання АОП, як і раніше, є чистим Spring AOP, а будь-яка залежність від компілятора AspectJ або інструмента зв'язування відсутня.

Компілятор AspectJ та инструмент зв'язування, які дозволяють використовувати мову AspectJ повнофункціонально.

Увімкнення підтримки @AspectJ

Щоб використовувати аспекти @AspectJ у конфігурації Spring, необхідно ввімкнути підтримку Spring для конфігурування Spring AOP на основі аспектів @AspectJ та автопроксування бінів залежно від того, чи забезпечили їх порадами з цих аспектів чи ні. Під автопроксуванням ми маємо на увазі, що якщо Spring визначає, що бін забезпечений порадою за одним або декількома аспектами, він автоматично створює проксі для цього біна, щоб перехоплювати виклики методів та забезпечити виконання поради за необхідності.

Підтримка @AspectJ може бути активована за допомогою конфігурації у стилі XML або Java. У будь-якому випадку необхідно переконатися, що бібліотека aspectjweaver.jar з AspectJ знаходиться на шляху класів вашої програми (версія 1.8 або пізніша). Ця бібліотека доступна в каталозі lib дистрибутива AspectJ або з репозиторію Maven Central.

Увімкнення підтримки @AspectJ за допомогою конфігурації Java

Щоб активувати підтримку @AspectJ за допомогою @Configurationз Java, додай анотацію @EnableAspectJAutoProxy, як показано в наступному прикладі:

Java

@Configuration
@EnableAspectJAutoProxy public class AppConfig {
}
Kotlin

@Configuration
@EnableAspectJAutoProxy
class AppConfig

Увімкнення підтримки @AspectJ за допомогою конфігурації XML

Щоб активувати підтримку @AspectJ за допомогою конфігурації на основі XML, використовуй елемент aop:aspectj-autoproxy, як показано в наступному прикладі:

<aop:aspectj-autoproxy/>

Оголошення аспекту

За умови активованої підтримки @AspectJ будь-який бін, визначений у твоєму контексті програми за допомогою класу, який є аспектом @AspectJ (має анотацію @Aspect), автоматично визначається Spring і використовується для конфігурування Spring AOP. Наступні два приклади демонструють просте визначення, необхідне для не надто корисного аспекту.

На першому з двох прикладів показано звичайне визначення біна в контексті програми, яка вказує на клас біна, що має анотацію @Aspect:

<bean id ="myAspect" class="org.xyz.NotVeryUsefulAspect">
        <!-- налаштовуємо властивості аспекту тут -->
</bean>

На другому з двох прикладів показано визначення класу NotVeryUsefulAspect, який анотований за допомогою анотації org .aspectj.lang.annotation.Aspect;

Java

package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
Kotlin

package org.xyz
import org.aspectj.lang.annotation .Aspect;
@Aspect class NotVeryUsefulAspect

Аспекти (класи, анотовані за допомогою @Aspect) можуть мати методи і поля, як і будь-який інший клас. Вони також можуть містити оголошення зрізів, порад і введень (міжтипових).

Автоматичне виявлення аспектів за допомогою сканування компонентів
Ти можеш зареєструвати класи аспектів як звичайні біни в конфігурації Spring XML за допомогою методів, анотованих @Bean, у класах, позначених анотацією @Configuration, або зробити так, щоб Spring автоматично виявляв їх у вигляді сканування шляху класів — так само, як і будь-який інший керований Spring бін. Однак зверни увагу, що анотації @Aspect недостатньо для автоматичного виявлення в шляху класів. Для цього необхідно додати окрему анотацію @Component (або, як варіант, спеціальну стереотипну інструкцію, яка відповідає правилам сканера компонентів Spring).
Забезпечення порадою аспектів за допомогою інших аспектів?
У Spring AOP самі аспекти не можуть бути цілями постачання порадою від інших аспектів. Анотація @Aspect для класу позначає його як аспект і, отже, виключає його з автопроксування.

Оголошення зрізу

Зрізи визначають точки з'єднання і, таким чином, дозволяють контролювати час виконання порад. Spring AOP підтримує лише точки з'єднання виконання методів для бінів Spring, тому можна вважати, що зріз збігається з виконанням методів для бінів Spring. Оголошення зрізу складається з двох частин: сигнатури, що містить ім'я та будь-які параметри, та вирази зрізу, що визначає, які саме виконання методу нас цікавлять. У стилі АОП з анотацією @AspectJ сигнатура зрізу вказується визначенням звичайного методу, а вираз зрізу вказується за допомогою анотації @Pointcut (метод, який служить сигнатурою зрізу, повинен мати тип повернення void).

Приклад може допомогти прояснити різницю між сигнатурою та виразом зрізу. Наступний приклад визначає зріз з ім'ям anyOldTransfer, який відповідає виконанню будь-якого методу з ім'ям transfer:

Java

@Pointcut("execution(* transfer(..))") // вираз зрізу
private void anyOldTransfer() {} // сигнатура зрізу
Kotlin

@Pointcut("execution(* transfer(..))") // вираз зрізу
private fun anyOldTransfer() {} // сигнатура зрізу

Вираз зрізу, який формує значення анотації @Pointcut, є звичайним виразом зрізу на AspectJ. Всебічний розгляд мови зрізів AspectJ див. у Посібник з програмування на AspectJ (а для розширень — Пам'ятку розробника по AspectJ 5) або одну з книг по AspectJ (наприклад, Eclipse AspectJ за авторством Кольє та ін. або AspectJ у дії за авторством Рамніваса Ладдада).

Покажчики зрізів, що підтримуються

Spring AOP підтримує наступні покажчики (designators) зрізів AspectJ (pointcut designators/PCD) для використання у виразах зрізу:

  • execution: Для узгодження точок з'єднання під час виконання методу. Це основний покажчик зрізів, який слід використовувати в роботі зі Spring AOP.

  • Обмежує узгодження точками з'єднання всередині певних типів (виконання методу, що оголошено всередині типу, що зіставляється, при використанні Spring AOP).

  • this: Обмежує узгодження точками з'єднання (виконання методів при використанні Spring AOP), де посилання на бін (проксі Spring AOP) є екземпляром заданого типу.

  • target: Обмежує узгодження точками з'єднання (виконання методів при використанні Spring AOP), де цільовий об'єкт (об'єкт додатку, що проксується) є екземпляром заданого типу.

  • args: Обмежує узгодження точками з'єднання (виконання методів при використанні Spring AOP), де аргументами є екземпляри заданих типів.

  • @target: Обмежує узгодження точками з'єднання (виконання методів при використанні Spring AOP), де клас об'єкта, що виконується, має анотацію даного типу.

  • @args: Обмежує узгодження точками з'єднання (виконання методів при використанні Spring AOP), де тип часу виконання переданих фактичних аргументів має анотації зазначених типів.

  • @within: Обмежує узгодження точками з'єднання всередині типів, що мають цю анотацію (виконання методів, оголошених у типах за допомогою даної анотації, при використанні Spring AOP).

  • @annotation: Обмежує узгодження точками з'єднання, де суб'єкт точки з'єднання (метод, що виконується в Spring AOP) має зазначену анотацію.

Інші типи покажчиків

Повнофункціональна мова зрізів AspectJ підтримує додаткові покажчики зрізів, які не підтримуються в Spring: call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this та @withincode. Використання цих покажчиків зрізів у виразах зрізу, що інтерпретуються Spring AOP, призводить до виникнення IllegalArgumentException.

Набір покажчиків зрізів, що підтримуються Spring AOP, може бути розширений у майбутніх випусках для забезпечення підтримки більшої кількості покажчиків зрізів з AspectJ.

Оскільки Spring AOP обмежує узгодження тільки точками з'єднання виконання методу, в попередньому описі покажчиків зрізів було дано більш вузьке визначення, ніж те, яке ти можеш знайти в посібнику з програмування на AspectJ. Крім того, сам AspectJ має семантику, засновану на типах, і в точці з'єднання виконання і this, і target посилаються на той самий об'єкт: об'єкт, що виконує метод. Spring AOP — це система на основі проксі, в якій відрізняється сам проксі-об'єкт (який прив'язаний до this) і цільовий об'єкт за проксі (який прив'язаний до target).

Через проксі-орієнтовану природу АОП-фреймворку Spring виклики всередині цільового об'єкта не перехоплюються за визначенням. У разі проксі JDK можна перехоплювати лише виклики методів публічного інтерфейсу проксі. За допомогою CGLIB перехоплюються виклики публічних та захищених методів проксі-сервера (і навіть методи з областю видимості в межах пакета, якщо необхідно). Однак загальні взаємодії через проксі завжди слід оформляти через публічні сигнатури.

Зверни увагу, що визначення зрізів зазвичай порівнюються з будь-яким методом. Якщо зріз призначений лише для публічного використання, навіть у сценарії CGLIB-проксі з потенційними непублічними взаємодіями через проксі, він має бути визначений відповідним чином.

Можливість використання нативного зв'язування AspectJ, керованого Spring, замість заснованого на проксі фреймворку Spring AOP. Нативне зв'язування являє собою інший спосіб використання АОП з іншими характеристиками, тому обов'язково ознайомся зі зв'язуванням, перш ніж приймати рішення.

Spring AOP також підтримує додатковий PCD під назвою бін. Цей PCD дозволяє обмежити узгодження точок з'єднання певними іменованими бінами Spring або набором іменованих бінів Spring (при використанні підстановочних знаків). Bean PCD має таку форму:

Java
bean(idOrNameOfBean)
Kotlin
bean(idOrNameOfBean)

Маркер idOrNameOfBean може бути ім'ям будь-якого біна Spring. Передбачена обмежена можливість використання підстановочних знаків за допомогою символу *, тому якщо встановити певні угоди про іменування твоїх бінів Spring, то можна написати вираз PCD bean для здійснення їх вибірки. Як і у випадку з іншими покажчиками зрізів, PCD для bean можна використовувати з операторами && (and), || (or) і ! (negation).

PCD для Bean підтримується тільки в Spring AOP, але при нативному зв'язуванні на AspectJ. Це специфічне для Spring розширення стандартних PCD, які визначає AspectJ, і тому воно недоступне для аспектів, оголошених у моделі, анотованої @Aspect.

PCD для Bean працює лише на рівні екземпляра (грунтуючись концепції імен бінів Spring), а не лише на рівні типів (ніж обмежується АОП з урахуванням зв'язування). Вказівники зрізів на основі екземплярів — це функціональна можливість АОП-фреймворку Spring, що ґрунтується на проксі, з урахуванням його тісної інтеграції з фабрикою бінів Spring, за допомогою якої природним та доступним способом можна визначати конкретні біни на ім'я.

Комбінування виразів зрізу

Ти можеш комбінувати вирази зрізу, використовуючи &&, || та ! Також можна посилатися на вирази зрізу на ім'я. У цьому прикладі показано три вирази зрізу:

Java

@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} 
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {} 
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} 
  1. anyPublicOperation узгоджується, якщо точкою з'єднання виконання методу є виконання будь-якого публічного методу.
  2. inTrading узгоджується, якщо виконання методу відбувається в торговому модулі.
  3. tradingOperation узгоджується, якщо виконання методу представляє будь-який публічний метод у торговому модулі.
Kotlin

@Pointcut("execution(public * * (..))")
private fun anyPublicOperation() {} 
@Pointcut("within(com.xyz.myapp.trading.. *)")
private fun inTrading() {} 
@Pointcut("anyPublicOperation() && inTrading()")
private fun tradingOperation() {} 
        
  1. anyPublicOperation узгоджується, якщо точка з'єднання виконання методу є виконанням будь-якого публічного методу.
  2. inTrading узгоджується, якщо виконання методу відбувається у торговому модулі.
  3. tradingOperation узгоджується, якщо виконання методу представляє будь-який публічний метод у торговому модулі.

Найкращим способом є побудова складніших виразів зрізу з дрібніших іменованих компонентів, як було показано раніше. При зверненні до зрізів на ім'я застосовуються звичайні правила видимості Java (ти можеш бачити приватні зрізи в тому ж типі, захищені зрізи в ієрархії, публічні зрізи в будь-якому місце тощо). Видимість не впливає на узгодження зрізів.

Спільне використання загальних визначень зрізу

Під час роботи з корпоративними додатками розробникам часто необхідно посилатися на модулі додатка та певні набори операцій з декількох аспектів. Для цього рекомендується визначити аспект CommonPointcuts, який фіксує загальні вирази зрізу. Такий аспект зазвичай нагадує такий приклад:

Java

package com.xyz.myapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class CommonPointcuts {
    /**
    * Точка з'єднання знаходиться на рівні взаємодії з інтернетом, якщо визначено метод
    * у типі в пакеті com.xyz.myapp.web або будь-якому підпакеті
    * під цим.
    */
    @Pointcut("within(com.xyz.myapp.web..*)")
    public void inWebLayer() {} /**
    * Точка з'єднання знаходиться на рівні служб, якщо визначено метод
    * у типі в пакеті com.xyz .myapp.service або будь-якому підпакеті
    * під цим.
    */
    @Pointcut("within(com.xyz.myapp.service..*)")
    public void inServiceLayer() {}
    /**
    * Точка з'єднання знаходиться на рівні доступу до даних, якщо визначено метод
    * у типі з пакета com .xyz.myapp.dao або будь-якого підпакету * під цим.
    */
    @Pointcut("within(com.xyz.myapp.dao..*)") public void inDataAccessLayer() {}
    /**
    * Бізнес-служба – це виконання будь-якого методу, визначеного у службі.
    * Інтерфейс. Дане визначення передбачає, що інтерфейси розміщені в пакеті "service", і що типи реалізації знаходяться в підпакетах.
    *
    * Якщо ви групуєте інтерфейси служб за функціональними областями (наприклад,
    * у пакетах com.xyz.myapp.abc.service та com.xyz.myapp.def.service) тоді
    * вираз зрізу "execution(* com.xyz.myapp). .service.*.*(...))".
    * можна використовувати натомість.
    *
    * Як варіант, можна записати вираз, використовуючи "бін-орієнтований"
    * PCD, наприклад, "bean(*Service)". (Припускається, що ви
    * назвали свої службові біни Spring послідовно).
    */
    @Pointcut("execution(* com.xyz.myapp..service.*.*(..))") public void businessService() {}
    /**
    * Операція доступу до даних – це виконання будь-якого методу, визначеного для
    * інтерфейсу Dao. Це визначення передбачає, що інтерфейси розміщені в
    * пакеті "dao", і що типи реалізації перебувають у підпакетах.
    */
    @Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
    public void dataAccessOperation() {}
}
Kotlin

package com.xyz.myapp
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Pointcut
@Aspect
class CommonPointcuts {
    /**
    * Точка з'єднання знаходиться на рівні взаємодії з інтернетом, якщо визначено метод
    * у типі в пакеті com.xyz.myapp.web або будь-якому підпакет
    * під цим.
    */
    @Pointcut("within(com.xyz.myapp.web..*)")
    fun inWebLayer() {
    }
    /**
    * Точка з'єднання знаходиться на рівні служб, якщо визначено метод
    * у типі в пакеті com.xyz. myapp.service або будь-якому підпакеті
    * під цим.
    */
    @Pointcut("within(com.xyz.myapp.service..*)")
    fun inServiceLayer() {
    }
    /**
    * Точка з'єднання знаходиться на рівні доступу до даних, якщо визначено метод
    * у типі з пакета com. xyz.myapp.dao або будь-якого підпакету
    * під цим.
    */
    @Pointcut("within(com.xyz.myapp.dao..*)")
    fun inDataAccessLayer() {
    }
    /**
    * Бізнес-служба – це виконання будь-якого методу, визначеного у службі.
    * Інтерфейс. Дане визначення передбачає, що інтерфейси розміщені в пакеті "service", і що типи реалізації знаходяться в підпакетах.
    *
    * Якщо ви групуєте інтерфейси служб за функціональними областями (наприклад,
    * у пакетах com.xyz.myapp.abc.service та com.xyz.myapp.def.service) тоді
    * вираз зрізу "execution(* com.xyz.myapp). .service.*.*(...))".
    * можна використовувати натомість.
    *
    * Як варіант, можна записати вираз, використовуючи "бін-орієнтований"
    * PCD, наприклад, "bean(*Service)". (Припускається, що ви * назвали свої службові біни Spring послідовно).
    */
    @Pointcut("execution(* com.xyz.myapp..service.*.*(..))") fun businessService() { }
    /**
    * Операція доступу до даних – це виконання будь-якого методу, визначеного для
    * інтерфейсу dao. Це визначення передбачає, що інтерфейси розміщені в
    * пакеті "dao", і що типи реалізації перебувають у підпакетах.
    */
    @Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
    fun dataAccessOperation() {
    }
}

Ти можеш посилатися на зрізи, визначені в такому аспекті, скрізь, де необхідно використовувати вираз зрізу. Наприклад, щоб зробити рівень служб транзакційним, можна написати таке:


<aop:config>
    <aop:advisor
            pointcut="com.xyz.myapp.CommonPointcuts.businessService()"
            advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

Приклади

Користувачі Spring AOP найчастіше використовують покажчик зрізів execution. Формат виразу з вказівником execution наступний:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
                throws-pattern?)
            

Всі частини, крім шаблону типу, що повертається(ret -type-pattern у попередньому фрагменті), шаблон імені та шаблон параметрів, є необов'язковими. Шаблон типу, що повертається, визначає, яким повинен бути тип методу, що повертається, щоб точка з'єднання збігалася. Як шаблон типу, що повертається, найчастіше використовується знак *. Він відповідає будь-якому типу повернення. Повне ім'я типу збігається лише у разі, якщо метод повертає даний тип. Шаблон імені збігається з іменем методу. Підстановковий знак * можна використовувати у шаблоні імені як повністю, так і частково. Якщо ти вказуєш шаблон типу, що оголошує, увімкни завершальний знак ., щоб приєднати його до компонента шаблону імені. Шаблон параметрів трохи складніший: () відповідає методу, який не приймає жодних параметрів, тоді як (..) відповідає будь-якій кількості (нуль або більше) параметрів. Шаблон (*) відповідає методу, який приймає один параметр будь-якого типу. (*,String) відповідає методу, який приймає два параметри. Перший може бути будь-якого типу, а другий має бути String.

У наступних прикладах показані деякі поширені вирази зрізу:

  • Виконання будь-якого публічного методу:

     execution(public * *(..))
  • Виконання будь-якого методу, ім'я якого починається з set:

    execution(* set*(..))
  • Виконання будь-якого методу, визначеного інтерфейсом AccountService:

     execution(*
                    com.xyz.service.AccountService.*(..))
  • Виконання будь-якого методу, визначеного в пакеті service:

     execution( * com.xyz.service.*.*(..))
  • Виконання будь-якого методу, визначеного в пакеті служб або одному з його підпакетів:

     execution(* com.xyz.service..*.* (..))
  • Будь-яка точка з'єднання (виконання методу тільки в Spring AOP) в рамках пакету служб:

     within(com.xyz.service.*)
  • Будь-яка точка з'єднання (виконання методу тільки в Spring AOP) в межах пакету служб або одного з його підпакетів:

     within(com.xyz.service..*)
  • Будь-яка точка з'єднання (виконання методу тільки в Spring AOP), де проксі реалізує інтерфейс AccountService:

    this(com.xyz.service.AccountService)
    this найчастіше використовується у формі, що прив'язується.
  • Будь-яка точка з'єднання (виконання методу тільки в Spring AOP), де цільовий об'єкт реалізує інтерфейс AccountService:

     target(com.xyz.service.AccountService)
    target найчастіше використовується у формі, що прив'язується.
  • Будь-яка точка з'єднання (виконання методу тільки в Spring AOP), яка приймає один параметр і де аргумент передається під час виконання, є Serializable:

    args(java.io.Serializable)
    args найчастіше використовується у формі, що прив'язується.

    Зверни увагу, що наведений у цьому прикладі зріз відрізняється від execution(* * *(java.io.Serializable)). Варіант "args" узгоджується, якщо аргумент, що передається під час виконання, є Serializable, а варіант "execution" узгоджується, якщо в сигнатурі методу оголошено єдиний параметр типу Serializable.

  • Будь-яка точка з'єднання (виконання методу тільки в Spring AOP), де цільовий об'єкт має анотацію @Transactional:

    @target(org.springframework.transaction.annotation.Transactional)
    Також можна використовувати @target у формі, що прив'язується.
  • Будь-яка точка з'єднання (виконання методу тільки в Spring AOP), де оголошений тип цільового об'єкта має анотацію @Transactional:

    @within(org.springframework.transaction.annotation.Transactional)
    Також можна використовувати @within у формі, що прив'язується.
  • Будь-яка точка з'єднання (виконання методу тільки в Spring AOP), де виконуючий метод має анотацію @Transactional:

    @annotation(org.springframework.transaction.annotation.Transactional)
    Також можна використовувати @annotation у прив'язуваній формі.
  • Будь-яка точка з'єднання (виконання методу тільки в Spring AOP), яка приймає один параметр, і де тип аргументу, що передається, має анотацію @Classified:

     @args(com.xyz.security.Classified)
    Також можна використовувати @args у формі, що прив'язується.
  • Будь-яка точка з'єднання (виконання методу тільки в Spring AOP) для бінів Spring з ім'ям tradeService:

     bean(tradeService)
  • Будь-яка точка з'єднання (виконання методу тільки в Spring AOP) для бінів Spring, імена яких відповідають виразу з знаками підстановки *Service:

     bean(*Service)

Написання хороших зрізів

Під час компіляціъ AspectJ обробляє зрізи, щоб оптимізувати ефективність узгодження. Вивчення коду та визначення відповідності кожної точки з'єднання (статично чи динамічно) зазначеному зрізу — це ресурсомісткий процес. (Динамічне зіставлення означає, що відповідність не можна повністю визначити за допомогою статичного аналізу, і що до коду вміщується тест для визначення фактичної відповідності при виконанні коду). При першому виявленні оголошення зрізу AspectJ переписує їх у формі, оптимальній для процедури зіставлення. Що це означає? По суті зрізи переписуються в ДНФ (диз'юнктивна нормальна форма), а компоненти зрізу сортуються таким чином, що спочатку перевіряються ті компоненти, які можна обчислити з меншими витратами ресурсів. Це означає, що не потрібно турбуватися про опис роботи різних покажчиків зрізів і можна використовувати їх у будь-якому порядку в оголошенні зрізів.

Проте AspectJ може виконати тільки те, що йому буде встановлено. Для оптимальної ефективності зіставлення слід подумати про кінцеву мету і максимально звузити простір пошуку відповідностей у визначенні. Існуючі покажчики, природно, належать до однієї з трьох груп: родові, означальні та контекстуальні:

  • Родові покажчики відбирають конкретний вид точки з'єднання: execution, get, set, call або handler.

  • Визначальні покажчики відбирають вибирають потрібну групу точок з'єднання (можливо, з багатьох родів): within і withincode

  • Контекстуальні покажчики зіставляються (і за бажанням прив'язуються) на основі контексту: this, target та @annotation

Добре написаний зріз повинен включати принаймні перші два типи покажчиків (родові та означальні). Ти можеш включати контекстуальні покажчики для зіставлення на основі контексту точки з'єднання або прив'язувати контекст для використання в пораді. Вказівка тільки родового або контекстуального покажчика спрацює, але може вплинути на складність виконання зв'язування (час і використовуваний обсяг пам'яті) через додаткову обробку та аналіз. Визначальні покажчики вкрай швидко зіставляються, а їх використання означає, що AspectJ може дуже швидко відсіяти групи точок з'єднання, які слід обробляти далі. Хороший зріз завжди повинен включати один такий, якщо це можливо.

Оголошення порад

Порада пов'язана з вираженням зрізу і виконується перед, після або замість виконання методу, що збігається зі зрізом. Вираз зрізу може бути або простим посиланням на іменований зріз, або безпосереднім виразом зрізів.

Порада Before

Можна оголосити пораду "перед" в аспекті за допомогою анотації @Before:

Java

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
    @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }
}
Kotlin

import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
@Aspect
class BeforeExample {
    @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    fun doAccessCheck() {
        // ...
    }
}

Якщо використовується безпосередній вираз зрізу, то можна переписати попередній приклад так:

Java

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
    @Before("execution(* com.xyz.myapp.dao.*.*(..))")
    public void doAccessCheck() {
        // ...
    }
}
Kotlin

import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
@Aspect
class BeforeExample {
    @Before("execution(* com.xyz.myapp.dao.*.*(..))")
    fun doAccessCheck() {
        // ...
    }
}

Порада After Returning

Рада "після повернення (after returning)" виконується, коли виконання збіглого методу повертається в нормальному порядку. Оголосити його можна за допомогою анотації @AfterReturning:

Java

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
    @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
    // ...
    }
}
Kotlin

import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterReturning
@Aspect
class AfterReturningExample {
    @AfterReturning("com.xyz. myapp.CommonPointcuts.dataAccessOperation()")
    fun doAccessCheck() {
    // ...
    }
}

Можна мати кілька оголошень порад (та інших членів), все всередині одного аспекту. У цих прикладах показане лише одне оголошення поради, щоб звернути увагу на вплив кожної з них. Можна використовувати форму @AfterReturning, яка прив'язує значення, що повертається, для надання такого доступу, як це показано в наступному прикладі:

Java

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
    @AfterReturning(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }
} 
Kotlin

import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterReturning
@Aspect
class AfterReturningExample {
    @AfterReturning(
        pointcut = "com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        returning = "retVal")
    fun doAccessCheck(retVal: Any) {
        // ...
    }
}

Ім'я, яке використовується в returning атрибуті, має відповідати імені параметра в методі поради. Якщо виконання методу повертається, значення, що повертається, передається методу поради як відповідне значення аргументу. Вираз returning також обмежує зіставлення лише тими виконаннями методу, які повертають значення зазначеного типу (в даному випадку Object, що відповідає будь-якому значення, що повертається).

Зверни увагу, що при використанні ради "після повернення" повернути зовсім інше посилання неможливо.

Порада After Throwing

Порада "після генерації винятку (after throwing)" виконується, коли виконання відповідного методу завершується генерацією винятку. Його можна оголосити за допомогою анотації @AfterThrowing, як показано в наступному прикладі:

Java

import org.aspectj.lang.annotation. Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
    @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doRecoveryActions() {
    // ...
    }
}
Kotlin

import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterThrowing
@Aspect
class AfterThrowingExample {
    @AfterThrowing("com.xyz. myapp.CommonPointcuts.dataAccessOperation()")
    fun doRecoveryActions() {
        // ...
    }
}

Часто потрібно, щоб порада запускалася тільки при генерації винятків певного типу, але також часто потрібний доступ до винятку в тілі поради. Атрибут throw можна використовувати як для обмеження порівняльного пошуку (за бажанням — інакше використовуй Throwable як тип виключення), так і для прив'язки згенерованого виключення до параметра поради. У цьому прикладі показано, як це зробити:

Java

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspublic
class AfterThrowingExample {
    @AfterThrowing(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }
} 
Kotlin

import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterThrowing
@Aspect
class AfterThrowingExample {
    @AfterThrowing(
        pointcut = "com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        throwing = "ex")
    fun doRecoveryActions(ex: DataAccessException) {
        // ...
    }
}

Ім'я, яке використовується в атрибуті throw, має відповідати імені параметра в методі поради. Якщо виконання методу завершується генерацією винятку, він передається методу поради як відповідне значення аргументу. Вираз throw також обмежує порівняльний пошук лише тими виконаннями методу, які генерують виняток зазначеного типу (в цьому випадку DataAccessException).

Зверни увагу, що @AfterThrowing не вказує на загальний зворотній виклик обробки винятків. Зокрема, метод поради, анотований @AfterThrowing, повинен отримувати винятки лише від самої точки з'єднання (оголошеного користувачем цільового методу), але не від супутнього методу @After/@AfterReturning.

Порада After (Finally)

Рада "після (остаточно) (After (finally))" виконується, коли відбувається завершення виконання відповідного методу. Вона оголошується за допомогою анотації @After. Пораду "після" необхідно підготувати до роботи як зі звичайними, так і з винятковими умовами повернення. Зазвичай вона використовується для вивільнення ресурсів та подібних цілей. У наступному прикладі показано, як використовувати пораду "після (завершення)":

Java

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
    @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }
}
Kotlin

import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.After
@Aspect
class AfterFinallyExample {
    @After("com.xyz. myapp.CommonPointcuts.dataAccessOperation()")
    fun doReleaseLock() {
        // ...
    }
}

Зверни увагу, що порада, позначена анотацією @After, в AspectJ визначається як "порада після (завершення)", аналогічно блоку "finally" в інструкції обробки виключення (try-catch statement)). Вона буде викликана для будь-якого результату, нормального повернення або винятку, згенерованого з точки з'єднання (оголошеного користувачем цільового методу), на відміну від анотації @AfterReturning, яка застосовується лише до успішних нормальних повернень.

Порада Around

Останній вид порад — це порада "замість". Порада "замість" замінює виконання зіставленого методу. Вона може виконуватися як до, так і після запуску методу і визначати, коли, як і навіть чи буде метод взагалі запущений. Порада "замість" часто використовується, якщо потрібно розділити стан "перед" і "після" виконання методу потокобезпечним чином — наприклад, як для запуску та зупинки таймера.

Завжди використовуй найменш впливову форму поради, яка відповідає твоїм вимогам.

Наприклад, не використовуй пораду "замість", якщо для твоїх потреб достатньо поради "перед".

Порада "замість" оголошується шляхом анотування методу за допомогою анотації @Around. Метод повинен оголосити Object як тип, що повертається, а перший параметр методу повинен бути типу ProceedingJoinPoint. У тілі методу поради потрібно викликати proceed() для ProceedingJoinPoint, щоб запустити основний метод. Виклик proceed() без аргументів призведе до того, що вихідні аргументи коду, що робить виклить, будуть передані базовому методу при його викликі. Для просунутих випадків використання існує перевантажений варіант методу proceed(), який приймає масив аргументів(Object[]). Значення в масиві будуть використані як аргументи базового методу під час його виклику.

Логіка роботи Proceed під час виклику за допомогою Object[] трохи відрізняється від логіки роботи proceed для поради "замість", скомпілюваної компілятором на AspectJ. Для поради "замість", написаної з використанням традиційної мови AspectJ, кількість аргументів, що передаються для proceed, повинна відповідати кількості аргументів, переданих пораді "замість" (а не кількості аргументів, що приймаються базовою точкою з'єднання), а значення, передане для продовження роботи в даній позиції аргументу, витісняє вихідне значення в точці з'єднання для сутності, до якої це значення було прив'язане (не переживай, якщо зараз це здається нісенітницею).

Підхід, що використовується Spring, більше простий і краще відповідає його заснованій на проксі та призначеній лише для виконання семантики. Тобі потрібно розуміти цю різницю, тільки якщо ти компілюєш аспекти, анотовані @AspectJ, написані для Spring, і використовуєш метод proceed з аргументами, з компілятором та інструментом зв'язування AspectJ. Існує спосіб написання таких аспектів, який на 100% сумісний як зі Spring AOP, так і з AspectJ.

Значення, що повертається порадою "замість", – це значення, що повертається, яке бачить код методу, що викликається. Наприклад, простий аспект кешування може повертати значення з кеша, якщо воно є, або викликати proceed() (і повертати це значення), якщо його немає. Зверни увагу, що proceed можна викликати один раз, безліч разів або взагалі не викликати в тілі поради "замість". Все це не заборонено.

Якщо ти оголосиш тип повернення методу поради "замість" як void, то коду, що викликає, завжди буде повертатися null, фактично ігноруючи результат будь-якого виклику proceed(). Тому рекомендується, щоб метод поради "замість" оголошував тип Object, що повертається. Метод поради зазвичай повинен повертати значення, що повертається під час виклику proceed(), навіть якщо базовий метод має тип повернення void. Однак опційно порада може повертати кешоване значення, обернене значення або будь-яке інше значення залежно від випадку використання.

У наступному прикладі показано, як використовувати пораду "замість":

Java

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // запускаємо секундомір
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }
}
Kotlin

import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.ProceedingJoinPoint
@Aspect
class AroundExample {
    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    fun doBasicProfiling(pjp: ProceedingJoinPoint): Any {
        // зупиняємо секундомір
        val retVal = pjp.proceed()
        // stop the stopwatch
        return retVal
}

Параметри порад

Spring пропонує повністю типізовані поради, що означає, що ти повідомляєш необхідні параметри в сигнатурі ради (як можна було побачити раніше в прикладах з поверненням і генеруванням виключення), а не постійно працюєш із масивами Object[]. Далі в цьому розділі ми розглянемо, як зробити аргументи та інші контекстні значення доступними для тіла поради. Спочатку ми розглянемо, як написати узагальнену пораду, здатну розпізнавати метод, який на даний момент забезпечується порадою.

Доступ до поточної JoinPoint

Для будь-якого методу поради можна оголосити за його перший параметр параметр типу org.aspectj.lang.JoinPoint. Зверни увагу, що для поради "замість" потрібно оголосити перший параметр типу ProceedingJoinPoint, який є підкласом JoinPoint.

Інтерфейс JoinPoint надає ряд корисних методів:

  • getArgs(): Повертає аргументи методу.

  • getThis(): Повертає проксі-об'єкт.

  • getTarget(): Повертає цільовий об'єкт.

  • getSignature(): Повертає опис методу, що забезпечується порадою.

  • toString(): Виводить практично використовуваний опис методу, що забезпечується порадою.

Передача параметрів до поради

Ми вже бачили, як прив'язувати значення, що повертається, або значення винятку (використовуючи поради "після повернення" та "після генерації виключення"). Щоб зробити аргументи доступними для тіла ради, можна використовувати форму прив'язування args. Якщо ти використовуєш ім'я параметра замість імені типу у args, значення відповідного аргументу передається як значення параметра під час виклику поради. На прикладі все буде зрозумілішим. Припустимо, тобі потрібно забезпечити порадою виконання операцій DAO, які приймають об'єкт Account як перший параметр, і тобі потрібен доступ до облікового запису в тілі рекомендації. Можна написати таке:

Java

@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..) ")
public void validateAccount(Account account) {
    // ...
}
Kotlin

@Before( "com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
fun validateAccount(account: Account) {
    // ...
}

Частина виразу зрізу args(account,..) служить двом цілям. По-перше, вона обмежує порівняльний пошук тільки тими виконаннями методу, за яких метод приймає хоча б один параметр, а аргумент, переданий як параметр, є екземпляром Account. По-друге, вона відкриває доступ пораді до фактичного об'єкта Account через параметр account.

Інший спосіб написання — оголосити зріз, який "надає" значення об'єкту Account, якщо воно відповідає точці з'єднання, а потім звернутися до названого зрізу з поради. Це буде виглядати так:

Java

@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,.. )")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}
Kotlin

@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private fun accountDataAccessOperation(account: Account) {
}
@Before("accountDataAccessOperation(account)")
fun validateAccount(account: Account) {
    // ...
}

Детальнішу інформацію див. у посібнику з програмування на AspectJ.

Проксі-об'єкт (this), цільовий об'єкт (target) та анотації(@ within, @target, @annotation та @args) можуть бути прив'язані аналогічним чином. Наступні два приклади показують, як зіставити виконання методів, анотованих анотацією @Auditable, та отримати код аудиту:

У першому з двох прикладів показано визначення анотації @Auditable:

Java

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}
Kotlin

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class Auditable(val value: AuditCode)

У другому з двох прикладів показана порада, що збігається з виконанням методів, позначених анотацією @Auditable:

Java

@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
    AuditCode code = auditable.value();
    // ...
}
Kotlin

@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
fun audit(auditable: Auditable) {
    val code = auditable.value()
    // ...
}

Параметри порад та узагальнення

Spring AOP може обробляти узагальнення, що використовуються в оголошеннях класів та параметрах методів. Припустимо, у тебе є загальний тип, подібний до наступного:

Java

public interface Sample<T> {
    void sampleGenericMethod(T param);
    void sampleGenericCollectionMethod(Collection<T> param);
}
Kotlin

interface Sample<T> {
    fun sampleGenericMethod(param: T)
    fun sampleGenericCollectionMethod(param: Collection<T>)
}

Можна обмежити перехоплення типів методів певними типами параметрів, прив'язавши параметр поради до типу параметра, для якого ти хочеш перехопити метод:

Java

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Реалізація поради
}
Kotlin

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
fun beforeSampleMethod(param: MyType) {
        // Реалізація ради
}

Цей підхід не працює для узагальнених колекцій. Таким чином, не вийде визначити зріз так:

Java

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args (param)")
public void beforeSampleMethod(Collection<MyType> param) {
        // Реалізація поради
}
Kotlin

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
fun beforeSampleMethod(param: Collection<MyType>) {
        // Реалізація поради
}

Щоб це спрацювало, довелося б перевіряти кожен елемент колекції, що є недоцільним, оскільки ми також не можемо вирішити, як поводитися з null значенням в цілому. Щоб досягти чогось подібного, необхідно привести параметр до виду Collection<?> і вручну перевіряти тип елементів.

Визначення імен аргументів

Прив'язка параметрів у викликах порад заснована на зіставленні імен, що використовуються у виразах зрізу, з оголошеними іменами параметрів у порадах та сигнатурах методів зрізів. Імена параметрів недоступні через рефлексію Java, тому Spring AOP використовує таку стратегію визначення імен параметрів:

  • Якщо імена параметрів були явно вказані користувачем, то використовуються задані імена параметрів. Анотації порад і зрізів мають необов'язковий атрибут argNames, який можна використовувати для встановлення імен аргументів методу, що анотується. Ці аргументи доступні під час виконання. У цьому прикладі показано, як використовувати атрибут argNames:

Java

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
        AuditCode code = auditable.value();
        //... використовуємо код, бін і точку з'єднання
}
Kotlin

@Before(value = "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames = "bean,auditable")
fun audit(bean: Any, auditable: Auditable) {
    val code = auditable.value()
     //... використовуємо код, бін і точку з'єднання
}

Якщо перший параметр має тип JoinPoint, ProceedingJoinPoint або JoinPoint.StaticPart, то можна не встановлювати ім'я параметра у значенні атрибуту argNames. Наприклад, якщо ти зміниш попередню пораду, щоб отримати об'єкт точки з'єднання, атрибут argNames не повинен містити його:

Java

@Before( value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean ,
    Auditable auditable) { AuditCode code = auditable.value();
        // ... використовуємо код, бін і точку з'єднання
}
Kotlin

@Before(value = "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames = "bean,auditable")
fun audit(jp: JoinPoint, bean: Any, auditable : Auditable) {
    val code = auditable.value()
    // ... використовуємо код, бін і точку з'єднання
}

Особлива обробка першого параметра типів JoinPoint, ProceedingJoinPoint та JoinPoint.StaticPart особливо зручна для екземплярів порад, які не здійснюють збирання жодного іншого контексту точки з'єднання. У таких ситуаціях атрибут argNames можна опустити. Наприклад, у наступній пораді не потрібно оголошувати атрибут argNames:

Java

@Before("com.xyz.lib.Pointcuts.anyPublicMethod ()")
public void audit(JoinPoint jp) {
    // ... використовуємо точку з'єднання
}
Kotlin

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
fun audit(jp: JoinPoint) {
    // ... використовуємо точку з'єднання
}
  • Використовувати атрибут argNames дещо незручно, тому якщо атрибут argNames не був заданий, Spring AOP переглядає налагоджувальну інформацію для класу і намагається визначати імена параметрів із таблиці локальних змінних. Ця інформація буде присутня до тих пір, поки класи не будуть скомпільовані з використанням налагоджувальної інформації (як мінімум -g:vars). Наслідки компіляції із цим прапором такі: (1) код стає трохи легше зрозуміти (реверс-інжиніринг); (2) розмір файлів класів стає трохи більшим (зазвичай несуттєво); (3) оптимізація для видалення локальних змінних, що не використовуються, не застосовується вашим компілятором. Іншими словами, тобі не загрожують труднощі, якщо ти будеш здійснювати побудову з цим прапором. Якщо аспект @AspectJ був скомпілюваний компілятором AspectJ(ajc ) навіть без налагоджувальної інформації, немає необхідності додавати атрибут argNames, оскільки компілятор збереже необхідну інформацію.

  • Якщо код був скомпілюваний без необхідної налагоджувальної інформації, Spring AOP намагатиметься простежити пари змінних, що прив'язуються до параметрів (наприклад, якщо у вираженні зрізу тільки одна змінна має прив'язку, а метод ради приймає тільки один параметр, то парний зв'язок очевидний). Якщо прив'язка змінних є неоднозначною з урахуванням наявної інформації, буде згенеровано виняток AmbiguousBindingException.

  • Якщо всі перераховані вище стратегії не спрацюють, буде згенеровано виняток IllegalArgumentException .

Продовження виконання методу з використанням аргументів

Раніше ми зазначали, що розповімо, як написати виклик методу proceed з використанням аргументів, який буде працювати послідовно в Spring AOP та AspectJ. Рішення полягає в тому, що сигнатура поради повинна пов'язувати всі параметри методу по порядку. У цьому прикладі показано, як це зробити:

Java

@Around("execution(List<Account> find*(..)) && " +
        "com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
        String accountHolderNamePattern) throws Throwable {
    String newPattern = preProcess(accountHolderNamePattern);
    return pjp.proceed(new Object[] {newPattern});
}
Kotlin

@Around("execution(List<Account> find*(..)) && " +
        "com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")
fun preProcessQueryPattern(pjp: ProceedingJoinPoint,
                        accountHolderNamePattern: String): Any {
    val newPattern = preProcess(accountHolderNamePattern)
    return pjp.proceed(arrayOf<Any>(newPattern))
}

У багатьох випадках тобі все одно доводиться здійснювати цю прив'язку (як у попередньому прикладі) .

Упорядкування порад

Що відбувається, якщо кілька порад виконуватимуться в одній точці з'єднання? Spring AOP слідує тим самим правилам старшинства, як і AspectJ визначення порядку виконання порад. Порада з найвищим старшинством (пріоритетом) виконується першою "на вході" (таким чином, якщо дані дві поради "перед", то першою виконується та, яка має найвищий рівень старшинства). "На виході" з точки з'єднання порада з найвищим рівнем старшинства виконується останньою (тому, якщо дані дві поради "після", порада з найвищим рівнем старшинства виконуватиметься другою).

Якщо дві поради, визначені в різних аспекти, що повинні виконуватися в одній і тій же точці з'єднання, якщо не зазначено інше, порядок виконання не визначено. Ти можеш контролювати порядок виконання, поставивши старшинство. Це робиться звичайним для Spring способом: або шляхом реалізації інтерфейсу org.springframework.core.Ordered у класі аспекту, або шляхом анотування поради анотацією @Order. За наявності двох аспектів, аспект, що повертає менше значення з Ordered.getOrder() (або значення інструкції), має найвищий рівень старшинства.

Кожна з окремих типів порад конкретного аспекту концептуально призначена для безпосереднього застосування до точки з'єднання. Як наслідок не передбачається, що метод поради, поміченої анотацією @AfterThrowing, отримуватиме виключення від супутнього методу, анотованого @After/@AfterReturning.

Починаючи з версії Spring Framework 5.2.7, методи ради, визначені в одному класі, позначеному анотацією @Aspect, які повинні виконуватися в одній і тій же точці з'єднання, призначаються відповідно до рівня старшинства на основі їх типу поради в наступному порядку, починаючи від найвищого до найнижчого рівня старшинства: @Around, @Before, @After, @AfterReturning, @AfterThrowing. Зверни увагу, однак, що метод поради, анотований @After, фактично буде викликаний після будь-яких методів ради, позначених анотаціями @AfterReturning або @AfterThrowing, у тому ж аспекті, наслідуючи семантику "поради після (завершення)" з AspectJ для анотації @After.

Якщо дві частини одного типу поради (наприклад, два методи поради з анотацією @After), визначені в одному класі, анотованому @Aspect, повинні виконуватися в одній і тій самій точці з'єднання, їх порядок не буде визначений (оскільки javac-компільовані класи не можуть отримувати порядок оголошення у вихідному коді через рефлексію). Розглянь можливість об'єднання таких методів ради в один метод поради для кожної точки з'єднання в кожному класі, позначеному анотацією @Aspect, або зроби рефакторинг порад в окремі класи, анотованих @Aspect, які можна впорядкувати на рівні аспекту за допомогою Ordered або @Order.

Запровадження

Запровадження (відомі в AspectJ (як міжтипові оголошення) дозволяють аспекту оголосити, що об'єкти, забезпечені порадою, реалізують встановлений інтерфейс, і надати реалізацію цього інтерфейсу від імені цих об'єктів.

Ви можете виконати запровадження за допомогою анотаціє @DeclareParents. Ця анотація використовується для оголошення того, що збігаються типів є новий батьківський тип (звідси і назва). Наприклад, якщо взяти інтерфейс UsageTracked та реалізацію цього інтерфейсу DefaultUsageTracked, то наступний аспект оголосить, що всі реалізатори службових інтерфейсів також реалізують інтерфейс UsageTracked (наприклад, для збору статистики через JMX):

Java

@Aspect
public class UsageTracking {
    @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
    public static UsageTracked mixin;
    @Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
    public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
    }
}
Kotlin

@Aspect
class UsageTracking {
    companion object {
        @DeclareParents(value = "com.xzy.myapp.service.*+", defaultImpl = DefaultUsageTracked::class)
        lateinit var mixin: UsageTracked
    }
    @Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
    fun recordUsage(usageTracked: UsageTracked) {
        usageTracked.incrementUseCount()
    }
}

Інтерфейс, який буде реалізований, визначається типом анотованого поля. Атрибут value анотації @DeclareParents є шаблоном типу AspectJ. Будь-який бін відповідного типу реалізує інтерфейс UsageTracked. Зверни увагу, що в пораді "перед" у попередньому приклад службові біни можуть бути безпосередньо використані як реалізацію інтерфейсу UsageTracked. При програмному доступі до біна потрібно написати наступне:

Java
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
Kotlin
val usageTracked = context.getBean("myService") as UsageTracked

Моделі створення екземпляра аспекту

Це складна тема. Якщо ти лише починаєш вивчати АОП, можеш сміливо пропустити цей розділ.

За замовчуванням у контексті програми існує єдиний екземпляр кожного аспекту. AspectJ називає це моделлю створення екземплярів-одинаків. Можна визначити аспекти з альтернативними циклами життя. Spring підтримує моделі створення екземплярів perthis та pertarget з AspectJ; percflow, percflowbelow та pertypewithin наразі не підтримуються.

Ти можеш оголосити аспект perthis, встановивши вираз perthis в анотації @Aspect. Розглянемо наступний приклад:

Java

@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
public class MyAspect {
    private int someState;
    @Before("com.xyz.myapp.CommonPointcuts.businessService()")
    public void recordServiceUsage() {
        // ...
    }
}
Kotlin

@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
class MyAspect {
    private val someState: Int = 0
    @Before("com.xyz.myapp.CommonPointcuts.businessService()")
    fun recordServiceUsage() {
        // ...
    }
}

У попередньому прикладі дія виразу perthis полягає в тому, що один екземпляр аспекту створюється для кожного унікального об'єкта-служби, що виконує бізнес-службу (кожний унікальний об'єкт, пов'язаний з this у точках з'єднання, що відповідають виразу зрізу). Екземпляр аспекту створюється за першого виклику методу для об'єкта-служби. Аспект виходить із області видимості, якщо об'єкт-служба теж виходить із області видимості. До створення екземпляра аспекту жодна з порад у ньому не виконується. Щойно екземпляр аспекту буде створено, оголошені у ньому поради почнуть виконуватися у зіставлених точках з'єднання, але якщо об'єкт-служба є тим, з яким пов'язаний даний аспект. Більш детальну інформацію про вирази per див. у посібнику з програмування на AspectJ.

Модель створення екземплярів pertarget працює так само, як і perthis, але вона створює один екземпляр аспекту для кожного унікального цільового об'єкта в точках з'єднання, що збігаються.

Приклад АОП

Тепер, коли ви побачили, як працюють всі складові частини, ми можемо зібрати їх разом, щоб зробити щось практично застосовне.

Іноді виконання бізнес-служб може завершитися помилкою через проблеми з паралелізмом (наприклад, через взаємоблокування). Якщо операцію повторити, то, швидше за все, вона буде успішною у наступній спробі. У випадку з бізнес-службами, з якими доречно повторювати операцію за таких умов (ідемпотентні операції, за яких не потрібно повертатися до користувача для вирішення конфлікту), ми хочемо доступним способом повторити таку операцію, щоб клієнтська частина не побачила PessimisticLockingFailureException. Ця вимога, яка очевидно охоплює кілька служб на рівні служб і, отже, ідеально підходить для реалізації через аспект.

Оскільки нам потрібно повторити операцію, необхідно використати пораду "замість", щоб можна було викликати метод proceed кілька разів. У наступному лістингу показано базову реалізацію аспекту:

Java

@Aspect
public class ConcurrentOperationExecutor implements Ordered {
    private static final int DEFAULT_MAX_RETRIES = 2;
    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;
    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }
    public int getOrder() {
        return this.order;
    }
    public void setOrder(int order) {
        this.order = order;
    }
    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }
}
Kotlin

@Aspect
class ConcurrentOperationExecutor : Ordered {
    private val DEFAULT_MAX_RETRIES = 2
    private var maxRetries = DEFAULT_MAX_RETRIES
    private var order = 1
    fun setMaxRetries(maxRetries: Int) {
        this.maxRetries = maxRetries
    }
    override fun getOrder(): Int {
        return this.order
    }
    fun setOrder(order: Int) {
        this.order = order
    }
    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any {
        var numAttempts = 0
        var lockFailureException: PessimisticLockingFailureException
        do {
            numAttempts++
            try {
                return pjp.proceed()
            } catch (ex: PessimisticLockingFailureException) {
                lockFailureException = ex
            }
        } while (numAttempts <= this.maxRetries)
        throw lockFailureException
    }
}

Зверни увагу, що аспект реалізує інтерфейс Ordered, щоб можна було зазначити рівень старшинства аспекту вище, ніж поради транзакції (нам потрібно, щоб при кожній повторній спробі транзакція була новою). Властивості maxRetries та order налаштовуються Spring. Основна дія відбувається у пораді "замість" doConcurrentOperation. Зверни увагу, що ми застосовуємо логіку повторних спроб до кожного businessService(). Намагаємося продовжити виконання, а у разі помилки PessimisticLockingFailureException намагаємося знову, якщо не були вичерпані всі спроби повторення.

Далі слідує відповідна конфігурація Spring:


<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
    <property name="maxRetries" value="3"/>
    <property name="order" value="100"/>
</bean>

Для уточнення аспекту, щоб він повторював лише ідемпотентні операції, ми можемо визначити наступну анотацію Idempotent:

Java

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}
Kotlin

@Retention(AnnotationRetention.RUNTIME)
annotation class Idempotent
    // marker annotation

Потім можна використовувати анотацію для анотування реалізації службових операцій. Зміна аспекту для повторного виконання лише ідемпотентних операцій включає в себе уточнення виразу зрізу таким чином, щоб збігалися лише @Idempotent операції, як показано нижче:

Java

@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    // ...
}
Kotlin

@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent) ")
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any {
    // ...
}