Життєві цикли ради

Кожна порада — це бін Spring. Екземпляр поради може бути загальним для всіх об'єктів, що мають пораду, або унікальним для кожного об'єкта, що має пораду. Це відповідає порадам для кожного класу або кожного примірника.

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

Поради для кожного екземпляра підходять для введень, для підтримки домішок (mixins). У цьому випадку порада додає стан в об'єкт, що проксується.

Ти можеш використовувати суміш загальних порад і порад для кожного екземпляра в одному проксі АОП.

Види порад у Spring

Spring надає кілька типів порад, а також можна розширити його функціонал для забезпечення підтримки довільних типів порад. У цьому розділі описані основні поняття та стандартні типи порад. Spring сумісний з інтерфейсом проект AOP Alliance для роботи з порадами, що використовують перехоплення виклику методів. Класи, що реалізують MethodInterceptor та реалізують пораду "перехоплення", повинні також реалізовувати такий інтерфейс:


public interface MethodInterceptor extends Interceptor {
    Object invoke(MethodInvocation invocation) throws Throwable;
}

Аргумент MethodInvocation методу invoke() відкриває викликаний метод, цільову точку з'єднання, проксі АОП та аргументи методу. Метод invoke() повинен повернути результат виклику: значення точки з'єднання, що повертається.

У наступному прикладі показаний зразок реалізації MethodInterceptor:

Java

public class DebugInterceptor implements MethodInterceptor {
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("Before: invocation=[" + invocation + "]");
        Object rval = invocation.proceed();
        System.out.println("Invocation returned");
        return rval;
    }
}
Kotlin

class DebugInterceptor : MethodInterceptor {
    override fun invoke(invocation: MethodInvocation): Any {
        println ("Before: invocation=[$invocation]")
        val rval = invocation.proceed()
        println("Invocation returned")
        return rval
    }
}

Зверни увагу на виклик методу proceed() з MethodInvocation. Він проходить ланцюжком перехоплювачів до точки з'єднання. Більшість перехоплювачів викликають цей метод і повертають його значення, що повертається. Однак MethodInterceptor, як і будь-яка інша порада, може повернути інше значення або згенерувати виняток замість того, щоб викликати метод продовження виконання. Але не варто робити це без вагомих причин.

Реалізації MethodInterceptor забезпечують сумісність з іншими реалізаціями AOP Alliance, що відповідають вимогам АОП. Інші типи порад, розглянуті в частині цього розділу, реалізують загальні концепції АОП, але специфічним для Spring способом. Хоча у використанні найбільш специфічного типу поради є перевага, дотримуйся поради MethodInterceptor, якщо тобі, можливо, буде необхідно виконати аспект в іншому АОП-фреймворку. Зверни увагу, що наразі зрізи функціонально несумісні між фреймворками, і AOP Alliance не може визначати інтерфейси зрізів.

Порада Before

Більш простим типом ради є порада "перед before)". Для неї не потрібен об'єкт MethodInvocation, оскільки він викликається лише перед входом до методу. Основна перевага поради "перед" полягає в тому, що немає необхідності викликати метод proceed( ) і, отже, немає можливості ненавмисно не продовжити ланцюжок перехоплювачів.

У наступному лістингу показаний інтерфейс MethodBeforeAdvice:


public interface MethodBeforeAdvice extends BeforeAdvice {
    void before(Method m, Object[] args, Object target) throws Throwable;
}

(Структура API Spring допускає наявність поля перед порадою, хоча до перехоплення поля застосовуються звичайні об'єкти, тому малоймовірно, що в Spring коли-небудь таке буде реалізовано).

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

В наступному прикладі показано пораду "перед" у Spring, яка рахує всі виклики методів:

Java

public class CountingBeforeAdvice implements MethodBeforeAdvice {
    private int count;
    public void before(Method m, Object[] args, Object target) throws Throwable {
        ++count;
    } public int getCount() {
        return count;
    }
}
Kotlin

class CountingBeforeAdvice : MethodBeforeAdvice {
    var count: Int = 0
    override fun before(m: Method, args: Array<Any>, target: Any?) {
        ++count
    }
}
Пораду "перед" можна використовувати з будь-яким зрізом.

Порада "генерація винятку"

Порада "генерація винятку (throws)" викликається після повернення точки з'єднання, якщо точка з'єднання згенерувала виняток. У Spring представлена типізована порада "генерація винятку". Зверни увагу, це означає, що інтерфейс org.springframework.aop.ThrowsAdvice не містить жодних методів. Це тег інтерфейсу, що ідентифікує, що цей об'єкт реалізує один або кілька типованих методів поради "генерація винятку". Вони повинні мати таку форму:

 afterThrowing([Method, args, target], subclassOfThrowable)

Обов'язковий лише останній аргумент. Сигнатури методів можуть мати один або чотири аргументи, залежно від того, чи зацікавлений метод поради в методі та аргументах. У наступних двох лістингах показані класи, які є прикладами порад "генерація винятку".

Якщо генерується виняток RemoteException (в тому числі з підкласів), викликається така порада:

Java

public class RemoteThrowsAdvice implements ThrowsAdvice {
    public void afterThrowing(RemoteException ex) throws Throwable {
        // Зробіть щось із видаленим винятком
    }
}
Kotlin

class RemoteThrowsAdvice : ThrowsAdvice {
    fun afterThrowing(ex: RemoteException) {
        // Зробіть щось із видаленим винятком
    }
} 

На відміну від попередньої поради, в наступному прикладі оголошено чотири аргументи, тому порада отримує доступ до методу, аргументів методу і цільового об'єкта. При виникненні ServletException викликається така порада:

Java

public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {
    public void afterThrowing(Method m, Object[] args , Object target, ServletException ex) {
        // Зробіть щось з усіма аргументами
    }
}
Kotlin

class ServletThrowsAdviceWithArguments : ThrowsAdvice {
    fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
        // Зробіть щось з усіма аргументами
    }

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

Java

public static class CombinedThrowsAdvice implements ThrowsAdvice {
    public void afterThrowing(RemoteException ex) throws Throwable {
        // Зробіть щось із видаленим винятком
    }
    public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
    // Зробіть що-небудь з усіма аргументами
    }
}
Kotlin

class CombinedThrowsAdvice : ThrowsAdvice {
    fun afterThrowing(ex: RemoteException) {
        // Зробіть щось з віддаленим винятком
    }
    fun afterThrowing(m: Method, args: Array<Any> , target: Any, ex: ServletException) {
        // Зробіть щось з усіма аргументами
    }
}
Якщо метод поради "генерація винятку" сам генерує виняток, він перевизначає вихідний виняток (тобто змінює виняток, згенерований для користувача). Виняток перевизначення зазвичай є винятком RuntimeException, що сумісний з будь-якою сигнатурою методу. Однак, якщо метод поради "генерація винятку" генерує виняток, що перевіряється, він повинен відповідати оголошеним виняткам цільового методу і, отже, певною мірою пов'язаний з конкретними сигнатурами цільового методу. Не генеруй неоголошений виняток, що перевіряється, який несумісний з сигнатурою цільового методу!
Пораду "генерація винятку" можна використовувати з будь-яким зрізом.

Порада Returning Advice

Порада "після повернення (after returning)" у Spring повинна реалізовувати інтерфейс org.springframework.aop.AfterReturningAdvice, що показано в наступному лістингу:


public interface AfterReturningAdvice extends Advice {
    void afterReturning(Object returnValue, Method m, Object[] args, Object target)
            throws Throwable;
}

Порада "після повернення" має доступ до значення, що повертається (яке він не може змінювати), викликаного методу, аргументів методу і мети.

Наступна порада "після повернення" підраховує всі успішні виклики методу, які не згенерували винятків:

Java

public class CountingAfterReturningAdvice implements AfterReturningAdvice {
    private int count;
    public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
            throws Throwable {
        ++count;
    }
    public int getCount() {
        return count;
    }
}
Kotlin

class CountingAfterReturningAdvice : AfterReturningAdvice {
    var count: Int = 0
        private set
    override fun afterReturning( returnValue: Any?, m: Method, args: Array<Any>, target: Any?) {
        ++count
    }
}

Ця порада не змінює шлях виконання. Якщо він генерує виняток, то він передається ланцюжком перехоплювачів замість значення, що повертається.

Пораду "після повернення" можна використовувати з будь-яким зрізом.

Порада Introduction

Spring працює з порадою "введення" як з особливим видом ради "перехоплення".

Для введення потрібні IntroductionAdvisor та IntroductionInterceptor, які реалізують наступний інтерфейс:


public interface IntroductionInterceptor extends MethodInterceptor {
    boolean implementsInterface(Class intf);
}

Метод invoke(), успадкований від інтерфейсу AOP Alliance MethodInterceptor, має реалізувати введення. Таким чином, якщо метод, що викликається, знаходиться у введеному інтерфейсі, перехоплювач введень відповідає за обробку виклику методу — він не може викликати proceed().

Порада "введення" не можна використовувати з будь-яким зрізом, оскільки він застосовується лише на рівні класу, а не методу. Ти можеш використовувати пораду "введення" лише за допомогою IntroductionAdvisor, яка має такі методи:


public interface IntroductionAdvisor extends Advisor, IntroductionInfo {
        ClassFilter getClassFilter();
        void validateInterfaces() throws IllegalArgumentException;
}
public interface IntroductionInfo {
        Class<?>[] getInterfaces();
}

Тут немає MethodMatcher і, отже, Pointcut, пов'язаного з порадою "введення". Логічною є тільки фільтрація класів.

Метод getInterfaces() повертає інтерфейси, представлені цим радником.

Метод validateInterfaces() використовується для перевірки того, чи можуть введені інтерфейси бути реалізовані налаштованим IntroductionInterceptor.

Розглянемо приклад із набору тестів Spring і припустимо, що ми хочемо впровадити наступний інтерфейс в один або декілька об'єктів:

Java

public interface Lockable {
    void lock();
    void unlock();
    boolean locked();
}
Kotlin

interface Lockable {
    fun lock()
    fun unlock()
    fun locked(): Boolean
}

Це приклад ілюструє домішку. Нам потрібно мати можливість приводити обладнані порадою об'єкти до Lockable, незалежно від їх типу, і викликати методи блокування та розблокування. Якщо ми викликаємо метод lock(), нам потрібно, щоб усі сетери генерували виняток LockedException. Таким чином, можна додати аспект, який надає можливість зробити об'єкти незмінними, і вони не знатимуть про це: хороший приклад АОП.

По-перше, нам потрібен перехоплювач IntroductionInterceptor, який виконуватиме всю важку роботу. У цьому випадку ми розширюємо допоміжний клас org.springframework.aop.support.DelegatingIntroductionInterceptor. Можна було б реалізувати IntroductionInterceptor безпосередньо, але використання DelegatingIntroductionInterceptor краще підходить для більшості випадків.

DelegatingIntroductionInterceptor призначений для делегування введення фактичної реалізації введених інтерфейсів, водночас приховуючи використання перехоплення. Ти можеш встановити делегата для будь-якого об'єкта за допомогою аргументу конструктора. Делегат за замовчуванням (коли використовується конструктор без аргументів) — це this. Так, у наступному прикладі делегатом є підклас LockMixin класу DelegatingIntroductionInterceptor. Отримавши делегата (за замовчуванням, самого себе), екземпляр DelegatingIntroductionInterceptor шукає всі інтерфейси, реалізовані делегатом (крім IntroductionInterceptor), та підтримує вступ для будь-якого з них. Підкласи, такі як LockMixin, можуть викликати метод suppressInterface(Class intf) для придушення інтерфейсів, які не повинні бути відкриті. Однак, незалежно від того, скільки інтерфейсів готовий підтримувати IntroductionInterceptor, використовуваний IntroductionAdvisor контролює, які інтерфейси дійсно будуть відкриті. Представлений інтерфейс приховує будь-яку реалізацію того ж інтерфейсу об'єктом.

Таким чином, LockMixin розширює DelegatingIntroductionInterceptor і реалізує сам Lockable. Суперклас автоматично визначає, що Lockable може підтримуватись для введення, тому вказувати це не потрібно. Таким чином, ми можемо ввести будь-яку кількість інтерфейсів.

Зверни увагу на використання змінної екземпляра locked. Це дозволяє ефективно додати додатковий стан до того, що зберігається в цільовому об'єкті.

У наступному прикладі показано приклад класу LockMixin:

Java

public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {
    private boolean locked;
    public void lock() {
        this.locked = true;
    }
    public void unlock() {
        this.locked = false;
    }
    public boolean locked() {
        return this.locked;
    }
    public Object invoke(MethodInvocation invocation) throws Throwable {
        if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
            throw new LockedException();
        }
        return super.invoke(invocation);
    }
}
Kotlin

class LockMixin : DelegatingIntroductionInterceptor(), Lockable {
    private var locked: Boolean = false
    fun lock() {
        this.locked = true
    }
    fun unlock() {
        this.locked = false
    }
    fun locked(): Boolean {
        return this.locked
    }
    override fun invoke(invocation: MethodInvocation): Any? {
        if (locked() && invocation.method.name.indexOf("set") == 0) {
            throw LockedException()
        }
        return super.invoke(invocation)
    }
}

Часто не доводиться перевизначати метод invoke(). Зазвичай вистачає реалізації DelegatingIntroductionInterceptor (яка викликає метод delegate, якщо цей метод представлений, а інакше переходить до точки з'єднання). В цьому випадку нам необхідно додати перевірку: сетер не може бути викликаний, якщо він перебуває в заблокованому режимі.

Потрібне введення повинне містити лише окремий екземпляр LockMixin та вказувати введені інтерфейсі (в цьому випадку тільки Lockable). У більш складному прикладі може бути посилання на перехоплювач введень (який буде визначений як прототип). У цьому випадку для LockMixin немає конфігурації, тому ми створюємо її за допомогою new. У цьому прикладі показано наш клас LockMixinAdvisor:

Java

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {
    public LockMixinAdvisor() {
        super(new LockMixin(), Lockable.class);
    }
}
Kotlin
class LockMixinAdvisor : DefaultIntroductionAdvisor(LockMixin(), Lockable::class.java)

Цей радник можна без перешкод застосовувати, оскільки він не вимагає жодного налаштування. (Але не можна використовувати IntroductionInterceptor без IntroductionAdvisor). Як завжди буває у випадку з введеннями, екземпляр радника має бути створений заздалегідь, оскільки він зберігає стан. Нам потрібен окремий екземпляр LockMixinAdvisor, і, отже, LockMixin, для кожного об'єкта, що має пораду. Радник включає частину стану забезпеченого порадою об'єкта.

Ми можемо застосувати цей радник програмно, використовуючи метод Advised.addAdvisor() або (рекомендований спосіб) у конфігурації XML, як і будь-який інший радник. Всі варіанти створення проксі, розглянуті нижче, включно з "творцями автопроксі", належно обробляють введення та домішки, що зберігають стан.

API-інтерфейс радника у Spring

У Spring радник (advisor) — це аспект, який містить лише один об'єкт поради, пов'язаний з виразом зрізу.

За винятком особливого випадку введення, будь-який радник може бути використаний з будь-якою порадою. org.springframework.aop.support.DefaultPointcutAdvisor — найчастіше використовуваний клас радника. Його можна використовувати з MethodInterceptor, BeforeAdvice або ThrowsAdvice.

У Spring можна змішувати види радників та порад в одному проксі АОП. Наприклад, можна використовувати пораду "перехоплення", пораду "генерація виключення" та пораду "перед" в одній конфігурації проксі. Spring автоматично створює необхідний ланцюжок перехоплювачів.