Advice API in Spring
-
环绕通知(Around Advice): 环绕整个方法调用,允许定制输入和输出。
-
前置通知(Before Advice): 在方法调用之前执行,用于在调用之前执行自定义操作。
-
抛出通知(Throws Advice): 在方法抛出异常时执行,用于处理或记录异常。
-
返回后通知(After Returning Advice): 在方法顺利返回时执行,用于处理或记录返回值。
-
切入通知(Introduction Advice): 将新接口和行为引入目标对象,扩展其功能,而无需修改其源代码。
现在我们可以检查 Spring AOP 如何处理建议。
Now we can examine how Spring AOP handles advice.
Advice Lifecycles
每条建议都是一个 Spring bean。建议实例可以在所有建议对象中共享,也可以是特定于每个建议对象的。这对应于按类或按实例的建议。
Each advice is a Spring bean. An advice instance can be shared across all advised objects or be unique to each advised object. This corresponds to per-class or per-instance advice.
按类建议使用最频繁。它适用于通用的建议,比如事务顾问。这些不依赖代理对象的当前状态或不添加新状态。它们仅对方法和参数起作用。
Per-class advice is used most often. It is appropriate for generic advice, such as transaction advisors. These do not depend on the state of the proxied object or add new state. They merely act on the method and arguments.
按实例建议适用于导入,用于支持混入。在这种情况下,建议会向代理对象添加状态。
Per-instance advice is appropriate for introductions, to support mixins. In this case, the advice adds state to the proxied object.
您可以在同一 AOP 代理中使用共享和按实例建议的混合。
You can use a mix of shared and per-instance advice in the same AOP proxy.
Advice Types in Spring
Spring 提供了几种建议类型并可以通过扩展支持任意建议类型。本节对基本概念和标准建议类型进行了描述。
Spring provides several advice types and is extensible to support arbitrary advice types. This section describes the basic concepts and standard advice types.
Interception Around Advice
Spring 中最基础的建议类型是在周围拦截的建议。
The most fundamental advice type in Spring is interception around advice.
Spring 符合 Alliance
AOP 接口,用于使用方法拦截的环绕通知。实现 MethodInterceptor
并实现环绕通知的类也应实现以下接口:
Spring is compliant with the AOP Alliance
interface for around advice that uses method
interception. Classes that implement MethodInterceptor
and that implement around advice should also implement the
following interface:
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
invoke()
方法的 MethodInvocation
参数公开被调用的方法、目标连接点、AOP 代理和传递给该方法的参数。invoke()
方法应返回调用的结果:连接点的返回值。
The MethodInvocation
argument to the invoke()
method exposes the method being
invoked, the target join point, the AOP proxy, and the arguments to the method. The
invoke()
method should return the invocation’s result: the return value of the join
point.
以下示例展示了一个简单的 MethodInterceptor
实现:
The following example shows a simple MethodInterceptor
implementation:
-
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
方法。但是,你不能毫无缘由地这样做。
Note the call to the proceed()
method of MethodInvocation
. This proceeds down the
interceptor chain towards the join point. Most interceptors invoke this method and
return its return value. However, a MethodInterceptor
, like any around advice, can
return a different value or throw an exception rather than invoke the proceed method.
However, you do not want to do this without good reason.
|
|
Before Advice
一种更简单的通知类型是前置通知。这不需要 MethodInvocation
对象,因为它只在进入方法前调用。
A simpler advice type is a before advice. This does not need a MethodInvocation
object, since it is called only before entering the method.
前置通知的主要优势是不必调用 proceed()
方法,因此没有可能无意中未沿着拦截器链继续进行。
The main advantage of a before advice is that there is no need to invoke the proceed()
method and, therefore, no possibility of inadvertently failing to proceed down the
interceptor chain.
以下列表展示了 MethodBeforeAdvice
接口:
The following listing shows the MethodBeforeAdvice
interface:
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method m, Object[] args, Object target) throws Throwable;
}
(Spring 的 API 设计允许字段前置通知,尽管通常的对象适用于字段拦截,但 Spring 永远不会实现它。)
(Spring’s API design would allow for field before advice, although the usual objects apply to field interception and it is unlikely for Spring to ever implement it.)
请注意,返回类型为 void
。前置通知可以在连接点运行前插入自定义行为,但无法更改返回值。如果前置通知抛出一个异常,它将停止拦截器链的进一步执行。该异常会沿着拦截器链向上传播。如果它是未经检查异常或出现在被调用方法的签名中,它将直接传递给客户端。否则,它将被 AOP 代理包装在受检异常中。
Note that the return type is void
. Before advice can insert custom behavior before the join
point runs but cannot change the return value. If a before advice throws an
exception, it stops further execution of the interceptor chain. The exception
propagates back up the interceptor chain. If it is unchecked or on the signature of
the invoked method, it is passed directly to the client. Otherwise, it is
wrapped in an unchecked exception by the AOP proxy.
以下示例展示了 Spring 中的前置通知,该通知计数所有方法调用:
The following example shows a before advice in Spring, which counts all method invocations:
-
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
}
}
在使用任何接入点之前可以使用建议。 |
Before advice can be used with any pointcut. |
Throws Advice
如果连接点抛出一个异常,则会在连接点返回后调用抛出通知。Spring 提供了类型化抛出通知。请注意,这意味着 org.springframework.aop.ThrowsAdvice
接口不包含任何方法。它是一个标记接口,标识给定对象实现了某个或多个类型化抛出通知方法。这些方法应具有以下形式:
Throws advice is invoked after the return of the join point if the join point threw
an exception. Spring offers typed throws advice. Note that this means that the
org.springframework.aop.ThrowsAdvice
interface does not contain any methods. It is a
tag interface identifying that the given object implements one or more typed throws
advice methods. These should be in the following form:
afterThrowing([Method, args, target], subclassOfThrowable)
唯一必需的只是最后一个参数。根据通知方法是否关注于该方法和参数,方法签名可能含有一个或四个参数。以下两个列表展示了抛出通知的示例类。
Only the last argument is required. The method signatures may have either one or four arguments, depending on whether the advice method is interested in the method and arguments. The next two listing show classes that are examples of throws advice.
如果抛出一个 RemoteException
(包括从其子类抛出),则将调用以下通知:
The following advice is invoked if a RemoteException
is thrown (including from subclasses):
-
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
,则将调用以下通知:
Unlike the preceding
advice, the next example declares four arguments, so that it has access to the invoked method, method
arguments, and target object. The following advice is invoked if a ServletException
is thrown:
-
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
。任何数量的抛出通知方法都可以组合在一个单个类中。以下列表展示了最终的示例:
The final example illustrates how these two methods could be used in a single class
that handles both RemoteException
and ServletException
. Any number of throws advice
methods can be combined in a single class. The following listing shows the final example:
-
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! |
If a throws-advice method throws an exception itself, it overrides the original exception (that is, it changes the exception thrown to the user). The overriding exception is typically a RuntimeException, which is compatible with any method signature. However, if a throws-advice method throws a checked exception, it must match the declared exceptions of the target method and is, hence, to some degree coupled to specific target method signatures. Do not throw an undeclared checked exception that is incompatible with the target method’s signature! |
抛出建议可用于任何接入点。 |
Throws advice can be used with any pointcut. |
After Returning Advice
Spring 中的返回后通知必须实现以下列表所示的 org.springframework.aop.AfterReturningAdvice
接口:
An after returning advice in Spring must implement the
org.springframework.aop.AfterReturningAdvice
interface, which the following listing shows:
public interface AfterReturningAdvice extends Advice {
void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable;
}
返回后通知能够访问返回值(它无法修改)、被调用方法、方法的参数和目标。
An after returning advice has access to the return value (which it cannot modify), the invoked method, the method’s arguments, and the target.
以下返回后通知计数未抛出异常的所有成功方法调用:
The following after returning advice counts all successful method invocations that have not thrown exceptions:
-
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
}
}
此通知不会更改执行路径。如果它抛出一个异常,该异常将沿拦截器链向上抛出,而不是返回的值。
This advice does not change the execution path. If it throws an exception, it is thrown up the interceptor chain instead of the return value.
在返回建议后可用于任何接入点。 |
After returning advice can be used with any pointcut. |
Introduction Advice
Spring 对待切入建议为一种特殊类型的拦截建议。
Spring treats introduction advice as a special kind of interception advice.
切入需要一个 IntroductionAdvisor
和一个 IntroductionInterceptor
,它们实现以下接口:
Introduction requires an IntroductionAdvisor
and an IntroductionInterceptor
that
implement the following interface:
public interface IntroductionInterceptor extends MethodInterceptor {
boolean implementsInterface(Class intf);
}
从 AOP Alliance MethodInterceptor
接口继承的 invoke()
方法必须实现切入。也就是说,如果调用的方法基于切入接口,那么切入拦截器负责处理方法调用——它无法调用 proceed()
。
The invoke()
method inherited from the AOP Alliance MethodInterceptor
interface must
implement the introduction. That is, if the invoked method is on an introduced
interface, the introduction interceptor is responsible for handling the method call — it
cannot invoke proceed()
.
切入建议不能与任何切入点配合使用,因为它只适用于类级别,而不是方法级别。您只能将切入建议与 IntroductionAdvisor
配合使用,该建议具有以下方法:
Introduction advice cannot be used with any pointcut, as it applies only at the class,
rather than the method, level. You can only use introduction advice with the
IntroductionAdvisor
, which has the following methods:
public interface IntroductionAdvisor extends Advisor, IntroductionInfo {
ClassFilter getClassFilter();
void validateInterfaces() throws IllegalArgumentException;
}
public interface IntroductionInfo {
Class<?>[] getInterfaces();
}
不存在 MethodMatcher
,因此也不存在与切入建议关联的 Pointcut
。只有类过滤具有逻辑意义。
There is no MethodMatcher
and, hence, no Pointcut
associated with introduction
advice. Only class filtering is logical.
getInterfaces()
方法返回该建议切入的接口。
The getInterfaces()
method returns the interfaces introduced by this advisor.
validateInterfaces()
方法在内部用于判断引入的接口能否由已配置的 IntroductionInterceptor
实现。
The validateInterfaces()
method is used internally to see whether or not the
introduced interfaces can be implemented by the configured IntroductionInterceptor
.
考虑 Spring 测试套件中的一个示例,假设我们要将以下接口切入一个或多个对象:
Consider an example from the Spring test suite and suppose we want to introduce the following interface to one or more objects:
-
Java
-
Kotlin
public interface Lockable {
void lock();
void unlock();
boolean locked();
}
interface Lockable {
fun lock()
fun unlock()
fun locked(): Boolean
}
这阐明了一种混合。我们希望能够将建议对象强制转换为 Lockable
(无论其类型是什么)并调用锁定和解锁方法。如果我们调用 lock()
方法,我们希望所有设置器方法都抛出一个 LockedException
。因此,我们可以添加一个方面,该方面提供使对象不可变的能力,而无需它们对此有任何了解:这是 AOP 的一个好例子。
This illustrates a mixin. We want to be able to cast advised objects to Lockable
,
whatever their type and call lock and unlock methods. If we call the lock()
method, we
want all setter methods to throw a LockedException
. Thus, we can add an aspect that
provides the ability to make objects immutable without them having any knowledge of it:
a good example of AOP.
首先,我们需要一个执行繁重工作的 IntroductionInterceptor
。在这种情况下,我们扩展 org.springframework.aop.support.DelegatingIntroductionInterceptor
快捷类。我们可以直接实现 IntroductionInterceptor
,但对大多数情况来说,使用 DelegatingIntroductionInterceptor
是最好的。
First, we need an IntroductionInterceptor
that does the heavy lifting. In this
case, we extend the org.springframework.aop.support.DelegatingIntroductionInterceptor
convenience class. We could implement IntroductionInterceptor
directly, but using
DelegatingIntroductionInterceptor
is best for most cases.
DelegatingIntroductionInterceptor
被设计为将一个切入委托给引入接口的实际实现,隐藏这样做的拦截使用。您可以使用构造函数参数将委托设置成任何对象。默认委托(当使用无参数构造函数时)为 this
。因此,在下一个示例中,委托为 DelegatingIntroductionInterceptor
的 LockMixin
子类。给定一个委托(默认情况下为它自己),一个 DelegatingIntroductionInterceptor
实例会查找由委托实现的所有接口(IntroductionInterceptor
除外),并支持针对其中任何一个的切入。比如 LockMixin
这样的子类可以调用 suppressInterface(Class intf)
方法来禁止不应该暴露的接口。但是,无论 IntroductionInterceptor
准备好支持多少个接口,所使用的 IntroductionAdvisor
会控制实际公开哪些接口。引入的接口会隐藏目标针对同一接口的任何实现。
The DelegatingIntroductionInterceptor
is designed to delegate an introduction to an
actual implementation of the introduced interfaces, concealing the use of interception
to do so. You can set the delegate to any object using a constructor argument. The
default delegate (when the no-argument constructor is used) is this
. Thus, in the next example,
the delegate is the LockMixin
subclass of DelegatingIntroductionInterceptor
.
Given a delegate (by default, itself), a DelegatingIntroductionInterceptor
instance
looks for all interfaces implemented by the delegate (other than
IntroductionInterceptor
) and supports introductions against any of them.
Subclasses such as LockMixin
can call the suppressInterface(Class intf)
method to suppress interfaces that should not be exposed. However, no matter how many
interfaces an IntroductionInterceptor
is prepared to support, the
IntroductionAdvisor
used controls which interfaces are actually exposed. An
introduced interface conceals any implementation of the same interface by the target.
因此,LockMixin
扩展了 DelegatingIntroductionInterceptor
并自身实现了 Lockable
。父类会自动选择 Lockable
可以用于切入,所以我们不必指定这一点。我们可以用这种方式引入任意数量的接口。
Thus, LockMixin
extends DelegatingIntroductionInterceptor
and implements Lockable
itself. The superclass automatically picks up that Lockable
can be supported for
introduction, so we do not need to specify that. We could introduce any number of
interfaces in this way.
注意 locked
实例变量的使用。这有效地为目标对象中所持有的状态添加了附加状态。
Note the use of the locked
instance variable. This effectively adds additional state
to that held in the target object.
以下示例展示了 LockMixin
类的示例:
The following example shows the example LockMixin
class:
-
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
方法;否则将继续进行连接点)通常就足够了。在本例中,我们需要添加一个检查:在锁定模式下不得调用任何设置器方法。
Often, you need not override the invoke()
method. The
DelegatingIntroductionInterceptor
implementation (which calls the delegate
method if
the method is introduced, otherwise proceeds towards the join point) usually
suffices. In the present case, we need to add a check: no setter method can be invoked
if in locked mode.
所需的切入只需要包含一个不同的 LockMixin
实例并指定引入的接口(在本例中,只为 Lockable
)。更复杂的示例可能会引用切入拦截器(将被定义为原型)。在本例中,没有与 LockMixin
相关的配置,因此,我们通过使用 new
创建它。以下示例展示了我们的 LockMixinAdvisor
类:
The required introduction only needs to hold a distinct
LockMixin
instance and specify the introduced interfaces (in this case, only
Lockable
). A more complex example might take a reference to the introduction
interceptor (which would be defined as a prototype). In this case, there is no
configuration relevant for a LockMixin
, so we create it by using new
.
The following example shows our LockMixinAdvisor
class:
-
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
。该建议包含建议对象状态的一部分。
We can apply this advisor very simply, because it requires no configuration. (However, it
is impossible to use an IntroductionInterceptor
without an
IntroductionAdvisor
.) As usual with introductions, the advisor must be per-instance,
as it is stateful. We need a different instance of LockMixinAdvisor
, and hence
LockMixin
, for each advised object. The advisor comprises part of the advised object’s
state.
我们可以通过使用 Advised.addAdvisor()
方法或(推荐的方式)XML 配置(与任何其他建议一样)以编程方式应用此建议。下面讨论的所有代理创建选项,包括“自动代理创建程序”,都能正确处理切入和有状态混合。
We can apply this advisor programmatically by using the Advised.addAdvisor()
method or
(the recommended way) in XML configuration, as any other advisor. All proxy creation
choices discussed below, including “auto proxy creators,” correctly handle introductions
and stateful mixins.