Advice API in Spring
-
环绕通知(Around Advice): 环绕整个方法调用,允许定制输入和输出。
-
前置通知(Before Advice): 在方法调用之前执行,用于在调用之前执行自定义操作。
-
抛出通知(Throws Advice): 在方法抛出异常时执行,用于处理或记录异常。
-
返回后通知(After Returning Advice): 在方法顺利返回时执行,用于处理或记录返回值。
-
切入通知(Introduction Advice): 将新接口和行为引入目标对象,扩展其功能,而无需修改其源代码。
现在我们可以检查 Spring AOP 如何处理建议。
Advice Lifecycles
每条建议都是一个 Spring bean。建议实例可以在所有建议对象中共享,也可以是特定于每个建议对象的。这对应于按类或按实例的建议。
按类建议使用最频繁。它适用于通用的建议,比如事务顾问。这些不依赖代理对象的当前状态或不添加新状态。它们仅对方法和参数起作用。
按实例建议适用于导入,用于支持混入。在这种情况下,建议会向代理对象添加状态。
您可以在同一 AOP 代理中使用共享和按实例建议的混合。
Advice Types in Spring
Spring 提供了几种建议类型并可以通过扩展支持任意建议类型。本节对基本概念和标准建议类型进行了描述。
Interception Around Advice
Spring 中最基础的建议类型是在周围拦截的建议。
Spring 符合 Alliance
AOP 接口,用于使用方法拦截的环绕通知。实现 MethodInterceptor
并实现环绕通知的类也应实现以下接口:
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
invoke()
方法的 MethodInvocation
参数公开被调用的方法、目标连接点、AOP 代理和传递给该方法的参数。invoke()
方法应返回调用的结果:连接点的返回值。
以下示例展示了一个简单的 MethodInterceptor
实现:
-
Java
-
Kotlin
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
}
}
请注意对 MethodInvocation
的 proceed()
方法的调用。这将沿着拦截器链朝向连接点进行。大多数拦截器会调用此方法并返回其返回值。但是,MethodInterceptor
(如同任何环绕通知)可以返回一个不同的值或抛出一个异常,而不是调用 proceed
方法。但是,你不能毫无缘由地这样做。
|
Before Advice
一种更简单的通知类型是前置通知。这不需要 MethodInvocation
对象,因为它只在进入方法前调用。
前置通知的主要优势是不必调用 proceed()
方法,因此没有可能无意中未沿着拦截器链继续进行。
以下列表展示了 MethodBeforeAdvice
接口:
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method m, Object[] args, Object target) throws Throwable;
}
(Spring 的 API 设计允许字段前置通知,尽管通常的对象适用于字段拦截,但 Spring 永远不会实现它。)
请注意,返回类型为 void
。前置通知可以在连接点运行前插入自定义行为,但无法更改返回值。如果前置通知抛出一个异常,它将停止拦截器链的进一步执行。该异常会沿着拦截器链向上传播。如果它是未经检查异常或出现在被调用方法的签名中,它将直接传递给客户端。否则,它将被 AOP 代理包装在受检异常中。
以下示例展示了 Spring 中的前置通知,该通知计数所有方法调用:
-
Java
-
Kotlin
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 Advice
如果连接点抛出一个异常,则会在连接点返回后调用抛出通知。Spring 提供了类型化抛出通知。请注意,这意味着 org.springframework.aop.ThrowsAdvice
接口不包含任何方法。它是一个标记接口,标识给定对象实现了某个或多个类型化抛出通知方法。这些方法应具有以下形式:
afterThrowing([Method, args, target], subclassOfThrowable)
唯一必需的只是最后一个参数。根据通知方法是否关注于该方法和参数,方法签名可能含有一个或四个参数。以下两个列表展示了抛出通知的示例类。
如果抛出一个 RemoteException
(包括从其子类抛出),则将调用以下通知:
-
Java
-
Kotlin
public class RemoteThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
}
class RemoteThrowsAdvice : ThrowsAdvice {
fun afterThrowing(ex: RemoteException) {
// Do something with remote exception
}
}
与前面的通知不同,下一个示例声明了四个参数,以便它能够访问被调用方法、方法参数和目标对象。如果抛出一个 ServletException
,则将调用以下通知:
-
Java
-
Kotlin
public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}
class ServletThrowsAdviceWithArguments : ThrowsAdvice {
fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
// Do something with all arguments
}
}
最后一个示例说明了如何在单个类中使用这两个方法,该类能够同时处理 RemoteException
和 ServletException
。任何数量的抛出通知方法都可以组合在一个单个类中。以下列表展示了最终的示例:
-
Java
-
Kotlin
public static class CombinedThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}
class CombinedThrowsAdvice : ThrowsAdvice {
fun afterThrowing(ex: RemoteException) {
// Do something with remote exception
}
fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
// Do something with all arguments
}
}
如果 throws-advice 方法抛出自身的一个异常,那么它将覆盖原始的异常(也就是说,它将抛出的异常更改为用户)。覆盖的异常通常是 RuntimeException,它与任何方法签名兼容。然而,如果 throws-advice 方法抛出一个已检查的异常,那么它必须匹配目标方法的已声明异常,进而或多或少与特定的目标方法签名耦合。Do not throw an undeclared checked exception that is incompatible with the target method’s signature! |
抛出建议可用于任何接入点。 |
After Returning Advice
Spring 中的返回后通知必须实现以下列表所示的 org.springframework.aop.AfterReturningAdvice
接口:
public interface AfterReturningAdvice extends Advice {
void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable;
}
返回后通知能够访问返回值(它无法修改)、被调用方法、方法的参数和目标。
以下返回后通知计数未抛出异常的所有成功方法调用:
-
Java
-
Kotlin
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 Advice
Spring 对待切入建议为一种特殊类型的拦截建议。
切入需要一个 IntroductionAdvisor
和一个 IntroductionInterceptor
,它们实现以下接口:
public interface IntroductionInterceptor extends MethodInterceptor {
boolean implementsInterface(Class intf);
}
从 AOP Alliance MethodInterceptor
接口继承的 invoke()
方法必须实现切入。也就是说,如果调用的方法基于切入接口,那么切入拦截器负责处理方法调用——它无法调用 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
-
Kotlin
public interface Lockable {
void lock();
void unlock();
boolean locked();
}
interface Lockable {
fun lock()
fun unlock()
fun locked(): Boolean
}
这阐明了一种混合。我们希望能够将建议对象强制转换为 Lockable
(无论其类型是什么)并调用锁定和解锁方法。如果我们调用 lock()
方法,我们希望所有设置器方法都抛出一个 LockedException
。因此,我们可以添加一个方面,该方面提供使对象不可变的能力,而无需它们对此有任何了解:这是 AOP 的一个好例子。
首先,我们需要一个执行繁重工作的 IntroductionInterceptor
。在这种情况下,我们扩展 org.springframework.aop.support.DelegatingIntroductionInterceptor
快捷类。我们可以直接实现 IntroductionInterceptor
,但对大多数情况来说,使用 DelegatingIntroductionInterceptor
是最好的。
DelegatingIntroductionInterceptor
被设计为将一个切入委托给引入接口的实际实现,隐藏这样做的拦截使用。您可以使用构造函数参数将委托设置成任何对象。默认委托(当使用无参数构造函数时)为 this
。因此,在下一个示例中,委托为 DelegatingIntroductionInterceptor
的 LockMixin
子类。给定一个委托(默认情况下为它自己),一个 DelegatingIntroductionInterceptor
实例会查找由委托实现的所有接口(IntroductionInterceptor
除外),并支持针对其中任何一个的切入。比如 LockMixin
这样的子类可以调用 suppressInterface(Class intf)
方法来禁止不应该暴露的接口。但是,无论 IntroductionInterceptor
准备好支持多少个接口,所使用的 IntroductionAdvisor
会控制实际公开哪些接口。引入的接口会隐藏目标针对同一接口的任何实现。
因此,LockMixin
扩展了 DelegatingIntroductionInterceptor
并自身实现了 Lockable
。父类会自动选择 Lockable
可以用于切入,所以我们不必指定这一点。我们可以用这种方式引入任意数量的接口。
注意 locked
实例变量的使用。这有效地为目标对象中所持有的状态添加了附加状态。
以下示例展示了 LockMixin
类的示例:
-
Java
-
Kotlin
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
类:
-
Java
-
Kotlin
public class LockMixinAdvisor extends DefaultIntroductionAdvisor {
public LockMixinAdvisor() {
super(new LockMixin(), Lockable.class);
}
}
class LockMixinAdvisor : DefaultIntroductionAdvisor(LockMixin(), Lockable::class.java)
我们可以非常简单地应用此建议,因为它无需配置。(但是,如果没有 IntroductionAdvisor
就无法使用 IntroductionInterceptor
。)与切入照常一样,建议必须是每个实例的,因为它是有状态的。对于每个建议对象,我们需要 LockMixinAdvisor
的不同实例,因此需要 LockMixin
。该建议包含建议对象状态的一部分。
我们可以通过使用 Advised.addAdvisor()
方法或(推荐的方式)XML 配置(与任何其他建议一样)以编程方式应用此建议。下面讨论的所有代理创建选项,包括“自动代理创建程序”,都能正确处理切入和有状态混合。