Жизненные циклы совета

Каждый совет – это бин Spring. Экземпляр совета может быть общим для всех снабженных советом объектов или уникальным для каждого снабженного советом объекта. Это соответствует советам для каждого класса или каждого экземпляра.

Чаще всего используются советы для каждого класса. Это подходит для общих советов, таких как транзакционные советники. Они не зависят от состояния проксируемого объекта и не добавляют новое состояние. Они просто работают в соответствии с методом и аргументами.

Советы для каждого экземпляра подходят для введений, для поддержки примесей (mixins). В этом случае совет добавляет состояние в проксируемый объект.

Вы можете использовать смесь общих советов и советов для каждого экземпляра в одном прокси АОП.

Виды советов в Spring

Spring предоставляет несколько типов советов, а также можно расширить его функционал для обеспечения поддержки произвольных типов советов. В этом разделе описаны основные понятия и стандартные типы советов.

Совет Interception Around

Наиболее фундаментальным типом совета в Spring является cовет "перехват (interception around)"

Spring совместим с интерфейсом проект AOP Alliance для работы с советами, использующими перехват вызова методов. Классы, реализующие MethodInterceptor и реализующие cовет "перехват", должны также реализовывать следующий интерфейс:

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, как и любой другой советник. Все варианты создания прокси, рассмотренные ниже, включая "создателей (creators) авто-прокси", надлежаще обрабатывают введения и примеси, сохраняющие состояния.

6.3. API-интерфейс советника в Spring

В Spring советник (advisor) – это аспект, который содержит только один объект совета, связанный с выражением среза.

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

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