Appearance
Aspect-Oriented Programming (AOP)
The configuration module is done. This module starts with AOP, because two of the most important features that follow, @Transactional (chapters 13–15) and @Async (16–17), are built on it. Understanding AOP here makes them easy later.
What AOP is for
In simple terms, AOP intercepts method calls, so you can run some code before and after a method without touching the method.
Why? Look at a typical business method:
java
public void placeOrder(Order order) {
log.info("placeOrder started"); // logging
transaction.begin(); // transaction handling
try {
// ... the actual business logic ...
transaction.commit();
} catch (Exception e) {
transaction.rollback();
throw e;
}
log.info("placeOrder finished");
}Only the middle is business logic. The rest is boilerplate, and it is repetitive: a hundred methods need the same logging and the same transaction handling. AOP says: write only the business logic in the method; I take care of the boilerplate around it.
The boilerplate moves into one class called an aspect. That gives:
- reusability: one piece of logging code serves all hundred methods;
- maintainability: change it in one place and all hundred are changed.
Step 1: add the dependency
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>Step 2: a first aspect
The class whose method we want to intercept:
java
@RestController
@RequestMapping("/api")
public class EmployeeController {
@GetMapping("/fetchEmployee")
public String fetchEmployee() {
return "item fetched";
}
}The aspect:
java
@Aspect
@Component
public class LoggingAspect {
@Before("execution(public String com.example.demo.EmployeeController.fetchEmployee())")
public void beforeMethod() {
System.out.println("inside beforeMethod aspect");
}
}Call GET /api/fetchEmployee:
text
inside beforeMethod aspectand the response is item fetched. Before fetchEmployee() ran, the aspect's method ran. EmployeeController was not changed at all.
The parts:
| Part | Here | Meaning |
|---|---|---|
| aspect | LoggingAspect with @Aspect | the class holding the boilerplate. It also needs @Component, because only beans are seen by Spring. |
| pointcut | execution(public String ...fetchEmployee()) | an expression that says where: which methods to intercept |
| advice | @Before + the method beforeMethod() | what runs, and when: before, after or around the matched method |
| join point | the call of fetchEmployee() | the point where the actual method is invoked |
Most of the work is writing pointcuts, so they come first.
Step 3: pointcuts
All examples below come from one run of one application. EmployeeController.fetchEmployee() (as above) calls methods on a helper bean:
java
@Service
public class EmployeeUtil {
public void employeeHelperMethod(String s, int i) { ... }
public void withDto(EmployeeDao dao) { ... }
}
@Service
public class EmployeeDao { }execution: match specific methods
execution matches methods by their signature:
text
execution( [modifier] return-type package.Class.method(parameters) )
optional required the method's full pathjava
@Before("execution(public String com.example.demo.EmployeeController.fetchEmployee())")- modifier (
public): optional; leave it out to match any. - return type (
String): required. - full path: package, class, method name.
- parameters:
()means no parameters.
Wildcards make it flexible:
| Wildcard | Matches | Example |
|---|---|---|
* | exactly one item: any one return type, any one name, any one parameter | execution(* ...EmployeeController.*(*)) |
.. | zero or more: any number of parameters, or any depth of sub-packages | execution(* com.example.demo..*.fetchEmployee(..)) |
From the run:
| Pointcut | Matched |
|---|---|
execution(* ...EmployeeController.*(*)) | nothing: fetchEmployee() has zero parameters, and (*) needs exactly one |
execution(* ...EmployeeUtil.*(String, ..)) | employeeHelperMethod(String, int): first parameter String, then anything |
execution(* com.example.demo..*.fetchEmployee(..)) | every fetchEmployee, with any parameters, in any class of the package and its sub-packages |
within: every method of a class or package
within works at class level: no return type, no method name.
java
@Before("within(com.example.demo.EmployeeUtil)") // every method of this class
@Before("within(com.example.demo..*)") // every class in the package and belowwithin(EmployeeUtil) matched employeeHelperMethod and withDto.
@within: every method of classes carrying an annotation
When a pointcut type starts with @, you give it an annotation instead of a class.
java
@Before("@within(org.springframework.stereotype.Service)")Every method of every class annotated @Service: here, all methods of EmployeeUtil.
@annotation: methods carrying an annotation
@within looks at the class's annotation; @annotation looks at the method's:
java
@Before("@annotation(org.springframework.web.bind.annotation.GetMapping)")Every method annotated @GetMapping: here, EmployeeController.fetchEmployee(). (Chapter 18 uses this with your own annotation, to intercept exactly the methods you mark.)
args: methods by their argument types
java
@Before("args(String, int)")Any method whose arguments are a String then an int, whatever its name or class: here, employeeHelperMethod("abc", 12). For an object argument, give the class: args(com.example.demo.EmployeeDao).
@args: methods whose argument's class carries an annotation
java
@Before("@args(org.springframework.stereotype.Service)")Any method with one argument whose class is annotated @Service. withDto(EmployeeDao dao) matched, because EmployeeDao is annotated @Service.
target: methods called on an instance of a type
java
@Before("target(com.example.demo.Employee)")Any method called on an object that is an Employee. With an interface, that covers every implementation:
java
public interface Employee { void fetchEmployee(); }
@Component @Qualifier("tempEmployee")
public class TempEmployee implements Employee { ... }
@Component @Qualifier("permanentEmployee")
public class PermanentEmployee implements Employee { ... }The controller injects @Qualifier("tempEmployee") Employee employee and calls employee.fetchEmployee(). target(Employee) matched TempEmployee.fetchEmployee, though the pointcut never names TempEmployee.
Combining pointcuts
Combine expressions with && (and), || (or) and ! (not):
java
// methods of EmployeeController AND in a class annotated @RestController
@Before("execution(* com.example.demo.EmployeeController.*()) && @within(org.springframework.web.bind.annotation.RestController)")
// either condition is enough
@Before("execution(* com.example.demo.EmployeeController.*()) || within(com.example.demo.SomethingElse)")Both matched fetchEmployee(): in the || case, the first part alone was enough.
Named pointcuts
To reuse an expression, give it a name: an empty method annotated with @Pointcut:
java
@Pointcut("execution(* com.example.demo.EmployeeUtil.*(..))")
public void employeeUtilMethods() {} // the body is never used; the method is just a name
@Before("employeeUtilMethods()")
public void beforeAdvice() { ... }
@After("employeeUtilMethods()")
public void afterAdvice() { ... }Step 4: advice, before, after, around
The pointcut picks the methods; the advice decides when its code runs.
| Advice | Runs |
|---|---|
@Before | before the method |
@After | after the method, whether it returned or threw (like finally) |
@AfterReturning | after the method returned normally |
@AfterThrowing | after the method threw |
@Around | around it: you decide where the method itself is called |
With @Before and @After, Spring calls the real method for you. With @Around, you call it, through the ProceedingJoinPoint:
java
@Around("execution(* com.example.demo.EmployeeUtil.outer())")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("around: before proceed");
Object result = joinPoint.proceed(); // the actual method runs here
System.out.println("around: after proceed");
return result;
}proceed() invokes the method (the join point) and returns its result. Code before it runs before the method, code after it runs after. If you never call proceed(), the method never runs, which is how an aspect can skip a call (a cache, a security check).
With both an @Around and an @After on outer(), the run printed:
text
around: before proceed
outer runs
@After -> EmployeeUtil.outer
around: after proceed@Around is the outermost layer: the @After advice ran inside it, after the method and before @Around's code after proceed().
JoinPoint (or ProceedingJoinPoint) as the advice's first parameter also tells you which method was intercepted: joinPoint.getSignature().getName(), joinPoint.getArgs().
Step 5: how it works inside
Some questions should be bothering you by now. How does a call to fetchEmployee() end up running my advice? And if there are thousands of pointcuts, are they all checked on every call? Here is the mechanism.
At startup
- Find the aspects. Spring finds the beans annotated
@Aspect. - Parse the pointcuts. Each expression is parsed once (Spring's
PointcutParser, from AspectJ) into a form that is quick to match, and kept. - Check every other bean. For each bean (
@Component,@Service,@RestController, ...), Spring checks whether any pointcut can match any of its methods (AbstractAutoProxyCreator.wrapIfNecessary). - Create a proxy for each bean that can be intercepted: a generated class that stands in for the bean.
- Inject the proxy. Everywhere the bean is injected, the proxy is injected instead of the real object.
So the expensive matching happens once, at startup; at call time Spring already knows which advice belongs to which method.
The proxy
Spring can generate a proxy in two ways:
- CGLIB: a generated subclass of your class that overrides its methods.
- JDK dynamic proxy: a generated class that implements the same interfaces.
Spring Boot uses CGLIB by default, for every bean, including beans that implement an interface (spring.aop.proxy-target-class=true is Boot's default). The run printed the injected objects' classes:
text
util=EmployeeUtil$$SpringCGLIB$$0 employee=TempEmployee$$SpringCGLIB$$0TempEmployee implements Employee, and still got a CGLIB proxy. (Plain Spring, without Boot, uses a JDK proxy for beans that implement an interface; in Boot you get that only with spring.aop.proxy-target-class=false.)
Conceptually, the generated subclass looks like this:
java
class EmployeeUtil$$SpringCGLIB$$0 extends EmployeeUtil {
@Override
public void employeeHelperMethod(String s, int i) {
// run the advice chain for this method, which in the end calls
// the real EmployeeUtil.employeeHelperMethod(s, i)
}
}Because a proxy works by overriding, it can only intercept methods it can override. Private methods are never intercepted.
At call time: the advice chain
When the controller calls employeeUtil.employeeHelperMethod(...), it is calling the proxy. The proxy's method (CglibAopProxy's interceptor):
- collects the advice that matched this method at startup into a chain;
- walks the chain (
ReflectiveMethodInvocation.proceed(), which keeps a counter of the next advice to run):- a
@Beforeadvice runs its code, then callsproceed()for the next link; - an
@Afteradvice callsproceed()first, then runs its code (in afinally); - an
@Aroundadvice is your method, which callsproceed()where you choose;
- a
- when the counter reaches the end of the chain, it invokes the real method (the join point);
- the calls return back up the chain, so the "after" parts run in reverse order.
Calls inside the same object are not intercepted
The chain only runs when a call goes through the proxy. In the run, EmployeeUtil.outer() called inner() of the same class, and a named pointcut matched inner():
java
public void outer() {
System.out.println("outer calls inner");
inner(); // really this.inner()
}text
outer calls inner
inner runsNo advice ran for inner(). Inside outer(), this is the real object, not the proxy, so the call skips the chain entirely. Remember this: @Transactional and @Async are built on the same proxies and have exactly the same limitation, which chapters 13 and 17 come back to.
Next: The most important use of AOP in Spring is transactions. Chapter 13 starts with why transactions are needed (critical sections, ACID), then shows @Transactional, and how it is nothing more than an @Around advice.
Check yourself
- What problem does AOP solve, and what is an aspect?
- Which dependency does AOP need, and which two annotations does an aspect class need?
- What is the difference between a pointcut, an advice and a join point?
- Does
execution(* com.example.demo.EmployeeController.*(*))matchfetchEmployee()? Why? - What do
*and..mean inexecution(* com.example.demo..*.fetchEmployee(..))? - What is the difference between
within,@withinand@annotation? args(String, int)and@args(org.springframework.stereotype.Service): what does each match?target(com.example.demo.Employee)whereEmployeeis an interface: which calls match?- How is
@Arounddifferent from@Before+@After? What happens if it never callsproceed()? - What happens at startup that makes AOP cheap at call time?
- Which kind of proxy does Spring Boot create for a bean that implements an interface?
outer()callsinner()in the same bean, and a pointcut matchesinner(). Does the advice run? Why?
Answers
- Boilerplate repeated across many methods (logging, transactions, security checks). An aspect is the one class that holds that code, applied to many methods without changing them.
spring-boot-starter-aop;@Aspectand@Component.- A pointcut says where (which methods); an advice is what runs and when (before, after, around); the join point is the actual method invocation.
- No:
(*)needs exactly one parameter, andfetchEmployee()has none. *: any one return type, any one class name...after the package: any depth of sub-packages;(..): any number of parameters.within: all methods of a given class or package.@within: all methods of classes carrying an annotation.@annotation: methods that themselves carry an annotation.args(String, int): methods whose arguments are aStringthen anint.@args(Service): methods whose (single) argument's class is annotated@Service.- Calls on any object that is an
Employee: every implementing class. @Aroundwraps the call in one method and calls the real method itself withjoinPoint.proceed(). Withoutproceed(), the real method never runs.- Pointcuts are parsed once, every bean is checked once, and proxies are created only for beans that need them; at call time the matching advice is already known.
- A CGLIB proxy (a generated subclass), because Spring Boot sets
proxy-target-class=trueby default. - No. The call goes to
this, the real object, not through the proxy, so the advice chain never runs.