Spring 中的通知 API
现在,我们可以研究 Spring AOP 如何处理建议。
建议生命周期
每个建议都是一个 Spring bean。一个建议实例可以在所有被建议的对象之间共享,也可以对每个被建议的对象是唯一的。这对应于按类或按实例的建议。
按类建议使用得最多。它适用于通用建议,例如事务顾问。这些不依赖于代理对象的状态或添加新状态。它们只是作用于方法和参数。
按实例建议适用于引入,以支持混合。在这种情况下,建议会向代理对象添加状态。
你可以在同一个 AOP 代理中使用共享和按实例建议的混合。
Spring 中的通知类型
Spring 提供了几种通知类型,并且可扩展以支持任意通知类型。本节介绍基本概念和标准通知类型。
环绕通知拦截
Spring 中最基本的通知类型是环绕通知拦截。
Spring 符合使用方法拦截的环绕通知的 AOP Alliance
接口。实现 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 方法。但是,如果没有充分的理由,您不希望这样做。
MethodInterceptor 实现提供与其他 AOP Alliance 兼容的 AOP 实现的互操作性。本节其余部分讨论的其他通知类型以 Spring 特定的方式实现了常见的 AOP 概念。虽然使用最具体的通知类型有优势,但如果您可能希望在另一个 AOP 框架中运行切面,请坚持使用 MethodInterceptor 环绕通知。请注意,切入点当前无法在框架之间互操作,并且 AOP Alliance 当前未定义切入点接口。
|
前置通知
一种更简单的通知类型是前置通知。它不需要 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
}
}
在任何切入点使用建议之前。 |
抛出建议
如果切入点抛出异常,则在切入点返回后调用抛出建议。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
。)与引入一样,顾问必须是按实例的,因为它是有状态的。对于每个被建议的对象,我们需要 LockMixinAdvisor
的不同实例,因此需要 LockMixin
。顾问包括被建议对象状态的一部分。
我们可以通过使用 Advised.addAdvisor()
方法或(推荐的方式)在 XML 配置中以编程方式应用此顾问,就像任何其他顾问一样。下面讨论的所有代理创建选项,包括“自动代理创建器”,都能正确处理引入和有状态的混合。