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:
-
obtain a method reference and invoke it using Java Reflection
-
create a JDK Dynamic Proxy to implement adhoc interface(s) to form a proxy at runtime for implementing advice
-
create a CGLIB Proxy to dynamically create a subclass to form a proxy at runtime for implementing advice
-
create an AOP proxy programmatically using a
ProxyFactory
-
implement dynamically assigned behavior to components in the Spring context using Spring Aspect-Oriented Programming (AOP) and AspectJ
-
identify method join points to inject using pointcut expressions
-
implement advice that executes before, after, and around join points
-
implement parameter injection into advice
-
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.
Figure 1. New Cross-Cutting Design Decision
|
We come to a point where 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 What other choice do we have? |
Figure 2. More Cross-Cutting Capabilities
|
2.2. Using Proxies
What we can do instead is leave However, there is a slight flaw to overcome. |
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.
Let’s say I am in |
We can use Java Reflection to solve this problem by
-
inspecting the target object’s class (
ClassA
orClassB
) to obtain a reference to the method (doSomethingA()
ordoSomethingB()
) 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
.
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.
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
.
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 |
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.
//obtain result from invoke() return
BedDTO createdBed = (BedDTO) result;
log.info("created bed: {}", createdBed);----
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.
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.
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.
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.
...
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.
@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.
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.
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.
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.
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.
...
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.
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.
ChairDTO createdChair = chairsServiceProxy.createItem(
ChairDTO.chairBuilder().name("Recliner").build());
log.info("created chair: {}", createdChair);
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
.
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.
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.
<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.
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.
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.
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.
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.
-
Buddy Method: For the
ClassA
withm1()
andm2()
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 form2()
they will not be honored unless they are also onm1()
. 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). -
Manually Instantiated: For
ClassB
wherem2()
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 form2()
, they will not be honored unless they are also in place onm1()
. It does not matter thatClassC
is annotated as a@Component
sinceClassB.m1()
manually instantiated it versus obtaining it from the Spring Context. -
Injected: For
ClassD
, an instance ofClassC
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 onClassC.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".
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
.
<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.
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.
...
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.
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
|
match method execution join points |
|
match methods below a package or type |
|
match methods of a type that has been annotated with a given annotation |
|
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. |
|
match the proxy’s target for a given type — useful when injecting typed advice arguments when the target implements a specific type |
|
match methods of a type that has been annotated with specific annotation |
|
match methods that have been annotated with a given annotation |
|
match methods that accept arguments matching this criteria |
|
match methods that accept arguments annotated with a given annotation |
|
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.
//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
.
@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.
...
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:
@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(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.
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 Patternsexecution(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) Patternexecution(* 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 package3 matches methods from any class below …svc
and start with lettersGrills
-
name-pattern - MANDATORY pattern for method name
Example Name (method) Patternexecution(* createItem(..)) (1) execution(* *..GrillsServiceImpl.createItem(..)) (2) execution(* create*(..)) (3)
1 matches any method called createItem
of any class of any package2 matches any method called createItem
within classGrillsServiceImpl
of any package3 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 typeT
.(..)
will match a method with zero (0) or more parametersExample noargs () Patternexecution(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 arguments3 matches any method from the GrillsServiceImpl
class of any package, taking no arguments, and the method name starts withdelete
Example Single Argument Patternsexecution(* 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 type3 matches any method that accepts a single parameter of GrillDTO
from any packageExample Multiple Argument Patternsexecution(* 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 typeint
and any second type3 matches any method called updateItem
that accepts two arguments of typeint
andGrillDTO
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.
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.
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 |
@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
.
@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.
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.
@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.
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.
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
.
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
.
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.
@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).
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.
@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.
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.
@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:
-
the first will be successful and return a result
-
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.
@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.
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.
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.
@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.
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.
@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.
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.
@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.
afterReturningMowerUpdate: 1, MowerDTO(super=ItemDTO(id=1, name=bush hog))
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.
@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 exception: {}, {} => {}", 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.
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.
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.
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:
|
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.
@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
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
|
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.
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("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.
@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.
@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.
|
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.
@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.
@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:
-
intercepts call to BedService.createItem()
-
builds a ProxyFactory to construct a proxy
-
assigns the target to be advised by the proxy
-
adds an Introduction interface
-
adds Introduction implementation as advice
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:
{"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 …
@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.
@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.
@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.
@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.
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
|
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
.
@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 |
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.
{
(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.