This version is still in development and is not considered stable yet. For the latest stable version, please use Spring Framework 6.2.1! |
Advice API in Spring
Now we can examine how Spring AOP handles advice.
Advice Lifecycles
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.
You can use a mix of shared and per-instance advice in the same AOP proxy.
Advice Types in 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
The most fundamental advice type in Spring is interception around advice.
Spring is compliant with the AOP Alliance interface for around advice that uses method
interception. Classes that implement around advice should therefore implement the
following MethodInterceptor
interface from the org.aopalliance.intercept
package:
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
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: typically the return value of
the join point.
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 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
}
}
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.
MethodInterceptor implementations offer interoperability with other AOP Alliance-compliant AOP
implementations. The other advice types discussed in the remainder of this section
implement common AOP concepts but in a Spring-specific way. While there is an advantage
in using the most specific advice type, stick with MethodInterceptor around advice if
you are likely to want to run the aspect in another AOP framework. Note that pointcuts
are not currently interoperable between frameworks, and the AOP Alliance does not
currently define pointcut interfaces.
|
Before Advice
A simpler advice type is a before advice. This does not need a MethodInvocation
object, since it is called only before entering the method.
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.
The following listing shows the MethodBeforeAdvice
interface:
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method m, Object[] args, Object target) throws Throwable;
}
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.
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
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
marker 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 listings show classes that are examples of throws advice.
The following advice is invoked if a RemoteException
is thrown (including subclasses of
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
}
}
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
}
}
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
}
}
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
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 treats introduction advice as a special kind of interception advice.
Introduction requires an IntroductionAdvisor
and an IntroductionInterceptor
that
implement the following interface:
public interface IntroductionInterceptor extends MethodInterceptor {
boolean implementsInterface(Class intf);
}
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()
.
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();
}
There is no MethodMatcher
and, hence, no Pointcut
associated with introduction
advice. Only class filtering is logical.
The getInterfaces()
method returns the interfaces introduced by this advisor.
The validateInterfaces()
method is used internally to see whether or not the
introduced interfaces can be implemented by the configured IntroductionInterceptor
.
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
}
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.
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.
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.
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.
Note the use of the locked
instance variable. This effectively adds additional state
to that held in the target object.
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)
}
}
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.
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)
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.
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.