Spring 中的通知 API
现在我们可以研究 Spring AOP 如何处理通知。
通知生命周期
每个通知都是一个 Spring Bean。一个通知实例可以跨所有被通知对象共享,也可以对于每个被通知对象都是唯一的。这对应于类级别通知(per-class advice)或实例级别通知(per-instance advice)。
类级别通知最常使用。它适用于通用通知,例如事务通知器(transaction advisors)。这些通知器不依赖于代理对象的状态或添加新状态。它们仅作用于方法和参数。
实例级别通知适用于引入(introductions),以支持 mixin。在这种情况下,通知会向代理对象添加状态。
你可以在同一个 AOP 代理中使用共享通知和实例级别通知的组合。
Spring 中的通知类型
Spring 提供了多种通知类型,并且可以扩展以支持任意通知类型。本节描述了基本概念和标准通知类型。
环绕拦截通知
Spring 中最基本的通知类型是环绕拦截通知。
Spring 遵循 AOP Alliance 的环绕通知接口,该接口使用方法拦截。因此,实现环绕通知的类应该实现 org.aopalliance.intercept
包中的以下 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 result = invocation.proceed();
System.out.println("Invocation returned");
return result;
}
}
class DebugInterceptor : MethodInterceptor {
override fun invoke(invocation: MethodInvocation): Any {
println("Before: invocation=[$invocation]")
val result = invocation.proceed()
println("Invocation returned")
return result
}
}
注意对 MethodInvocation
的 proceed()
方法的调用。这会将执行流沿着拦截器链向下推进到连接点。大多数拦截器都会调用此方法并返回其返回值。然而,MethodInterceptor
,就像任何环绕通知一样,可以返回一个不同的值或抛出异常,而不是调用 proceed 方法。但是,如果没有充分的理由,不应该这样做。
MethodInterceptor 实现提供了与遵循 AOP Alliance 标准的其他 AOP 实现的互操作性。本节其余部分讨论的其他通知类型实现了常见的 AOP 概念,但采用了 Spring 特有的方式。虽然使用最具体的通知类型有好处,但如果你可能想在其他 AOP 框架中运行切面,最好坚持使用 MethodInterceptor 环绕通知。请注意,目前切入点在不同框架之间是不可互操作的,AOP Alliance 目前也没有定义切入点接口。 |
前置通知
一种更简单的通知类型是前置通知。它不需要 MethodInvocation
对象,因为它只在进入方法之前调用。
前置通知的主要优点是无需调用 proceed()
方法,因此不可能意外地未能沿着拦截器链向下推进。
以下列表显示了 MethodBeforeAdvice
接口
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method m, Object[] args, Object target) throws Throwable;
}
注意返回类型是 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
}
}
前置通知可以与任何切入点一起使用。 |
抛出通知
抛出通知在连接点返回后被调用,如果连接点抛出了异常。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
}
}
如果一个抛出通知方法本身抛出了异常,它会覆盖原始异常(也就是说,它会改变抛给用户的异常)。覆盖异常通常是一个 RuntimeException,它与任何方法签名都兼容。然而,如果一个抛出通知方法抛出了一个受检异常,它必须与目标方法声明的异常匹配,因此,在某种程度上与特定的目标方法签名耦合。不要抛出与目标方法签名不兼容的、未声明的受检异常! |
抛出通知可以与任何切入点一起使用。 |
返回后通知
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
}
}
此通知不会改变执行路径。如果它抛出异常,异常会沿拦截器链向上传播,而不是返回返回值。
返回后通知可以与任何切入点一起使用。 |
引入通知
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
}
这说明了一个 mixin。我们希望能够将任何类型的被通知对象强制转换为 Lockable
,并调用 lock 和 unlock 方法。如果我们调用 lock()
方法,我们希望所有 setter 方法都抛出 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
方法,否则继续向下执行到连接点)通常就足够了。在本例中,我们需要添加一个检查:如果在锁定模式下,任何 setter 方法都不能被调用。
所需的引入只需持有一个独立的 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
。)与引入通知一样,通知器必须是实例级别的(per-instance),因为它是 estatales。每个被通知对象都需要一个不同的 LockMixinAdvisor
实例,也因此需要不同的 LockMixin
实例。通知器构成了被通知对象状态的一部分。
我们可以通过使用 Advised.addAdvisor()
方法以编程式方式应用此通知器,或者(推荐方式)像其他任何通知器一样在 XML 配置中应用。下面讨论的所有代理创建选项,包括“自动代理创建器”,都能正确处理引入通知和有状态的 mixin。