Жизненные циклы совета
Каждый совет – это бин 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
:
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;
}
}
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, который подсчитывает все вызовы методов:
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;
}
}
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
(в том числе из подклассов) вызывается следующий совет:
public class RemoteThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Сделайте что-нибудь с удаленным исключением
}
}
class RemoteThrowsAdvice : ThrowsAdvice {
fun afterThrowing(ex: RemoteException) {
// Сделайте что-нибудь с удаленным исключением
}
}
В отличие от предыдущего совета, в следующем примере объявлено четыре аргумента, поэтому совет получает доступ к вызываемому методу, аргументам метода и целевому объекту. При возникновении ServletException
вызывается следующий совет:
public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Сделайте что-нибудь со всеми аргументами
}
}
class ServletThrowsAdviceWithArguments : ThrowsAdvice {
fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
// Сделайте что-нибудь со всеми аргументами
}
}
Последний пример иллюстрирует, как эти два метода можно использовать в одном классе, который обрабатывает как RemoteException
, так и ServletException
. В одном классе можно объединить любое количество методов совета "генерация исключения". В следующем листинге показан последний пример:
public static class CombinedThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Сделайте что-нибудь с удаленным исключением
}
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Сделайте что-нибудь со всеми аргументами
}
}
class CombinedThrowsAdvice : ThrowsAdvice {
fun afterThrowing(ex: RemoteException) {
// Сделайте что-нибудь с удаленным исключением
}
fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
// Сделайте что-нибудь со всеми аргументами
}
}
Совет 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;
}
Совет "после возврата" имеет доступ к возвращаемому значению (которое он не может менять), вызываемому методу, аргументам метода и цели.
Следующий совет "после возврата" подсчитывает все успешные вызовы метода, которые не сгенерировали исключений:
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;
}
}
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 и предположим, что мы хотим внедрить следующий интерфейс в один или несколько объектов:
public interface Lockable {
void lock();
void unlock();
boolean locked();
}
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
:
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);
}
}
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
:
public class LockMixinAdvisor extends DefaultIntroductionAdvisor {
public LockMixinAdvisor() {
super(new LockMixin(), Lockable.class);
}
}
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 автоматически создает необходимую цепочку перехватчиков.