1. Introduction

Many times, business logic must execute additional behavior outside its core focus. For example, auditing, performance metrics, transaction control, retry logic, etc. We need a way to bolt on additional functionality ("advice") without knowing what the implementation code ("target") will be, what interfaces it will implement, or even if it will implement an interface.

Frameworks must solve this problem every day. To fully make use of advanced frameworks like Spring and Spring Boot, it is good to understand and be able to implement solutions using some of the dynamic behavior available like:

  • Java Reflection

  • Dynamic (Interface) Proxies

  • CGLIB (Class) Proxies

  • Aspect Oriented Programming (AOP)

1.1. Goals

You will learn:

  • to decouple potentially cross-cutting logic away from core business code

  • to obtain and invoke a method reference

  • to wrap add-on behavior to targets in advice

  • to construct and invoke a proxy object containing a target reference and decoupled advice

  • to locate callable join point methods in a target object and apply advice at those locations

  • to enhance services and objects with additional state and behavior

1.2. Objectives

At the conclusion of this lecture and related exercises, you will be able to:

  1. obtain a method reference and invoke it using Java Reflection

  2. create a JDK Dynamic Proxy to implement adhoc interface(s) to form a proxy at runtime for implementing advice

  3. create a CGLIB Proxy to dynamically create a subclass to form a proxy at runtime for implementing advice

  4. create an AOP proxy programmatically using a ProxyFactory

  5. implement dynamically assigned behavior to components in the Spring context using Spring Aspect-Oriented Programming (AOP) and AspectJ

  6. identify method join points to inject using pointcut expressions

  7. implement advice that executes before, after, and around join points

  8. implement parameter injection into advice

  9. enhance services and objects at runtime with additional state using Introductions

2. Rationale

Our problem starts off with two independent classes depicted as ClassA and ClassB and a caller labeled as Client. doSomethingA() is unrelated to doSomethingB() but may share some current or future things in common — like transactions, database connection, or auditing requirements.

svcaop statictype1
Figure 1. New Cross-Cutting Design Decision

We come to a point where CapabilityX is needed in both doSomethingA() and doSomethingB(). An example of this could be normalization or input validation. We could implement the capability within both operations or in near-best situations implement a common solution and have both operations call that common code.

Reuse is good, but depending on how you reuse, it may be more intrusive than necessary.

2.1. Adding More Cross-Cutting Capabilities

Of course, it does not end there, and we have established what could be a bad pattern of coupling the core business code of doSomethingA() and doSomethingB() with tangential features of the additional capabilities (e.g., auditing, timing, retry logic, etc.).

What other choice do we have?

svcaop statictype2
Figure 2. More Cross-Cutting Capabilities

2.2. Using Proxies

svcaop dynamictype

What we can do instead is leave ClassA and ClassB alone and wrap calls to them in a series of one or more proxied calls. X might perform auditing, Y might perform normalization of inputs, and Z might make sure the connection to the database is available and a transaction active. All we need ClassA and ClassB to do is their designated "something".

However, there is a slight flaw to overcome. ClassA and ClassB do not implement a common interface; doSomethingA() and doSomethingB() look very different in signature, and capabilities; neither X, Y, Z or any of the proxy layers know a thing about ClassA or ClassB.

We need to tie these unrelated parts together. Let’s begin to solve this with Java Reflection.

3. Reflection

Java Reflection provides a means to examine a Java class and determine facts about it that can be useful in describing it and invoking it.

svcaop reflection

Let’s say I am in ProxyX applying doSomethingX() to the call and I want to invoke some to-be-determined (TBD) method in the target object. ProxyX does not need to know anything except what to call to have the target invoked. This call will eventually point to doSomethingA() or doSomethingB() at some point.

We can use Java Reflection to solve this problem by

  • inspecting the target object’s class (ClassA or ClassB) to obtain a reference to the method (doSomethingA() or doSomethingB()) we wish to call

  • identify the arguments to be passed to the call

  • identify the target object to call

Let’s take a look at this in action.

3.1. Reflection Method

Java Reflection provides the means to get access to Fields and Methods of a class. In the example below, I show code that gets a reference to the createItem method, in the ItemsService interface, and accepts an object of type ItemDTO.

Obtaining a Java Reflection Method Reference
import info.ejava.examples.svc.aop.items.services.ItemsService;
import java.lang.reflect.Method;
...
Method method = ItemsService.class.getMethod("createItem", ItemDTO.class); (1)
log.info("method: {}", method);
...
1 getting reference to method within ItemsService interface

Java Class has numerous methods that allow us to inspect interfaces and classes for fields, methods, annotations, and related types (e.g., inheritance). getMethod() looks for a method with the String name ("createItem") that accepts the supplied type(s) (ItemDTO). Arguments is a vararg array, so we can pass in as many types as necessary to match the intended call.

The result is a Method instance that we can use to refer to the specific method to be called — but not the target object or specific argument values.

Example Reflection Method Output
method: public abstract info.ejava.examples.svc.aop.items.dto.ItemDTO
    info.ejava.examples.svc.aop.items.services.ItemsService.createItem(
        info.ejava.examples.svc.aop.items.dto.ItemDTO)

3.2. Calling Reflection Method

We can invoke the Method reference with a target object and arguments and receive the response as a java.lang.Object.

Example Reflection Method Call
import info.ejava.examples.svc.aop.items.dto.BedDTO;
import info.ejava.examples.svc.aop.items.services.ItemsService;
import java.lang.reflect.Method;
...

ItemsService<BedDTO> bedsService = ... (1)
Method method = ... (2)

//invoke method using target object and args
Object[] args = new Object[] { BedDTO.bedBuilder().name("Bunk Bed").build() }; (3)
log.info("invoke calling: {}({})", method.getName(), args);

Object result = method.invoke(bedsService, args); (4)

log.info("invoke {} returned: {}", method.getName(), result);
1 obtain a target object to invoke
2 get a Method reference like what was shown earlier
3 arguments are passed into invoke() using a varargs array
4 invoke the method on the object and obtain the result
Example Method Reflection Call Output
invoke calling: createItem([BedDTO(super=ItemDTO(id=0, name=Bunk Bed))])
invoke createItem returned: BedDTO(super=ItemDTO(id=1, name=Bunk Bed))

3.3. Reflection Method Result

The end result is the same as if we called the BedsServiceImpl directly.

Example Method Reflection Result
    //obtain result from invoke() return
    BedDTO createdBed = (BedDTO) result;
    log.info("created bed: {}", createdBed);----
Example Method Reflection Result Output
created bed: BedDTO(super=ItemDTO(id=1, name=Bunk Bed))

There, of course, is more to Java Reflection that can fit into a single example — but let’s now take that fundamental knowledge of a Method reference and use that to form some more encapsulated proxies using JDK Dynamic (Interface) Proxies and CGLIG (Class) Proxies.

4. JDK Dynamic Proxies

The JDK offers a built-in mechanism for creating dynamic proxies for interfaces. These are dynamically generated classes — when instantiated at runtime — will be assigned an arbitrary set of interfaces to implement. This allows the generated proxy class instances to be passed around in the application, masquerading as the type(s) they are a proxy for. This is useful in frameworks to implement features for implementation types they will have no knowledge of until runtime. This eliminates the need for compile-time generated proxies. [1]

4.1. Creating Dynamic Proxy

We create a JDK Dynamic Proxy using the static newProxyInstance() method of the java.lang.reflect.Proxy class. It takes three arguments:

  • the classloader for the supplied interfaces

  • the interfaces to implement

  • the handler to implement the custom advice details of the proxy code and optionally complete the intended call (e.g., security policy check handler)

In the example below, GrillServiceImpl extends ItemsServiceImpl<T>, which implements ItemsService<T>. We are creating a dynamic proxy that will implement that interface and delegate to an advice instance of MyInvocationHandler that we write.

Creating Dynamic Proxy
import info.ejava.examples.svc.aop.items.aspects.MyDynamicProxy;
import info.ejava.examples.svc.aop.items.services.GrillsServiceImpl;
import info.ejava.examples.svc.aop.items.services.ItemsService;
import java.lang.reflect.Proxy;
...

ItemsService<GrillDTO> grillService = new GrillsServiceImpl(); (1)

ItemsService<GrillDTO> grillServiceProxy = (ItemsService<GrillDTO>)
    Proxy.newProxyInstance( (2)
            grillService.getClass().getClassLoader(),
            new Class[]{ItemsService.class}, (3)
            new MyInvocationHandler(grillService) (4)
        );

log.info("created proxy {}", grillServiceProxy.getClass());
log.info("handler: {}",
        Proxy.getInvocationHandler(grillServiceProxy).getClass());
log.info("proxy implements interfaces: {}",
        ClassUtils.getAllInterfaces(grillsServiceProxy.getClass()));
1 create target implementation object unknown to dynamic proxy
2 instantiate dynamic proxy instance and underlying dynamic proxy class
3 identify the interfaces implemented by the dynamic proxy class
4 provide advice instance that will handle adding proxy behavior and invoking target instance

4.2. Generated Dynamic Proxy Class Output

The output below shows the $Proxy86 class that was dynamically created and that it implements the ItemsService interface and will delegate to our custom MyInvocationHandler advice.

Example Generated Dynamic Proxy Class Output
created proxy: class com.sun.proxy.$Proxy86
handler: class info.ejava.examples.svc.aop.items.aspects.MyInvocationHandler
proxy implements interfaces:
  [interface info.ejava.examples.svc.aop.items.services.ItemsService, (1)
   interface java.io.Serializable] (2)
1 ItemService interface supplied at runtime
2 Serializable interface implemented by DynamicProxy implementation class

4.3. Alternative Proxy All Construction

Alternatively, we can write a convenience builder that simply forms a proxy for all implemented interfaces of the target instance. The Apache Commons ClassUtils utility class is used to obtain a list of all interfaces implemented by the target object’s class and parent classes.

Alternative Proxy All Construction
import org.apache.commons.lang3.ClassUtils;
...
@RequiredArgsConstructor
public class MyInvocationHandler implements InvocationHandler {
    private final Object target;

    public static Object newInstance(Object target) {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
            ClassUtils.getAllInterfaces(target.getClass()).toArray(new Class[0]),(1)
            new MyInvocationHandler(target));
    }
1 Apache Commons ClassUtils used to obtain all interfaces for target object

4.4. InvocationHandler Class

JDK Dynamic Proxies require an instance that implements the InvocationHandler interface to implement the custom work (aka "advice") and delegate the call to the target instance (aka "around advice"). This is a class that we write. The InvocationHandler interface defines a single reflection-oriented invoke() method taking the proxy, method, and arguments to the call. Construction of this object is up to us — but the raw target object is likely a minimum requirement — as we will need that to make a clean, delegated call.

Example Dynamic Proxy InvocationHandler
...
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
...
@RequiredArgsConstructor
public class MyInvocationHandler implements InvocationHandler { (1)
    private final Object target; (2)

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable { (3)
        //proxy call
    }
}
1 class must implement InvocationHandler
2 raw target object to invoke
3 invoke() is provided reflection information for call

4.5. InvocationHandler invoke() Method

The invoke() method performs any necessary advice before or after the proxied call and uses standard method reflection to invoke the target method. This is the same Method class from the earlier discussion on Java Reflection. The response or thrown exception can be directly returned or thrown from this method.

InvocationHandler Proxy invoke() Method
@Override
public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable {
    //do work ...
    log.info("invoke calling: {}({})", method.getName(), args);

    Object result = method.invoke(target, args);

    //do work ...
    log.info("invoke {} returned: {}", method.getName(), result);
    return result;
}
Must invoke raw target instance — not the proxy
Calling the supplied proxy instance versus the raw target instance would result in a circular loop. We must somehow have a reference to the raw target to be able to directly invoke that instance.

4.6. Calling Proxied Object

The following is an example of the proxied object being called using its implemented interface.

Example Proxied Object Call
GrillDTO createdGrill = grillServiceProxy.createItem(
        GrillDTO.grillBuilder().name("Broil King").build());
log.info("created grill: {}", createdGrill);

The following shows that the call was made to the target object, work was able to be performed before and after the call within the InvocationHandler, and the result was passed back as the result of the proxy.

Example Proxied Call Output
invoke calling: createItem([GrillDTO(super=ItemDTO(id=0, name=Broil King))]) (1)
invoke createItem returned: GrillDTO(super=ItemDTO(id=1, name=Broil King)) (2)
created grill: GrillDTO(super=ItemDTO(id=1, name=Broil King)) (3)
1 work performed within the InvocationHandler advice prior to calling target
2 work performed within the InvocationHandler advice after calling target
3 target method’s response returned to proxy caller

JDK Dynamic Proxies are definitely a level up from constructing and calling Method directly as we did with straight Java Reflection. They are the proxy type of choice within Spring but have the limitation that they can only be used to proxy interface-based objects and not no-interface classes.

If we need to proxy a class that does not implement an interface — CGLIB is an option.

5. CGLIB

Code Generation Library (CGLIB) is a byte instrumentation library that allows the manipulation or creation of classes at runtime. [2]

Where JDK Dynamic Proxies implement a proxy behind an interface, CGLIB dynamically implements a subclass of the class proxied.

This library has been fully integrated into spring-core, so there is nothing additional to add to begin using it directly (and indirectly when we get to Spring AOP).

5.1. Creating CGLIB Proxy

The following code snippet shows a CGLIB proxy being constructed for a ChairsServiceImpl class that implements no interfaces. Take note that there is no separate target instance — our generated proxy class will be a subclass of ChairsServiceImpl and it will be part of the target instance. The real target will be in the base class of the instance. We register an instance of MethodInterceptor to handle the custom advice and optionally complete the call. This is a class that we write when authoring CGLIB proxies.

Creating CGLIB Proxy
import info.ejava.examples.svc.aop.items.aspects.MyMethodInterceptor;
import info.ejava.examples.svc.aop.items.services.ChairsServiceImpl;
import org.springframework.cglib.proxy.Enhancer;
...
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ChairsServiceImpl.class); (1)
enhancer.setCallback(new MyMethodInterceptor()); (2)
ChairsServiceImpl chairsServiceProxy = (ChairsServiceImpl)enhancer.create(); (3)

log.info("created proxy: {}", chairsServiceProxy.getClass());
log.info("proxy implements interfaces: {}",
        ClassUtils.getAllInterfaces(chairsServiceProxy.getClass()));
1 create CGLIB proxy as subclass of target class
2 provide instance that will handle adding proxy advice behavior and invoking base class
3 instantiate CGLIB proxy — this is our target object

The following output shows that the proxy class is of a CGLIB proxy type and implements no known interface other than the CGLIB Factory interface. Note that we were able to successfully cast this proxy to the ChairsServiceImpl type — the assigned base class of the dynamically built proxy class.

Example Generated CGLIB Proxy Class
created proxy: class info.ejava.examples.svc.aop.items.services.GrillsServiceImpl$$EnhancerByCGLIB$$a4035db5
proxy implements interfaces: [interface org.springframework.cglib.proxy.Factory] (1)
1 Factory interface implemented by CGLIB proxy implementation class

5.2. MethodInterceptor Class

To intelligently process CGLIB callbacks, we need to supply an advice class that implements MethodInterceptor. This gives us access to the proxy instance being invoked, the reflection Method reference, call arguments, and a new type of parameter — MethodProxy, which is a reference to the target method implementation in the base class.

Example MethodInterceptor Class
...
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class MyMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object proxy, Method method, Object[] args,
                                    MethodProxy methodProxy) (1)
                                    throws Throwable {
        //proxy call
    }
}
1 additional method used to invoke target object implementation in base class

5.3. MethodInterceptor intercept() Method

The details of the intercept() method are much like the other proxy techniques we have looked at and will look at in the future. The method has a chance to do work before and after calling the target method, optionally calls the target method, and returns the result. The main difference is that this proxy is operating within a subclass of the target object.

Example MethodInterceptor intercept() Method
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
...
@Override
public Object intercept(Object proxy, Method method, Object[] args,
        MethodProxy methodProxy) throws Throwable {
    //do work ...
    log.info("invoke calling: {}({})", method.getName(), args);

    Object result = methodProxy.invokeSuper(proxy, args); (1)

    //do work ...
    log.info("invoke {} returned: {}", method.getName(), result);

    //return result
    return result;
}
1 invoking target object implementation in base class

5.4. Calling CGLIB Proxied Object

The net result is that we are still able to reach the target object’s method and also have the additional capability implemented around the call of that method.

Example CGLIB Proxied Object Call
ChairDTO createdChair = chairsServiceProxy.createItem(
        ChairDTO.chairBuilder().name("Recliner").build());
log.info("created chair: {}", createdChair);
Example CGLIB Proxied Object Call Output
invoke calling: createItem([ChairDTO(super=ItemDTO(id=0, name=Recliner))])
invoke createItem returned: ChairDTO(super=ItemDTO(id=1, name=Recliner))
created chair: ChairDTO(super=ItemDTO(id=1, name=Recliner))

5.5. Dynamic Object CGLIB Proxy

The CGLIB example above formed a proxy around the base class. That is a unique feature to CGLIB because it can proxy no interface classes. If you remember, Dynamic JDK Proxies can only proxy interfaces and must be handed the instance to proxy at runtime. The proxied object is constructed separate from the Dynamic JDK Proxy and then wrapped by the InvocationHandler.

Review: Dynamic JDK Interface Proxy Wrapping Existing Object
ItemsService<GrillDTO> grillService = new GrillsServiceImpl(); (1)

ItemsService<GrillDTO> grillServiceProxy = (ItemsService<GrillDTO>)
    Proxy.newProxyInstance(
            grillService.getClass().getClassLoader(),
            new Class[]{ItemsService.class},
            new MyInvocationHandler(grillService) (2)
        );
1 the proxied object
2 Dynamic JDK Proxy configured to proxy existing object

The same thing is true about CGLIB. We can design a MethodInterceptor that accepts an existing instance and ignores the base class. In this use, the no-interface base class (from a pure Java perspective) is being used as strictly an interface (from an integration perspective). This is helpful when we need to dynamically proxy an object that comes in the door.

CGLIB Proxy Wrapping Existing Object
ChairsServiceImpl chairsServiceToProxy = ...

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ChairsServiceImpl.class);
enhancer.setCallback(new MyMethodInterceptor(  chairsServiceToProxy  )); (1)
ChairsServiceImpl chairsServiceProxy = (ChairsServiceImpl)enhancer.create();
1 we are free to design the MethodInterceptor to communicate with whatever we need

6. AOP Proxy Factory

Spring AOP offers a programmatic way to set up proxies using a ProxyFactory and allow Spring to determine which proxy technique to use. This becomes an available option once we add the spring-boot-starter-aop dependency.

Spring AOP Maven Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

The following snippet shows a basic use case where the code locates a service and makes a call to that service. No proxy is used during this example. I just wanted to set the scene to get started.

Basic Use Case - No Proxy
MowerDTO mower1 = MowerDTO.mowerBuilder().name("John Deer").build();
ItemsService<MowerDTO> mowerService = ...

mowerService.createItem(mower1);

With AOP, we can write advice by implementing numerous interfaces with methods that are geared towards the lifecycle of a method call (e.g., before calling, after success). The following snippet shows a single advice class implementing a few advice methods. We will cover the specific advice methods more in the later declarative Spring AOP section.

Example Before and After Return Advice
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
...
public class SampleAdvice1 implements MethodBeforeAdvice, AfterReturningAdvice {
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        log.info("before: {}.{}({})", target, method, args);
    }

    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        log.info("after: {}.{}({}) = {}", target, method, args, returnValue);
    }
}

Using a Spring AOP ProxyFactory, we can programmatically assemble an AOP Proxy with the advice that will either use JDK Dynamic Proxy or CGLIB.

import org.springframework.aop.framework.ProxyFactory;

...
MowerDTO mower2 = MowerDTO.mowerBuilder().name("Husqvarna").build();
ItemsService<MowerDTO> mowerService = ... (1)

ProxyFactory proxyFactory = new ProxyFactory(mowerService); (2)
proxyFactory.addAdvice(new SampleAdvice1()); (3)

ItemsService<MowerDTO> proxiedMowerService =
        (ItemsService<MowerDTO>) proxyFactory.getProxy(); (4)
proxiedMowerService.createItem(mower2); (5)

log.info("proxy class={}", proxiedMowerService.getClass());
1 this service will be the target of the proxy
2 use ProxyFactory to assemble the proxy
3 assign one or more advice to the target
4 obtain reference to the proxy
5 invoke the target via the proxy

Spring AOP will determine the best approach to implement the proxy and produce one that is likely based on either the JDK Dynamic Proxy or CGLIB. The following snippet shows a portion of the proxyFactory.getProxy() call where the physical proxy is constructed.

Construction of Either Dynamic Proxy or CGLIB Proxy
package org.springframework.aop.framework;
...
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
...
        return new ObjenesisCglibAopProxy(config);
    }
    else {
        return new JdkDynamicAopProxy(config);
    }

The following snippet shows the (before and afterReturn) output from the example target call using the proxy created by Spring AOP ProxyFactory and the name of the proxy class. From the output, we can determine that a JDK Dynamic Proxy was used to implement the proxy.

Before and AfterReturn Advice Output During Call to Target
SampleAdvice1#before:23 before: info.ejava.examples.svc.aop.items.services.MowersServiceImpl@509c0153.public abstract info.ejava.examples.svc.aop.items.dto.ItemDTO info.ejava.examples.svc.aop.items.services.ItemsService.createItem(info.ejava.examples.svc.aop.items.dto.ItemDTO)([MowerDTO(super=ItemDTO(id=0, name=Husqvarna))])(1)

SampleAdvice1#afterReturning:28 after: info.ejava.examples.svc.aop.items.services.MowersServiceImpl@509c0153.public abstract info.ejava.examples.svc.aop.items.dto.ItemDTO info.ejava.examples.svc.aop.items.services.ItemsService.createItem(info.ejava.examples.svc.aop.items.dto.ItemDTO)([MowerDTO(super=ItemDTO(id=0, name=Husqvarna))]) = MowerDTO(super=ItemDTO(id=2, name=Husqvarna))(2)

 proxy class=class jdk.proxy3.$Proxy94 (3)
1 output of before advice
2 output of after advice
3 output with name of proxy class

When you need to programmatically assemble proxies — Reflection, Dynamic Proxies, and CGLIB provide a wide set of tools to accomplish this and AOP ProxyFactory can provide a high level abstraction above them all. In the following sections, we will look at how we can automate this and have a little insight into how automation is achieving its job.

7. Interpose

OK — all that dynamic method calling was interesting, but what sets all that up? Why do we see proxies sometimes and not other times in our Spring application? We will get to the setup in a moment, but let’s first address when we can expect this type of behavior magically setup for us and not. What occurs automatically is primarily a matter of "interpose". Interpose is a term used when we have a chance to insert a proxy in between the caller and target object. The following diagram depicts three scenarios: buddy methods of the same class, calling method of manually instantiated class, and calling method of injected object.

svcaop interpose
Figure 3. Interpose is only Automatic for Injected Components
  1. Buddy Method: For the ClassA with m1() and m2() in the same class, Spring will normally not attempt to interpose a proxy in between those two methods (e.g., @PreAuthorize, @Cacheable). It is a straight Java call between methods of a class. That means no matter what annotations and constraints we define for m2() they will not be honored unless they are also on m1(). There is at least one exception for buddy methods, for @Configuration(proxyBeanMethods=true) — where a CGLIB proxy class will intercept calls between @Bean methods to prevent direct buddy method calls from instantiating independent POJO instances per call (i.e., not singleton components).

  2. Manually Instantiated: For ClassB where m2() has been moved to a separate class but manually instantiated — no interpose takes place. This is a straight Java call between methods of two different classes. That also means that no matter what annotations are defined for m2(), they will not be honored unless they are also in place on m1(). It does not matter that ClassC is annotated as a @Component since ClassB.m1() manually instantiated it versus obtaining it from the Spring Context.

  3. Injected: For ClassD, an instance of ClassC is injected. That means that the injected object has a chance to be a proxy class (either JDK Dynamic Proxy or CGLIB Proxy) to enforce the constraints defined on ClassC.m2().

Keep this in mind as you work with various Spring configurations and review the following sections.

8. Spring AOP

Spring Aspect Oriented Programming (AOP) provides a framework where we can define cross-cutting behavior to injected @Components using one or more of the available proxy capabilities behind the scenes. Spring AOP Proxy uses JDK Dynamic Proxy to proxy beans with interfaces and CGLIB to proxy bean classes lacking interfaces.

Spring AOP is a very capable but scaled back and simplified implementation of AspectJ. All the capabilities of AspectJ are allowed within Spring. However, the features integrated into Spring AOP itself are limited to method proxies formed at runtime. The compile-time byte manipulation offered by AspectJ is not part of Spring AOP.

8.1. AOP Definitions

The following represent some core definitions to AOP. Advice, AOP proxy, target object and (conceptually) the join point should look familiar to you. The biggest new concept here is the pointcut predicate used to locate the join point and how that is all modularized through a concept called "aspect".

svcaop aopterms
Figure 4. AOP Key Terms

Join Point is a point in the program, (e.g., calling a method or throwing exception) in which we want to inject some code. For Spring AOP — this is always an event related to a method. AspectJ offers more types of join points.

Pointcut is a predicate rule that matches against a join point (i.e., a method begin, success, exception, or finally) and associates advice (i.e., more code) to execute at that point in the program. Spring uses the AspectJ pointcut language.

Advice is an action to be taken at a join point. This can be before, after (success, exception, or always), or around a method call. Advice chains are formed much the same as Filter chains of the web tier.

AOP proxy is an object created by AOP framework to implement advice against join points that match the pointcut predicate rule.

Aspect is a modularization of a concern that cuts across multiple classes/methods (e.g., timing measurement, security auditing, transaction boundaries). An aspect is made up of one or more advice action(s) with an assigned pointcut predicate.

Target object is an object being advised by one or more aspects. Spring uses proxies to implement advised (target) objects.

Introduction is declaring additional methods or fields on behalf of a type for an advised object, allowing us to add an additional interface and implementation.

Weaving is the linking aspects to objects. Spring AOP does this at runtime. AspectJ offers compile-time capabilities.

8.2. Enabling Spring AOP

To use Spring AOP, we must first add a dependency on spring-boot-starter-aop. That adds a dependency on spring-aop and aspectj-weaver.

Spring AOP Maven Dependency
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

We enable Spring AOP within our Spring Boot application by adding the @EnableAspectJProxy annotation to a @Configuration class or to the @SpringBootApplication class.

Enabling Spring AOP using Annotation
import org.springframework.context.annotation.EnableAspectJAutoProxy;
...
@Configuration
@EnableAspectJAutoProxy (1)
public class ...
1 add @EnableAspectJAutoProxy to a configuration class to enable dynamic AOP behavior in application

8.3. Aspect Class

Starting at the top — we have the Aspect class. This is a special @Component that defines the pointcut predicates to match and advice (before, after success, after throws, after finally, and around) to execute for join points.

Example Aspect Class
...
import org.aspectj.lang.annotation.Aspect;

@Component (1)
@Aspect (2)
public class ItemsAspect {
    //pointcuts
    //advice
}
1 annotated @Component to be processed by the application context
2 annotated as @Aspect to have pointcuts and advice inspected

8.4. Pointcut

In Spring AOP — a pointcut is a predicate rule that identifies the method join points to match against for Spring beans (only). To help reduce complexity of definition, when using annotations — pointcut predicate rules are expressed in two parts:

  • pointcut expression that determines exactly which method executions we are interested in

  • Java method signature — with name and parameters — to reference it

The signature is a method that returns void. The method name and parameters will be usable in later advice declarations. Although the abstract example below does not show any parameters, they will become quite useful when we begin injecting typed parameters.

Example Pointcut
import org.aspectj.lang.annotation.Pointcut;
...
@Pointcut(/* pointcut expression*/) (1)
public void serviceMethod(/* pointcut parameters */) {} //pointcut signature (2)
1 pointcut expression defines predicate matching rule(s)
2 pointcut Java signature defines a name and parameter types for the pointcut expression

8.5. Pointcut Expression

The Spring AOP pointcut expressions use the AspectJ pointcut language. Supporting the following designators

  • execution

match method execution join points

  • within

match methods below a package or type

  • @within

match methods of a type that has been annotated with a given annotation

  • this

match the proxy for a given type — useful when injecting typed advice arguments when the proxy (not the target) implements a specific type. Proxies can implement more than just the target’s interface.

  • target

match the proxy’s target for a given type — useful when injecting typed advice arguments when the target implements a specific type

  • @target

match methods of a type that has been annotated with specific annotation

  • @annotation

match methods that have been annotated with a given annotation

  • args

match methods that accept arguments matching this criteria

  • @args

match methods that accept arguments annotated with a given annotation

  • bean

Spring AOP extension to match Spring bean(s) based on a name or wildcard name expression

Don’t use pointcut contextual designators for matching
Spring AOP Documentation recommends we use within and/or execution as our first choice of performant predicate matching and add contextual designators (args, @annotation, this, target, etc.) when needed for additional work versus using contextual designators alone for matching.

8.6. Example Pointcut Definition

The following example will match against any method in the service’s package, taking any number of arguments and returning any return type.

Example execution Pointcut
//execution(<return type> <package>.<class>.<method>(params))
@Pointcut("execution(* info.ejava.examples.svc.aop.items.services.*.*(..))") //expression
public void serviceMethod() {} //signature

8.7. Combining Pointcut Expressions

We can combine pointcut definitions into compound definitions by referencing them and joining with a boolean ("&&" or "||") expression. The example below adds an additional condition to serviceMethod() that restricts matches to methods accepting a single parameter of type GrillDTO.

Example Combining Pointcut Expressions
@Pointcut("args(info.ejava.examples.svc.aop.items.dto.GrillDTO)") //expression
public void grillArgs() {} //signature

@Pointcut("serviceMethod() && grillArgs()") //expression
public void serviceMethodWithGrillArgs() {} //signature
Use as Example of Combining Two Pointcut Expressions
Use this example as an example of combining two pointcut expressions. Forming a matching expression using the contextual args() feature works, but is in violation of Spring AOP Documentation performance recommendations. Use contextual args() feature to identify portions of the call to pass into the advice. That will be shown soon.

8.8. Advice

The code that will act on the join point is specified in a method of the @Aspect class and annotated with one of the advice annotations. The following is an example of advice that executes before a join point.

Example Advice
...
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Component
@Aspect
@Slf4j
public class ItemsAspect {
    ...
    @Before("serviceMethodWithGrillArgs()")
    public void beforeGrillServiceMethod() {
        log.info("beforeGrillServiceMethod");
    }

The following table contains a list of the available advice types:

Table 1. Available Advice Types

@Before

runs prior to calling join point

@AfterReturning

runs after successful return from join point

@AfterThrowing

runs after exception from join point

@After

runs after join point no matter — i.e., finally

@Around

runs around join point. Advice must call join point and return result.

An example of each is towards the end of these lecture notes. For now, let’s go into detail on some of the things we have covered.

9. Pointcut Expression Examples

Pointcut expressions can be very expressive and can take some time to fully understand. The following examples should provide a head start in understanding the purpose of each and how they can be used. Other examples are available in the Spring AOP page.

9.1. execution Pointcut Expression

The execution expression allows for the definition of several pattern elements that can identify the point of a method call. The full format is as follows. [3]

execution Pointcut Expression Elements
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

However, only the return type, name, and parameter definitions are required.

Required execution Patterns
execution(ret-type-pattern name-pattern(param-pattern))

The specific patterns include:

  • modifiers-pattern - OPTIONAL access definition (e.g., public, protected)

  • ret-type-pattern - MANDATORY type pattern for return type

    Example Return Type Patterns
    execution(info.ejava.examples.svc.aop.items.dto.GrillDTO *(..)) (1)
    execution(*..GrillDTO *(..)) (2)
    1 matches methods that return an explicit type
    2 matches methods that return GrillDTO type from any package
  • declaring-type-pattern - OPTIONAL type pattern for package and class

    Example Declaring Type (package and class) Pattern
    execution(* info.ejava.examples.svc.aop.items.services.GrillsServiceImpl.*(..)) (1)
    execution(* *..GrillsServiceImpl.*(..)) (2)
    execution(* info.ejava.examples.svc..Grills*.*(..)) (3)
    1 matches methods within an explicit class
    2 matches methods within a GrillsServiceImpl class from any package
    3 matches methods from any class below …​svc and start with letters Grills
  • name-pattern - MANDATORY pattern for method name

    Example Name (method) Pattern
    execution(* createItem(..)) (1)
    execution(* *..GrillsServiceImpl.createItem(..)) (2)
    execution(* create*(..)) (3)
    1 matches any method called createItem of any class of any package
    2 matches any method called createItem within class GrillsServiceImpl of any package
    3 matches any method of any class of any package that starts with the letters create
  • param-pattern - MANDATORY pattern to match method arguments. () will match a method with no arguments. (*) will match a method with a single parameter. (T,*) will match a method with two parameters with the first parameter of type T. (..) will match a method with zero (0) or more parameters

    Example noargs () Pattern
    execution(void info.ejava.examples.svc.aop.items.services.GrillsServiceImpl.deleteItems())(1)
    execution(* *..GrillsServiceImpl.*()) (2)
    execution(* *..GrillsServiceImpl.delete*()) (3)
    1 matches an explicit method that takes no arguments
    2 matches any method within a GrillsServiceImpl class of any package and takes no arguments
    3 matches any method from the GrillsServiceImpl class of any package, taking no arguments, and the method name starts with delete
    Example Single Argument Patterns
    execution(* info.ejava.examples.svc.aop.items.services.GrillsServiceImpl.createItem(*))(1)
    execution(* createItem(info.ejava.examples.svc.aop.items.dto.GrillDTO)) (2)
    execution(* *(*..GrillDTO)) (3)
    1 matches an explicit method that accepts any single argument
    2 matches any method called createItem that accepts a single parameter of a specific type
    3 matches any method that accepts a single parameter of GrillDTO from any package
    Example Multiple Argument Patterns
    execution(* info.ejava.examples.svc.aop.items.services.GrillsServiceImpl.updateItem(*,*))(1)
    execution(* updateItem(int,*)) (2)
    execution(* updateItem(int,*..GrillDTO)) (3)
    1 matches an explicit method that accepts two arguments of any type
    2 matches any method called updateItem that accepts two arguments of type int and any second type
    3 matches any method called updateItem that accepts two arguments of type int and GrillDTO from any package

9.2. within Pointcut Expression

The within pointcut expression is similar to supplying an execution expression with just the declaring type pattern specified.

Example within Expressions
within(info.ejava.examples.svc.aop.items..*) (1)
within(*..ItemsService+) (2)
within(*..BedsServiceImpl) (3)
1 match all methods in package info.ejava.examples.svc.aop.items and its subpackages
2 match all methods in classes that implement ItemsService interface
3 match all methods in BedsServiceImpl class

9.3. target and this Pointcut Expressions

The target and this pointcut designators are very close in concept to within. Like JDK Dynamic Proxy and CGLIB, AOP proxies can implement more interfaces (via Introductions) than the target being proxied. target refers to the object being proxied. this refers to the proxy. These are considered "contextual" designators and are primarily placed in the predicate to pull out members of the call for injection.

Example target and this Patterns
target(info.ejava.examples.svc.aop.items.services.BedsServiceImpl) (1)
this(info.ejava.examples.svc.aop.items.services.BedsServiceImpl) (2)
1 matches methods of target object — object being proxied — is of type
2 matches methods of proxy object — object implementing proxy — is of type
Example @target and @annotation Patterns
@target(org.springframework.stereotype.Service) (1)
@annotation(org.springframework.core.annotation.Order) (2)
1 matches all methods in class annotated with @Service and implemented by target object
2 matches all methods having annotation @Order

10. Advice Parameters

Our advice methods can accept two types of parameters:

  • typed using context designators

  • dynamic using JoinPoint

Context designators like args, @annotation, target, and this allow us to assign a logical name to a specific part of a method call so that can be injected into our advice method.

Dynamic injection involves a single JointPoint object that can answer the contextual details of the call.

Do not use context designators alone as predicates to locate join points
The Spring AOP documentation recommends using within and execution designators to identify a pointcut and contextual designators like args to bind aspects of the call to input parameters. That guidance is not fully followed in the following context examples. We easily could have made the non-contextual designators more explicit.

10.1. Typed Advice Parameters

We can use the args expression in the pointcut to identify criteria for parameters to the method and to specifically access one or more of them.

The left side of the following pointcut expression matches on all executions of methods called createGrill() taking any number of arguments. The right side of the pointcut expression matches on methods with a single argument. When we match that with the createGrill signature — the single argument must be of the type GrillDTO.

Example Single, Typed Argument
@Pointcut("execution(* createItem(..)) && args(grillDTO)") (1) (2)
public void createGrill(GrillDTO grillDTO) {} (3)

@Before("createGrill(grill)") (4)
public void beforeCreateGrillAdvice(GrillDTO grill) { (5)
    log.info("beforeCreateGrillAdvice: {}", grill);
}
1 left hand side of pointcut expression matches execution of createItem methods with any parameters
2 right hand side of pointcut expression matches methods with a single argument and maps that to name grillDTO
3 pointcut signature maps grillDTO to a Java type — the names within the pointcut must match
4 advice expression references createGrill pointcut and maps first parameter to name grill
5 advice method signature maps name grill to a Java type — the names within the advice must match but do not need to match the names of the pointcut

The following is logged before the createGrill method is called.

Example Single, Typed Argument Output
beforeCreateGrillAdvice: GrillDTO(super=ItemDTO(id=0, name=weber))

10.2. Multiple,Typed Advice Parameters

We can use the args designator to specify multiple arguments as well. The right hand side of the pointcut expression matches methods that accept two parameters. The pointcut method signature maps these to parameters to Java types. The example advice references the pointcut but happens to use different parameter names. The names used match the parameters used in the advice method signature.

Example Multiple, Typed Arguments
@Pointcut("execution(* updateItem(..)) && args(grillId, updatedGrill)")
public void updateGrill(int grillId, GrillDTO updatedGrill) {}

@Before("updateGrill(id, grill)")
public void beforeUpdateGrillAdvice(int id, GrillDTO grill) {
    log.info("beforeUpdateGrillAdvice: {}, {}", id, grill);
}

The following is logged before the updateGrill method is called.

Example Multiple, Typed Arguments Output
beforeUpdateGrillAdvice: 1, GrillDTO(super=ItemDTO(id=0, name=green egg)

10.3. Annotation Parameters

We can target annotated classes and methods and make the value of the annotation available to the advice using the pointcut signature mapping. In the example below, we want to match on all methods below the items package that have an @Order annotation and pass that annotation as a parameter to the advice.

Example @Annotation Parameter
import org.springframework.core.annotation.Order;
...
@Pointcut("@annotation(order)") (1)
public void orderAnnotationValue(Order order) {} (2)

@Before("within(info.ejava.examples.svc.aop.items..*) && orderAnnotationValue(order)")
public void beforeOrderAnnotation(Order order) { (3)
    log.info("before@OrderAnnotation: order={}", order.value()); (4)
}
1 we are targeting methods with an annotation and mapping that to the name order
2 the name order is being mapped to the type org.springframework.core.annotation.Order
3 the @Order annotation instance is being passed into advice
4 the value for the @Order annotation can be accessed

I have annotated one of the candidate methods with the @Order annotation and assigned a value of 100.

Example @Annotation Parameter Target Method
import org.springframework.core.annotation.Order;
...
@Service
public class BedsServiceImpl extends ItemsServiceImpl<BedDTO> {
    @Override
    @Order(100)
    public BedDTO createItem(BedDTO item) {

In the output below — we see that the annotation was passed into the advice and provided with the value 100.

Example @Annotation Parameter Output
before@OrderAnnotation: order=100
Annotations can pass contextual values to advice
Think how a feature like this — where an annotation on a method with attribute values — can be of use with security role annotations (@PreAuthorize(hasRole('ADMIN'))).

10.4. Target and Proxy Parameters

We can map the target and proxy references into the advice method using the target() and this() designators. In the example below, the target name is mapped to the ItemsService<BedsDTO> interface and the proxy name is mapped to a vanilla java.lang.Object. The target type mapping constrains this call to the BedsServiceImpl object being proxied.

Example target and this Parameters
@Before("target(target) && this(proxy)")
public void beforeTarget(ItemsService<BedDTO> target, Object proxy) {
    log.info("beforeTarget: target={}, proxy={}",target.getClass(),proxy.getClass());
}

The advice prints the name of each class. The output below shows that the target is of the target implementation type and the proxy is of a CGLIB proxy type (i.e., it is the proxy to the target).

Example target and this Parameters Result
beforeTarget:
  target=class info.ejava.examples.svc.aop.items.services.BedsServiceImpl,
  proxy=class info.ejava.examples.svc.aop.items.services.BedsServiceImpl$$EnhancerBySpringCGLIB$$a38982b5

10.5. Dynamic Parameters

If we have generic pointcuts and do not know ahead of time which parameters we will get and in what order, we can inject a JoinPoint parameter as the first argument to the advice. This object has many methods that provide dynamic access to the context of the method — including parameters. The example below logs the classname, method, and array of parameters in the call.

Example JointPoint Injection
@Before("execution(* *..Grills*.*(..))")
public void beforeGrillsMethodsUnknown(JoinPoint jp) {
    log.info("beforeGrillsMethodsUnknown: {}.{}, {}",
            jp.getTarget().getClass().getSimpleName(),
            jp.getSignature().getName(),
            jp.getArgs());
}

10.6. Dynamic Parameters Output

The following output shows two sets of calls: createItem and updateItem. Each was intercepted at the controller and service level.

Example JointPoint Injection Output
beforeGrillsMethodsUnknown: GrillsController.createItem,
                                    [GrillDTO(super=ItemDTO(id=0, name=weber))]
beforeGrillsMethodsUnknown: GrillsServiceImpl.createItem,
                                    [GrillDTO(super=ItemDTO(id=0, name=weber))]
beforeGrillsMethodsUnknown: GrillsController.updateItem,
                                    [1, GrillDTO(super=ItemDTO(id=0, name=green egg))]
beforeGrillsMethodsUnknown: GrillsServiceImpl.updateItem,
                                    [1, GrillDTO(super=ItemDTO(id=0, name=green egg))]

11. Advice Types

We have five advice types:

  • @Before

  • @AfterReturning

  • @AfterThrowing

  • @After

  • @Around

For the first four — using JoinPoint is optional. The last type (@Around) is required to inject ProceedingJoinPoint — a subclass of JoinPoint — to delegate to the target and handle the result. Let’s take a look at each to have a complete set of examples.

To demonstrate, I am going to define advice of each type that will use the same pointcut below.

Example Pointcut to Demonstrate Advice Types
@Pointcut("execution(* *..MowersServiceImpl.updateItem(*,*)) && args(id,mowerUpdate)")(1)
public void mowerUpdate(int id, MowerDTO mowerUpdate) {} (2)
1 matches all updateItem methods calls in the MowersServiceImpl class taking two arguments
2 arguments will be mapped to type int and MowerDTO

There will be two matching update calls:

  1. the first will be successful and return a result

  2. the second will throw an example NotFound RuntimeException

11.1. @Before

The Before advice will be called prior to invoking the join point method. It has access to the input parameters and can change the contents of them. This advice does not have access to the result.

Example @Before Advice
@Before("mowerUpdate(id, mowerUpdate)")
public void beforeMowerUpdate(JoinPoint jp, int id, MowerDTO mowerUpdate) {
    log.info("beforeMowerUpdate: {}, {}", id, mowerUpdate);
}

The before advice only has access to the input parameters prior to making the call. It can modify the parameters, but not swap them around. It has no insight into what the result will be.

@Before Advice Example for Successful Call
beforeMowerUpdate: 1, MowerDTO(super=ItemDTO(id=0, name=bush hog))

Since the before advice is called prior to the join point, it is oblivious that this call ended in an exception.

@Before Advice Example for Call Throwing Exception
beforeMowerUpdate: 2, MowerDTO(super=ItemDTO(id=0, name=john deer))

11.2. @AfterReturning

After returning, advice will get called when a join point successfully returns without throwing an exception. We have access to the result through an annotation field and can map that to an input parameter.

Example @AfterReturning Advice
@AfterReturning(value = "mowerUpdate(id, mowerUpdate)",
    returning = "result")
public void afterReturningMowerUpdate(JoinPoint jp, int id, MowerDTO mowerUpdate, MowerDTO result) {
    log.info("afterReturningMowerUpdate: {}, {} => {}", id, mowerUpdate, result);
}

The @AfterReturning advice is called only after the successful call and not the exception case. We have access to the input parameters and the result. The result can be changed before returning to the caller. However, the input parameters have already been processed.

@AfterReturning Advice Example for Successful Call
afterReturningMowerUpdate: 1, MowerDTO(super=ItemDTO(id=1, name=bush hog))
    => MowerDTO(super=ItemDTO(id=1, name=bush hog))

11.3. @AfterThrowing

The @AfterThrowing advice is called only when an exception is thrown. Like the successful sibling, we can map the resulting exception to an input variable to make it accessible to the advice.

Example @AfterThrowing Advice
@AfterThrowing(value = "mowerUpdate(id, mowerUpdate)", throwing = "ex")
public void afterThrowingMowerUpdate(JoinPoint jp, int id, MowerDTO mowerUpdate, ClientErrorException.NotFoundException ex) { (1)
    log.info("afterThrowingMowerUpdate: {}, {} => {}", id,mowerUpdate,ex.toString());
}
1 advice will only be called if NotFoundException is thrown — otherwise skipped

The @AfterThrowing advice has access to the input parameters and the exception. The exception will still be thrown after the advice is complete. I am not aware of any ability to squelch the exception and return a non-exception here. Look to @Around to give you that capability at a minimum.

@AfterThrowing Advice Example for Call Throwing Exception
afterThrowingMowerUpdate: 2, MowerDTO(super=ItemDTO(id=0, name=john deer))
    => info.ejava.examples.common.exceptions.ClientErrorException$NotFoundException: item[2] not found

11.4. @After

@After is called after a successful return or exception thrown. It represents logic that would commonly appear in a finally block to close out resources.

Example @After Advice
@After("mowerUpdate(id, mowerUpdate)")
public void afterMowerUpdate(JoinPoint jp, int id, MowerDTO mowerUpdate) {
    log.info("afterReturningMowerUpdate: {}, {}", id, mowerUpdate);
}

The @After advice is always called once the joint point finishes executing. It does not provide an indication of whether the method completed or threw an exception.

@After Advice Example for Successful Call
afterReturningMowerUpdate: 1, MowerDTO(super=ItemDTO(id=1, name=bush hog))
@After Advice Example for Call Throwing Exception
afterReturningMowerUpdate: 2, MowerDTO(super=ItemDTO(id=0, name=john deer))
@After is not Informed of Success or Failure
@After callbacks do not provide a sign of success/failure when they are called. Treat this like a finally clause.

11.5. @Around

@Around is the most capable advice but possibly the most expensive/complex to execute. It has full control over the input and return values and whether the call is made at all. The example below logs the various paths through the advice.

Example @Around Advice
@Around("mowerUpdate(id, mowerUpdate)")
public Object aroundMowerUpdate(ProceedingJoinPoint pjp, int id, MowerDTO mowerUpdate) throws Throwable {
    Object result = null;
    try {
        log.info("entering aroundMowerUpdate: {}, {}", id, mowerUpdate);
        result = pjp.proceed(pjp.getArgs());
        log.info("returning after successful aroundMowerUpdate: {}, {} => {}", id, mowerUpdate, result);
        return result;
    } catch (Throwable ex) {
        log.info("returning after aroundMowerUpdate excdeption: {}, {} => {}", id, mowerUpdate, ex.toString());
        throw ex;
    } finally {
        log.info("returning after aroundMowerUpdate: {}, {} => {}",
                id, mowerUpdate, (result==null ? null :result.toString()));
    }
}

The @Around advice example will log activity before calling the join point, after successful return from join point, and finally after all advice completes.

@Around Advice Example for Successful Call
entering aroundMowerUpdate: 1, MowerDTO(super=ItemDTO(id=0, name=bush hog))
returning after successful aroundMowerUpdate: 1, MowerDTO(super=ItemDTO(id=1, name=bush hog))
    => MowerDTO(super=ItemDTO(id=1, name=bush hog))
returning after aroundMowerUpdate: 1, MowerDTO(super=ItemDTO(id=1, name=bush hog))
    => MowerDTO(super=ItemDTO(id=1, name=bush hog))

The @Around advice example will log activity before calling the join point, after an exception from the join point, and finally after all advice completes.

@Around Advice Example for Call Throwing Exception
entering aroundMowerUpdate: 2, MowerDTO(super=ItemDTO(id=0, name=john deer))
returning after aroundMowerUpdate exception: 2, MowerDTO(super=ItemDTO(id=0, name=john deer))
    => info.ejava.examples.common.exceptions.ClientErrorException$NotFoundException: item[2] not found
returning after aroundMowerUpdate: 2, MowerDTO(super=ItemDTO(id=0, name=john deer))
    => info.ejava.examples.common.exceptions.ClientErrorException$NotFoundException: item[2] not found

12. Introductions

JDK Dynamic Proxy and CGLIB provide a means to define interfaces implemented by the proxy. It was the callback handler’s job to deliver that method call to the correct location. It was easily within the callback handler’s implementation ability to delegate to additional object state within the callback handler instance. The integration of these independent types is referred to as a mixin.

svcaop introductions
Figure 5. Introductions

Introductions enable you to implement a mixin. One can define a target object to implement additional interfaces and provide an implementation for the additional interface(s). This can be handled:

  • using a declarative @DeclareParents construct for component targets in the Spring context

  • with the aid of AOP advice and the ProxyFactory we discussed earlier for data targets.

12.1. Component Introductions

Introductions can be automatically applied to any component being processed by Spring via declarative AOP.

12.1.1. Component Introduction Declaration

The following snippet shows a MyUsageIntroduction interface that will be applied to component classes matching the supplied pattern. We are also assigning a required MyUsageIntroductionImpl implementation that will be the target of the MyUsageIntroduction interface calls.

Example MyUsageIntroduction Definition
@DeclareParents(value="info.ejava.examples.svc.aop.items.services.*", (1)
        defaultImpl = MyUsageIntroductionImpl.class) (2)
public static MyUsageIntroduction mixin; (3)
1 service class pattern to apply Introduction
2 implementation for Introduction interface
3 Java interface type of the Introduction added to the target component — mixin name is not used

12.1.2. Introduction POJO Interface/Implementation

The interface and implementation are nothing special. The implementation is a standard POJO. One thing to note, however, is that the methods in this Introduction interface need to be unique relative to the target. Any duplication in method signatures will be routed to the target and not the Introduction.

For the example, we will implement some state and methods that will be assigned to each advised target component. The following snippets show the interface and implementation that will be used to track calls made to the target.

Example Component Introduction Interface
public interface MyUsageIntroduction {
  List<String> getAllCalled();
  void addCalled(String called);
  void clear();
}

The calls are being tracked as a simple collection of Strings. The client, having access to the service, can also have access to the Introduction methods and state.

Example Introduction Implementation
public class MyUsageIntroductionImpl
                implements MyUsageIntroduction {
  private List<String> calls = new ArrayList<>();
  @Override
  public List<String> getAllCalled() {
        return new ArrayList<>(calls);
  }
  @Override
  public void addCalled(String called) {
      calls.add(called);
  }
  @Override
  public void clear() { calls.clear();
  }
}
Introduction and Target Methods must have Distinct/Unique Signatures
Introduction interface methods must have a signature distinct from the target or else the Introduction will be bypassed.

At this point — we are done enhancing the component with an Introduction interface and implementation. The diagram below shows most of the example all together.

svcaop introduction parents
Figure 6. @DeclareParent Resulting Proxy

The remaining parts of the example have to do with using that additional "mixin" behavior via the proxy.

12.1.3. Component Introduction Example AOP Use

With the Introduction being applied to all component classes matching the pattern above, we are going to interact with the Introduction for all calls to createItem(). We will add code to identify the call being made and log that in the Introduction.

The following snippet shows a @Pointcut expression that will match all createItem() method calls within the same package defined to have the Introduction. The use of AOP here is independent of our proxied target. We can access the same methods from everywhere the proxy is injected — as you will see when we make calls shortly.

Pointcut for Example Introduction Trigger
@Pointcut("execution(* info.ejava.examples.svc.aop.items.services.*.createItem(..))")
public void anyCreateItem() {}

The advice associated with the pointcut, shown in the snippet below, will be called before any createItem() calls against a proxy implementing the MyUsageIntroduction interface. The call is injected with the proxy using the Introduction interface. The advice is oblivious to the actual proxy target type. This example client uses the JoinPoint object to identify the call being made to the proxy and places that into the Introduction list of calls.

Advice for Example Introduction Trigger
@Before("anyCreateItem() && this(usageIntro)") (1)
public void recordCalled(JoinPoint jp, MyUsageIntroduction usageIntro) { (2)
    Object arg = jp.getArgs()[0];
    usageIntro.addCalled(jp.getSignature().toString() + ":" + arg); (3)
}
1 apply advice to createItem calls to proxies implementing the MyUsageIntroduction interface
2 call advice and inject a reference using Introduction type
3 use the Introduction to add information about the call being made
this() References the Proxy, target() References the target
The contextual this designator matches any proxy implementing the MyUsageIntroduction. The contextual target designator would match any target implementing the designated interface. In this case, we must match on the proxy.

12.1.4. Injected Component Example Introduction Use

With the AOP @Before advice in place, we can invoke Spring injected bedsService to trigger the extra Introduction behavior. The bedsService can be successfully cast to MyUsageIntroduction since this instance is a Spring proxy implementing the two (2) interfaces. Using a handle to the MyUsageIntroduction interface we can make use of the additional Introduction information stored with the bedService component.

Client Using Introduction from Injected Component
@Autowired
private ItemsService<BedDTO> bedsService; (1)
...
    BedDTO bed = BedDTO.bedBuilder().name("Single Bed").build();

    BedDTO createdBed = bedsService.createItem(bed); (2)

    MyUsageIntroduction usage = (MyUsageIntroduction) bedsService;(3)
    String signature = String.format("BedDTO %s.createItem(BedDTO):%s",
        BedsServiceImpl.class.getName(), bed);

    then( usage.getAllCalled() ).containsExactly(signature); (4)
1 Introduction was applied to component by Spring
2 call to createItem will trigger @Before advice
3 component can be cast to Introduction interface
4 verification that Introduction was successfully applied to component

Using Introductions for Spring component targets allows us to not only add disparate advice behavior — but also retain state specific to each target with the target.

12.2. Data Introductions

Introductions can also be applied to data. Although it is not technically the same proxy mechanism used with Spring AOP, if you have any familiarity with Hibernate and "managed entities", you will be familiar with the concept of adding behavior and state on a per-target data object basis (Hibernate transitioned from CGLIB to Byte Buddy). We have to push some extra peddles to proxy data since data passed to or returned from component calls are not automatically subject to interpose.

12.2.1. Example Data Introduction

The following snippet shows the core of an example Introduction interface and implementation that will be added to the target data objects. It defines a set of accesses the attributed user of the call has relative to the target data object. We wish to store this state with the target data object.

Table 2. Example Data Introduction Interface
public interface MyAccessIntroduction<T> {
    enum Access { MEMBER, OWNER, ADMIN }
    List<Access> getUserRoles();
    void setUserRoles(List<Access> roles);
    T getData(); (1)
1 Introduction Accesses will augment target
svcaop introduction data only

The implementation below shows the Introduction implementation being given direct access to the target data object (T data). This will be shown later in the example. Of note, since the toString() method duplicates the signature from the target.toString(), the Introduction’s toString() will be bypassed for the target’s by the proxy.

Example Data Introduction Implementation
@RequiredArgsConstructor
@Getter
@ToString (2)
public class MyAccessIntroductionImpl<T> implements MyAccessIntroduction<T> {
    private final List<Access> userRoles = new ArrayList<>();
    private final T data; (1)
    @Override
    public void setUserRoles(List<Access> roles) { ...
...
1 optional direct access to target data use will be shown later
2 contributes no value; duplicates target.toString(); will never be called via proxy

12.2.2. Intercepting Data Target

Unlike the managed components, Spring does not have an automated way to add Introductions to objects returned from methods. We will set up custom interpose using Spring AOP Advice and use manual calls to the ProxyFactory discussed earlier to build our proxy. The following snippet shows a @Pointcut pattern that will match all getItem() methods in our target component package. This is matching the service/method that will return the target data object.

Example Pointcut for Intercepting Data Target
@Pointcut("execution(* info.ejava.examples.svc.aop.items.services.*.getItem(..))")
public void getter() {}

The figure below shows the high level relationships and assembly. The AOP advice:

  1. intercepts call to BedService.createItem()

  2. builds a ProxyFactory to construct a proxy

  3. assigns the target to be advised by the proxy

  4. adds an Introduction interface

  5. adds Introduction implementation as advice

svcaop introduction data
Figure 7. Data Target Introduction

The built proxy is returned to the caller in place of the target with the ability to:

  • be the target BedDTO type with target properties

  • be the MyAccessIntroduction type with access properties and have access to the target

The proxy should then be able to give us a mixin view of our target that will look something like the following:

Mixin View of Target Object
{"id":3,"name":"Bed0", (1)
  "userRoles":["MEMBER"]} (2)
1 target data object state
2 Introduction state

12.2.3. Adding Introduction to Target Data Object

With pointcut defined …​

Review: Example Pointcut for Intercepting Data Target
@Pointcut("execution(* info.ejava.examples.svc.aop.items.services.*.getItem(..))")
public void getter() {}

... we can write @Around advice that will use the AOP ProxyFactory to create a proxy for the target data object and return the proxy to the caller. This needs to be @Around advice since the response object will be replaced with the proxy.

Programmatically Applying Introduction to Data Target
@Around(value = "getter()")
public Object decorateWithAccesses(ProceedingJoinPoint pjp) throws Throwable {
    ItemDTO advisedObject = (ItemDTO) pjp.proceed(pjp.getArgs()); (1)

    //build the proxy with the target data
    ProxyFactory proxyFactory = new ProxyFactory(advisedObject);
    //directly support an interface for the target object
    proxyFactory.setProxyTargetClass(true); (2)

    //assign the mixin
    proxyFactory.addInterface(MyAccessIntroduction.class); (3)
    DelegatingIntroductionInterceptor dii = new DelegatingIntroductionInterceptor(new MyAccessIntroductionImpl(advisedObject)); (4)
    proxyFactory.addAdvice(dii); (5)

    //return the advised object
    ItemDTO proxyObject = (ItemDTO) proxyFactory.getProxy(); (6)
    return proxyObject;
}
1 called method produces the target data object
2 configure proxy to implement the interface of the target data object
3 configure proxy to implement the interface for the Introduction
4 interceptor will be a per-target Introduction
5 each proxy can have many advisors/advice
6 caller will get the proxy — wrapping the target data object — that now includes the Introduction

12.2.4. Applying Accesses to Introduction

With the Introduction in place, we can now independently assign the attributed user accesses for the specific target object. This is a fake example — so the deriveAccess is being used to pick access based on ID alone.

Using Introduction State in Downstream Advice
@AfterReturning(value = "getter()", returning = "protectedObject") (1)
public void assignAccess(ItemDTO protectedObject) throws Throwable { (1)
    log.info("determining access for {}", protectedObject);
    MyAccessIntroduction.Access access = deriveAccess(protectedObject.getId());

    //assigning roles
    ((MyAccessIntroduction) protectedObject).setUserRoles(List.of(access)); (2)

    log.info("augmented item {} with accesses {}", protectedObject, ((MyAccessIntroduction) protectedObject).getUserRoles());
}

//simply make up one of the available accesses for each call based on value of id
private MyAccessIntroduction.Access deriveAccess(int id) {
    int index = id % MyAccessIntroduction.Access.values().length;
    return MyAccessIntroduction.Access.values()[index];
}
1 injecting the returned target data object as ItemDTO to obtain ID
2 using Introduction behavior to decorate with accesses

At this point in time, the caller of getItem() should receive a proxy wrapping the specific target data object decorated with accesses associated with the attributed user. We are now ready for that caller to complete the end-to-end flow before we come back and discuss some additional details glossed over.

12.2.5. Using Data Introduction

The following provides a simplified version of the example client that obtains a BedDTO from an advised getItem() call and is able to have access to the Introduction state for that target data object.

Caller Obtains Target Data Object with Introduction State Decoration
@Autowired
private ItemsService<BedDTO> bedsService; (1)
...
BedDTO bed = ...

BedDTO retrievedBed = bedsService.getItem(bed.getId()); (2)

...=(MyAccessIntroduction) retrieved).getUserRoles(); (3)
1 Spring component with advice
2 advice applied to target BedDTO data object returned.
3 caller has access to Introduction state within target data object proxy

With the end-to-end shown, let’s go back and fill in a few details.

12.2.6. Order Matters

I separated the Introduction advice into separate callbacks in order to make the logic more modular and to make a point about deterministic order. Of course, order matters when applying the related advice described above. As previously presented, we had no deterministic control over the @AfterReturning advice running before or after the @Around advice. It would not work if the Introduction was not added to the target data object until after the accesses were calculated.

Order of advice cannot be controlled using the AOP declarative technique. However, we can separate them into two (2) separate @Aspects and define the order at the @Aspect level. The snippet below shows the two (2) @Advice with their assigned @Order.

public class MyAccessAspects {
    @Pointcut("execution(* info.ejava.examples.svc.aop.items.services.*.getItem(..))")
    public void getter() {}

    @Component
    @Aspect
    @Order(1) //run this before value assignment aspect
    static class MyAccessDecorationAspect {
        @Around(value = "getter()")
        public Object decorateWithAccesses(ProceedingJoinPoint pjp) throws Throwable {
...
    @Component
    @Aspect
    @Order(0) //run this after decoration aspect
    static class MyAccessAssignmentAspect {
        @AfterReturning(value = "getter()", returning = "protectedObject")
        public void assignAccess(ItemDTO protectedObject) throws Throwable {
...

With the above use of separating the advice into separate @Aspect, we now have deterministic control of the order of processing.

12.2.7. Jackson JSON Marshalling

One other option I wanted to achieve was to make the resulting object seamlessly appear to gain additional state for marshalling to external clients. One would normally do that making the following Jackson call using the proxy.

Proxy Marshalled to JSON
String json = new ObjectMapper().writeValueAsString(retrievedBed);
log.info("json={}", json);

The result would ideally be JSON that contained the target data object state augmented with some Introduction state.

Target Data Object State Augmented with Introduction State
{"id":3,"name":"Bed0", (1)
  "userRoles":["MEMBER"]} (2)
1 target data object state
2 Introduction state

However, as presented, we will encounter this or some equally bad error. This is because Jackson and CGLIB do not play well together.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle (through reference chain: info.ejava.examples.svc.aop.items.dto.BedDTO$$SpringCGLIB$$0["advisors"]->org.springframework.aop.support.DefaultIntroductionAdvisor[0]->org.springframework.aop.support.DefaultIntroductionAdvisor["classFilter"])

While researching, I read where Byte Buddy (used by Hibernate), provides better support for this type of use. An alternative I was able to get to work was to apply @JsonSerialize to the Introduction interface to supply a marshalling definition for Jackson to follow.

The snippet below shows where the raw target data object will be made available to Jackson using a wrapper object. That is because any attempt by any method to return the raw data target will result in ProxyFactory wrapping that in a proxy — which would put us back to where we started. Therefore, a wrapper record has been defined to hide the target data object from ProxyFactory but supply it to Jackson with the instruction to ignore it — using @JsonUnwrapped.

Jackson Serialization Definition for Introduction
@JsonSerialize(as=MyAccessIntroduction.class)
public interface MyAccessIntroduction<T> {
    ...
    @JsonIgnore (4)
    T getData(); (2)

    @JsonUnwrapped (5)
    TargetWrapper<T> getRawTarget(); (3)

    record TargetWrapper<T>(@JsonUnwrapped T data){} (1)
}
1 record type defined to wrap raw target — to hide from proxying code
2 method returning raw target will be turned into CGLIB proxy before reaching caller when called
3 method returning wrapped target will provide access to raw target through record
4 CGLIB proxy reaching client is not compatible with Jackson - make Jackson skip it
5 Jackson will ignore the wrapper record and marshal the target contents at this level
Raw Target and Wrapped Target Access
public class MyAccessIntroductionImpl<T> implements MyAccessIntroduction<T> {
   ...
    @Override
    public T getData() {
        return this.data;
    }
    @Override
    public TargetWrapper<T> getRawTarget() {
        return new TargetWrapper<>(this.data);
    }
}

With the above constructs in place, we are able to demonstrate adding "mixin" state/behavior to target data objects. A complication to this goal was to make the result compatible with Jackson JSON. A suggestion found for the Jackson/CGLIB issue was to replace the proxy code with Byte Buddy — which happens to be what Hibernate uses for its data entity proxy implementation.

12.3. JSON Output Result

The snippet below shows the JSON payload result containing both the target object and Introduction state.

Jackson JSON Output
{
(2)
  "userRoles" : [ "ADMIN" ],
(1)
  "id" : 2,
  "name" : "Bed0"
}
1 wrapped target marshalled without an element wrapper
2 Introduction state marshalled with target object state

13. Other Features

We have covered a lot of capabilities in this chapter and likely all you will need. However, know there was at least one topic left unaddressed that I thought might be of interest in certain circumstances.

  • Schema Based AOP Support - Spring also offers a means to express AOP behavior using XML. They are very close in capability to what was covered here — so if you need the ability to flexibly edit aspects in production without changing the Java code — this is an attractive option.

14. Summary

In this module, we learned:

  • how we can decouple potentially cross-cutting logic from business code using different levels of dynamic invocation technology

  • to obtain and invoke a method reference using Java Reflection

  • to encapsulate advice within proxy classes using interfaces and JDK Dynamic Proxies

  • to encapsulate advice within proxy classes using classes and CGLIB dynamically written subclasses

  • to integrate Spring AOP into our project

  • to programmatically construct a proxy using Spring AOP ProxyFactory that will determine proxy technology to use

  • to identify method join points using AspectJ language

  • to implement different types of advice (before, after (completion, exception, finally), and around)

  • to inject contextual objects as parameters to advice

  • to implement disparate "mixin" behavior with Introductions

    • for components using @DeclaredParent

    • for data objects using programmatic ProxyFactory

After learning this material, you will surely be able to automatically envision the implementation techniques used by Spring to add framework capabilities to our custom business objects. Those interfaces we implement and annotations we assign are likely the target of many Spring AOP aspects, adding advice in a configurable way.


1. "Dynamic Proxy Classes", Oracle JavaSE 8 Technotes
2. "Introduction to cglib", Baeldung, Aug 2019
3. "Spring AOP execution Examples", Spring AOP