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 MethodInterceptor and that implement around advice should also implement the following interface:

Java
public interface MethodInterceptor extends Interceptor {

	Object invoke(MethodInvocation invocation) throws Throwable;
}
Kotlin
interface MethodInterceptor : Interceptor {

	fun invoke(invocation: MethodInvocation) : Any
}

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.

The following example shows a simple MethodInterceptor implementation:

Java
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;
	}
}
Kotlin
class DebugInterceptor : MethodInterceptor {

	override fun invoke(invocation: MethodInvocation): Any {
		println("Before: invocation=[$invocation]")
		val rval = invocation.proceed()
		println("Invocation returned")
		return rval
	}
}

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:

Java
public interface MethodBeforeAdvice extends BeforeAdvice {

	void before(Method m, Object[] args, Object target) throws Throwable;
}
Kotlin
interface MethodBeforeAdvice : BeforeAdvice {

	fun before(m: Method, args: Array<Any>, target: Any)
}

(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.)

Note that the return type is void. Before advice can insert custom behavior before the join point executes but cannot change the return value. If a before advice throws an exception, it aborts 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
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;
	}
}
Kotlin
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 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.

The following advice is invoked if a RemoteException is thrown (including from subclasses):

Java
public class RemoteThrowsAdvice implements ThrowsAdvice {

	public void afterThrowing(RemoteException ex) throws Throwable {
		// Do something with remote exception
	}
}
Kotlin
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
public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

	public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
		// Do something with all arguments
	}
}
Kotlin
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
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
	}
}
Kotlin
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:

Java
public interface AfterReturningAdvice extends Advice {

	void afterReturning(Object returnValue, Method m, Object[] args, Object target)
			throws Throwable;
}
Kotlin
interface AfterReturningAdvice : Advice {

	fun afterReturning(returnValue: Any, m: Method, args: Array<Any>, target: Any)
}

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
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;
	}
}
Kotlin
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:

Java
public interface IntroductionInterceptor extends MethodInterceptor {

	boolean implementsInterface(Class intf);
}
Kotlin
interface IntroductionInterceptor : MethodInterceptor {

	fun implementsInterface(intf: Class<*>): Boolean
}

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:

Java
public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

	ClassFilter getClassFilter();

	void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

	Class[] getInterfaces();
}
Kotlin
interface IntroductionAdvisor : Advisor, IntroductionInfo {

	val classFilter: ClassFilter

	@Throws(IllegalArgumentException::class)
	fun validateInterfaces()
}

interface IntroductionInfo {

	val interfaces: Array<Class<*>>
}

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
public interface Lockable {
	void lock();
	void unlock();
	boolean locked();
}
Kotlin
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
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);
	}

}
Kotlin
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
public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

	public LockMixinAdvisor() {
		super(new LockMixin(), Lockable.class);
	}
}
Kotlin
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.