What this post covers
This is the eleventh post in the "Digging into Spring's Source" series. Part 10 followed what happens when you hand a target object and some advice to ProxyFactory: a proxy comes out, built either the JDK way or the CGLIB way. In real Spring applications, though, hardly anyone calls ProxyFactory directly. You switch on @EnableAspectJAutoProxy, register an @Aspect class as a bean, and the container builds and inserts the proxies by itself. This time we open up that "by itself".
There are three questions to answer.
- When
@EnableAspectJAutoProxyis on, how is an@Aspectclass discovered and turned into advisors? - How does the container decide which beans to wrap in a proxy?
- When a method is called on the proxy, in what order do
@Around,@Before,@After,@AfterReturningand@AfterThrowingrun?
| Item | Details |
|---|---|
| Source | Spring Framework 7.1.0-SNAPSHOT |
| Commit | f447f3c310 (2026-10-01, main branch) |
| Module read most | spring-aop (and a little of spring-context) |
| Classes read most | AbstractAutoProxyCreator, AbstractAdvisorAutoProxyCreator, AnnotationAwareAspectJAutoProxyCreator, BeanFactoryAspectJAdvisorsBuilder, ReflectiveAspectJAdvisorFactory, DefaultAdvisorChainFactory, ReflectiveMethodInvocation |
Every line number and every detail in this post refers to that commit. In other versions the line numbers will differ and some details may too. Part 1 explains how to get the source at the same commit.
My study notes go much deeper, line by line. This post keeps only the main thread that someone seeing Spring for the first time can follow. What was left out is named at the end, under "If you want to go deeper".
Words to know first
A proxy is an object that wraps the original and stands in for it, and AOP is the technique of slipping behaviour shared by many classes into such proxies. The two ways of building one (JDK and CGLIB) and ProxyFactory are covered in Part 10.
An aspect is a class that holds both "the behaviour to insert" and "where to insert it". In Spring you mark it with @Aspect. Each method inside it is a piece of advice, the behaviour that actually gets inserted, and annotations such as @Before, @After and @Around say "before the original method, after it, or around both sides". The "where" is called a pointcut and is written as a string expression such as execution(* com.example.Service.*(..)).
An advisor is a word used inside Spring AOP for one pointcut and one piece of advice bundled together. The Advisor interface has getAdvice() (Advisor lines 36–55), and the variant that also carries a pointcut is PointcutAdvisor (lines 26–31). An @Aspect class with five advice methods becomes five advisors.
An interceptor and the advice chain are what the proxy actually executes. MethodInterceptor is an interface with a single method, invoke(MethodInvocation) (lines 45–57); it does its own work and then calls invocation.proceed() to hand over to the next one. Interceptors lined up one after another are the advice chain, and the original method is called at the very end.
A bean post-processor (BeanPostProcessor) is a hook that cuts in while a bean is being built. Part 4 showed that if the "after initialisation" hook returns a different object, that object becomes the bean. This is exactly the door through which the automatic proxy enters; the kinds of post-processors and their order are in Part 6.
The auto-proxy creator is the main character of this post. It is a bean post-processor that, for every bean that gets built, takes the advisors along, checks whether any of them apply, and wraps the bean in a proxy if so.
1. What the single line @EnableAspectJAutoProxy does
Open @EnableAspectJAutoProxy and you find two attributes (proxyTargetClass, exposeProxy) and, apart from those, just one @Import(AspectJAutoProxyRegistrar.class) (lines 119–123). As Part 9 showed, giving @Import an ImportBeanDefinitionRegistrar lets that class register bean definitions directly. What AspectJAutoProxyRegistrar does is essentially one line.
@Override
public void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
// ...
}
The elided part reads the annotation's proxyTargetClass and exposeProxy attributes and applies them. AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary registers one bean definition of class AnnotationAwareAspectJAutoProxyCreator under the long name org.springframework.aop.config.internalAutoProxyCreator (lines 54–55, lines 94–98, registration code at lines 136–141). That bean definition has role ROLE_INFRASTRUCTURE (a marker meaning "internal to Spring, not a user bean") and an order property of HIGHEST_PRECEDENCE. Because the bean is a BeanPostProcessor, refresh() from Part 7 finds and registers it in its post-processor step, and from then on it cuts into the "after initialisation" hook of every bean that gets built.
How one annotation turns into one bean definition. The grey boxes are things covered in earlier posts.
Easy to get wrong: there is only one auto-proxy creator per container
AopConfigUtils lists three auto-proxy creator classes in order of priority.
static {
// Set up the escalation list...
APC_PRIORITY_LIST.add(InfrastructureAdvisorAutoProxyCreator.class);
APC_PRIORITY_LIST.add(AspectJAwareAdvisorAutoProxyCreator.class);
APC_PRIORITY_LIST.add(AnnotationAwareAspectJAutoProxyCreator.class);
}
spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java lines 62–67
If a bean definition with that name already exists, nothing new is registered; only when the requested class has a higher priority is the existing definition's class name swapped, an escalation (lines 124–134). For example, even if @EnableTransactionManagement has already registered InfrastructureAdvisorAutoProxyCreator through AutoProxyRegistrar (AutoProxyRegistrar line 73, TransactionManagementConfigurationSelector line 49), the arrival of @EnableAspectJAutoProxy upgrades it to the most capable one, AnnotationAwareAspectJAutoProxyCreator. So a container always has exactly one auto-proxy creator, and no bean is ever wrapped twice by two creators.
One more thing: the Javadoc of @EnableAspectJAutoProxy says the feature requires aspectjweaver on the classpath (lines 110–112). The annotations @Aspect, @Before and friends belong to the AspectJ library, and so does the parser for the pointcut expressions.
2. The auto-proxy creator is a post-processor
AnnotationAwareAspectJAutoProxyCreator sits at the bottom of an inheritance chain of five classes that starts at ProxyProcessorSupport. Each level has a different job, so here is the map first.
Inheritance runs top to bottom. The green box is the class that @EnableAspectJAutoProxy registers.
| Class | Job | Where |
|---|---|---|
ProxyProcessorSupport |
Proxy settings and Ordered; the decision to go the JDK way when there is an interface |
line 35, lines 97–116 |
AbstractAutoProxyCreator |
Implements the post-processor: "should I wrap?" → "wrap" | lines 96–97 |
AbstractAdvisorAutoProxyCreator |
The four steps that find "the advisors that apply to this bean" | line 55 |
AspectJAwareAdvisorAutoProxyCreator |
Sorts by AspectJ rules, adds ExposeInvocationInterceptor, skips the aspect itself |
lines 50–51 |
AnnotationAwareAspectJAutoProxyCreator |
Adds the advisors built from @Aspect beans to the candidates |
line 51 |
The starting point is the "after initialisation" hook of AbstractAutoProxyCreator. As Part 4 quoted, unless the bean already received its proxy as an early reference, the hook calls wrapIfNecessary (lines 285–293). The early-reference path (lines 237–241) ends up in the same wrapIfNecessary too. Here is its body.
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
// Create proxy if we have advice.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
Yellow is the advisedBeans cache, blue is the procedure, green is the end where the proxy becomes the bean.
Reading from the top:
- A bean already judged is done.
advisedBeansis a map that remembers "did I wrap this bean?" (line 134). If it saysFALSE, the advisors are not looked up again. - Infrastructure classes are never wrapped.
isInfrastructureClassis true when the bean is anAdvice,Pointcut,AdvisororAopInfrastructureBean(lines 359–368).AnnotationAwareAspectJAutoProxyCreatoradds "or an@Aspectclass" (lines 98–110); the comment there explains that if an aspect itself became a proxy, advice invocations would go through the proxy and cause trouble. - Look up the advisors that apply.
getAdvicesAndAdvisorsForBeanis abstract in this class (lines 611–612) and the next level implements it. If none applies it returnsDO_NOT_PROXY, which isnull(line 103), and the original stays the bean. - If some apply, build the proxy.
createProxygoes tobuildProxy, which creates theProxyFactoryfrom Part 10, adds the advisors and callsgetProxy(lines 440–494). The target is wrapped in aSingletonTargetSource: the very original object that has just finished initialising.
JDK or CGLIB is decided right here
Inside buildProxy there is this branch (lines 451–466). If the bean definition carries a "proxy the class" marker, proxyTargetClass is switched on; otherwise evaluateProxyInterfaces inspects the bean class's interfaces. If there is a "reasonable" interface, one with at least one method, those interfaces are added to the proxy; if not, proxyTargetClass is switched on (ProxyProcessorSupport lines 97–116). From there, as Part 10 showed, ProxyFactory looks at proxyTargetClass and the interfaces and picks the method.
Let's check it for real. One bean with an interface, one without, and one that no advice matches. This example was run on the released Spring Framework 7.0.9 (not on a self-built 7.1.0-SNAPSHOT, the version this series reads).
// An example written for this post (not Spring's code)
public interface PaymentService { void pay(); }
public static class PaymentServiceImpl implements PaymentService {
@Override public void pay() { }
}
public static class ReportService { // no interface
public void print() { }
}
public static class PlainService { // no advice matches it
public void nothing() { }
}
@Aspect
public static class TraceAspect {
@Before("execution(* ex.u10.ProxyTypeExample.PaymentService.*(..)) || " +
"execution(* ex.u10.ProxyTypeExample.ReportService.*(..))")
public void trace() { }
}
All four beans go into a configuration class annotated with @EnableAspectJAutoProxy, and the class of each bean is printed. (The program printed its labels in Korean; they are translated here.)
class of the PaymentService bean: jdk.proxy2.$Proxy16
JDK proxy? true
class of the ReportService bean: ex.u10.ProxyTypeExample$ReportService$$SpringCGLIB$$0
JDK proxy? false
class of the PlainService bean: ex.u10.ProxyTypeExample$PlainService
getBean(PaymentServiceImpl.class): NoSuchBeanDefinitionException
PaymentService, which has an interface, became a JDK proxy; ReportService, which has none, became a CGLIB proxy ($$SpringCGLIB$$ in the class name); and PlainService, which no advice matches, kept its original class. The last line is a common trap. A JDK proxy only implements the interface and does not extend PaymentServiceImpl, so asking getBean for the implementation class finds nothing.
Easy to get wrong
- If no advisor applies, no proxy is made.
PlainServiceabove is that case. It is not "every bean is a proxy once@EnableAspectJAutoProxyis on"; only beans with at least one applicable advisor are. - The wrap decision is per bean, with a cache. The key of
advisedBeanscombines the bean class and the bean name (lines 305–312), so two beans of the same class are judged separately. - The post-processor does not only use the "after initialisation" hook. In
postProcessBeforeInstantiation(the "before instantiation" hook) it already records infrastructure classes inadvisedBeans(lines 244–255).shouldSkipis called there, and as the next section shows, that is what first triggers the collection of advisor candidates.
3. How an @Aspect becomes advisors
Step 3 of section 2, getAdvicesAndAdvisorsForBean, is implemented by AbstractAdvisorAutoProxyCreator (lines 75–84), and the real work is in findEligibleAdvisors. Four steps are visible at a glance.
protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
List<Advisor> candidateAdvisors = findCandidateAdvisors();
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
try {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
// ...
}
return eligibleAdvisors;
}
- Collect candidates (
findCandidateAdvisors): gather every advisor in the container. This is the subject of this section. - Keep the ones that apply (
findAdvisorsThatCanApply): keep only those that can apply to this bean class. Section 4. - Extend (
extendAdvisors): add helper advisors if needed. The default implementation is empty (lines 175–176). - Sort (
sortAdvisors): decide the execution order. The default isAnnotationAwareOrderComparator, which looks at@OrderandOrdered(lines 161–164).
Candidates come from two places
The default findCandidateAdvisors delegates to BeanFactoryAdvisorRetrievalHelper, which finds every bean of type Advisor in the container and fetches it with getBean (lines 66–112). That is the route used by features such as transactions, which register advisors as beans directly. But an @Aspect class is an ordinary class that does not implement Advisor. So AnnotationAwareAspectJAutoProxyCreator overrides the method and adds the result of aspectJAdvisorsBuilder.buildAspectJAdvisors() to the list its parent collected (lines 87–96). That buildAspectJAdvisors is where @Aspect gets found. Here is its skeleton for the first call.
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Object.class, true, false);
for (String beanName : beanNames) {
// ...
Class<?> beanType = this.beanFactory.getType(beanName, false);
// ...
if (this.advisorFactory.isAspect(beanType)) {
// ...
MetadataAwareAspectInstanceFactory factory =
new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
if (this.beanFactory.isSingleton(beanName)) {
this.advisorsCache.put(beanName, classAdvisors);
}
// ...
advisors.addAll(classAdvisors);
// ...
aspectNames.add(beanName);
// ...
}
}
this.aspectBeanNames = aspectNames;
return advisors;
Because the bean names are requested for type Object.class, it walks every bean name in the container. It does not create the beans; it only learns the class with getType and then asks isAspect, that is, whether the class carries @Aspect (AbstractAspectJAdvisorFactory lines 87–91). If it is an aspect, advisorFactory.getAdvisors builds the list of advisors, and for a singleton that list is stored in advisorsCache under the bean name. The list of aspect bean names (aspectBeanNames) is remembered too, so from the second call on nothing is walked and everything comes from the cache (lines 149–164).
How one aspect class becomes several advisors. The yellow box is the cache used from the second time on.
One aspect, several advisors
ReflectiveAspectJAdvisorFactory.getAdvisors walks the methods of the aspect class and builds one advisor per advice method (lines 122–166). Which methods, and in what order, is decided by getAdvisorMethods.
private List<Method> getAdvisorMethods(Class<?> aspectClass) {
List<Method> methods = new ArrayList<>();
ReflectionUtils.doWithMethods(aspectClass, methods::add, adviceMethodFilter);
if (methods.size() > 1) {
methods.sort(adviceMethodComparator);
}
return methods;
}
adviceMethodFilter drops methods annotated with @Pointcut (lines 74–76); a pointcut method only gives a name to an expression and has no behaviour to insert. Then adviceMethodComparator sorts by annotation kind. The key ordering of this post comes from here.
static {
// Note: although @After is ordered before @AfterReturning and @AfterThrowing,
// an @After advice method will actually be invoked after @AfterReturning and
// @AfterThrowing methods due to the fact that AspectJAfterAdvice.invoke(MethodInvocation)
// invokes proceed() in a `try` block and only invokes the @After advice method
// in a corresponding `finally` block.
Comparator<Method> adviceKindComparator = new ConvertingComparator<Method, @Nullable Annotation>(
new InstanceComparator<>(
Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
method -> {
AspectJAnnotation ann = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
return (ann != null ? ann.getAnnotation() : null);
});
Comparator<Method> methodNameComparator = new ConvertingComparator<>(Method::getName);
adviceMethodComparator = adviceKindComparator.thenComparing(methodNameComparator);
}
The order is Around → Before → After → AfterReturning → AfterThrowing, and within one kind, by method name. It is not the order in which you wrote them in the source file. A comment explains why: since Java 7 the JDK no longer returns methods in declaration order, so sorting by kind and name keeps the order stable across runs (lines 136–143). The first line of the comment, "@After is sorted before @AfterReturning but actually runs after it", is something we will confirm from real output in section 5.
A method becomes an advisor in getAdvisor (lines 200–222). From the string in the annotation's pointcut or value attribute (AbstractAspectJAdvisorFactory line 161, lines 201–209) it builds an AspectJExpressionPointcut (lines 224–238) and puts that pointcut and the method into an InstantiationModelAwarePointcutAdvisorImpl (lines 213–214). That long-named class is "an advisor that came from an @Aspect". For a singleton aspect, its constructor also builds the advice object straight away (lines 109–114).
The advice object is created by getAdvice, which picks a different class per annotation.
switch (aspectJAnnotation.getAnnotationType()) {
case AtPointcut -> {
// ...
return null;
}
case AtAround -> springAdvice = new AspectJAroundAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
case AtBefore -> springAdvice = new AspectJMethodBeforeAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
case AtAfter -> springAdvice = new AspectJAfterAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
case AtAfterReturning -> {
springAdvice = new AspectJAfterReturningAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
// ...
}
case AtAfterThrowing -> {
springAdvice = new AspectJAfterThrowingAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
// ...
}
// ...
}
All five classes extend AbstractAspectJAdvice, but they implement different interfaces. That difference becomes a difference in how they run, in section 5.
| Annotation | Advice class | Interfaces implemented | Where |
|---|---|---|---|
@Around |
AspectJAroundAdvice |
MethodInterceptor |
line 39 |
@Before |
AspectJMethodBeforeAdvice |
MethodBeforeAdvice |
line 34 |
@After |
AspectJAfterAdvice |
MethodInterceptor, AfterAdvice |
lines 35–36 |
@AfterReturning |
AspectJAfterReturningAdvice |
AfterReturningAdvice, AfterAdvice |
lines 39–40 |
@AfterThrowing |
AspectJAfterThrowingAdvice |
MethodInterceptor, AfterAdvice |
lines 35–36 |
The place that actually calls the advice method is invokeAdviceMethodWithGivenArgs in the shared parent. It takes the aspect bean from aspectInstanceFactory, invokes the method by reflection, and rethrows any exception the advice method threw after unwrapping it (AbstractAspectJAdvice lines 633–658).
Easy to get wrong
- An
@Aspectclass must be a bean. Aspects are found by walking bean names, so an aspect not registered via@Componentor@Beanmight as well not exist. - The aspect scan happens once. Once
aspectBeanNamesis filled, there is no second walk (lines 88–93). And that "once" is earlier than you might think: as the end of section 2 showed,shouldSkipis called in the before-instantiation hook of any bean that is not an infrastructure class, and theshouldSkipofAspectJAwareAdvisorAutoProxyCreatorcallsfindCandidateAdvisors(lines 102–112). A bean definition added to the container after that is not recognised as an aspect. - Not every method in an
@Aspectclass is advice.getAdvisorreturnsnullfor a method without an AspectJ annotation (lines 206–210), andgetAdvisorsleaves those out of the list (lines 144–147).
4. Which beans to wrap, and in what order
With the candidates collected, only those that fit the bean being built are kept (step 2 of section 3). AbstractAdvisorAutoProxyCreator.findAdvisorsThatCanApply delegates to AopUtils.findAdvisorsThatCanApply (lines 130–140), which asks canApply for each candidate (lines 319–340). The skeleton of canApply:
public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
Assert.notNull(pc, "Pointcut must not be null");
if (!pc.getClassFilter().matches(targetClass)) {
return false;
}
MethodMatcher methodMatcher = pc.getMethodMatcher();
// ...
Set<Class<?>> classes = new LinkedHashSet<>();
if (!Proxy.isProxyClass(targetClass)) {
classes.add(ClassUtils.getUserClass(targetClass));
}
classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
for (Class<?> clazz : classes) {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
for (Method method : methods) {
// ...
methodMatcher.matches(method, targetClass)) {
return true;
}
}
}
return false;
}
spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java lines 239–274
A pointcut consists of a ClassFilter that filters classes and a MethodMatcher that filters methods (Pointcut lines 33–45). The class filter goes first; if it passes, the code loops over every method of the class and its interfaces and keeps the advisor if even one matches. "Even one" is enough because this step only decides "should a proxy be made?". Which methods actually get the advice is checked again at call time, in section 5. AspectJExpressionPointcut is both a ClassFilter and a MethodMatcher (lines 83–84) and passes both questions to AspectJ's expression engine (line 273, line 336).
One more is inserted at the very front
AspectJAwareAdvisorAutoProxyCreator overrides extendAdvisors, step 3 of section 3 (lines 97–100). It calls AspectJProxyUtils.makeAdvisorChainAspectJCapableIfNecessary, which, if the list contains at least one AspectJ advisor, inserts ExposeInvocationInterceptor.ADVISOR at index 0 (lines 49–67). ExposeInvocationInterceptor is an interceptor that stores the invocation in progress (MethodInvocation) in a ThreadLocal and passes on (lines 91–101). It is needed because @Before and @After advice must know, when their turn comes, "which method is being called right now" (the JoinPoint), and the before() of the MethodBeforeAdvice interface does not receive that directly. So AbstractAspectJAdvice fetches it with ExposeInvocationInterceptor.currentInvocation() (lines 80–91, lines 670–676).
Sorting: @Order between aspects, kind order within an aspect
AspectJAwareAdvisorAutoProxyCreator also overrides sortAdvisors, step 4 of section 3 (lines 72–90). The comparison rule is AspectJPrecedenceComparator, whose compare has two stages (lines 81–88).
- First it compares with
AnnotationAwareOrderComparator(lines 66–68). The order value of an advisor that came from an@Aspectcomes from the@Orderon the aspect class (or itsOrderedimplementation), and isLOWEST_PRECEDENCEif there is none (BeanFactoryAspectInstanceFactorylines 128–143,InstantiationModelAwarePointcutAdvisorImpllines 180–182). Smaller comes first. - If the order values are equal and both came from the same aspect, it compares declaration order (lines 90–123). But, as section 3 showed, the declaration order of advice found by reflection is hard-coded to
0for all of them (lines 136–144), so this comparison always says "equal". The order within one aspect therefore stays as the kind order thatgetAdvisorMethodssorted into.
The sort itself is done by PartialOrder.sort from the AspectJ library (line 79); that code lives outside this repository, so I did not verify it from source. Instead, the result is checked with an example. Two aspects annotated @Order(2) and @Order(1), each with a @Before and an @After, registered deliberately in the order 2 → 1.
// An example written for this post (not Spring's code)
@Aspect
@Order(2)
public static class SecondAspect {
@Before("execution(* ex.u10.AspectOrderExample.OrderService.*(..))")
public void before() { System.out.println(" @Order(2) @Before"); }
@After("execution(* ex.u10.AspectOrderExample.OrderService.*(..))")
public void after() { System.out.println(" @Order(2) @After"); }
}
@Aspect
@Order(1)
public static class FirstAspect {
@Before("execution(* ex.u10.AspectOrderExample.OrderService.*(..))")
public void before() { System.out.println(" @Order(1) @Before"); }
@After("execution(* ex.u10.AspectOrderExample.OrderService.*(..))")
public void after() { System.out.println(" @Order(1) @After"); }
}
@Order(1) @Before
@Order(2) @Before
[target] place() runs
@Order(2) @After
@Order(1) @After
The smaller value, @Order(1), stands on the outside: first on the way in, last on the way out. The bean registration order made no difference.
This section's three steps joined in one line. The resulting list applies the sorting rules from the text to the example (PartialOrder.sort itself was not checked in source); the order at the bottom is the example's actual output.
Easy to get wrong
@Ordergoes on the aspect class, not on the advice method. The order value is read from the aspect bean. The order between two@Beforemethods in the same aspect is by method name and cannot be changed with@Order.- An aspect with no order comes last. The default is
LOWEST_PRECEDENCE, so an aspect with@Orderalways stands outside one without. Among aspects with no order, the Javadoc says the order is undefined (AbstractAdvisorAutoProxyCreatorlines 42–48). - Advice can end up in front of
ExposeInvocationInterceptor. That interceptor's order value isHIGHEST_PRECEDENCE + 1(lines 103–106). Give an aspectHIGHEST_PRECEDENCEand it sorts in front of it, and if that advice uses theJoinPointan exception is thrown. The exception message describes exactly this situation (lines 71–82).
5. Calling a method on the proxy: the advice chain
Now the proxy exists. What happens when a method is called? In the JDK proxy's invoke (the method from Part 10) there is a line that builds the advice chain and a line that runs it.
// Get the interception chain for this method.
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
// ...
if (chain.isEmpty()) {
// ...
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
// We need to create a method invocation...
MethodInvocation invocation =
new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// Proceed to the joinpoint through the interceptor chain.
retVal = invocation.proceed();
}
spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java lines 202–220
The CGLIB proxy's DynamicAdvisedInterceptor.intercept has the same two lines (line 724, line 738). So whichever way the proxy was built, we only need to look at "build the chain" and "proceed()".
Building the chain: advisors become this method's interceptor list
getInterceptorsAndDynamicInterceptionAdvice first checks a cache keyed by method (methodCache) and, on a miss, has DefaultAdvisorChainFactory build the list (AdvisedSupport lines 516–538). The heart of the builder:
for (Advisor advisor : advisors) {
if (advisor instanceof PointcutAdvisor pointcutAdvisor) {
// Add it conditionally.
if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
boolean match;
// ...
match = mm.matches(method, actualClass);
// ...
if (match) {
MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
// ...
interceptorList.addAll(Arrays.asList(interceptors));
// ...
}
}
}
// ...
}
It walks the advisors held by the proxy in their sorted order, keeps only those whose pointcut matches the method being called this time, and turns them into interceptors. Where section 4's canApply asked "does any method of this class match?", this asks "does this very method match?". That is why the chain can differ from method to method on the same proxy, and why it is cached per method.
"Turning into interceptors" is done by registry.getInterceptors(advisor). In the table of section 3, two of the five advice classes (@Before, @AfterReturning) were not MethodInterceptors. DefaultAdvisorAdapterRegistry uses the advice as-is if it is a MethodInterceptor, and otherwise runs through its registered adapters to wrap it (lines 80–96). There are three default adapters (lines 52–56). Look at the interceptors two of them produce and "before" and "after" are literally in the code.
@Override
public @Nullable Object invoke(MethodInvocation mi) throws Throwable {
this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
return mi.proceed();
}
@Override
public @Nullable Object invoke(MethodInvocation mi) throws Throwable {
Object retVal = mi.proceed();
this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
return retVal;
}
The other three are MethodInterceptors themselves and have their own invoke. The invoke of @After and @AfterThrowing solves the last puzzle of this post.
@Override
public @Nullable Object invoke(MethodInvocation mi) throws Throwable {
try {
return mi.proceed();
}
finally {
invokeAdviceMethod(getJoinPointMatch(), null, null);
}
}
spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterAdvice.java lines 45–53
@Override
public @Nullable Object invoke(MethodInvocation mi) throws Throwable {
try {
return mi.proceed();
}
catch (Throwable ex) {
if (shouldInvokeOnThrowing(ex)) {
invokeAdviceMethod(getJoinPointMatch(), null, ex);
}
throw ex;
}
}
spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterThrowingAdvice.java lines 60–71
@After is a finally, so it runs whether the method returned or threw, and @AfterThrowing calls the advice in a catch and then rethrows the exception with throw ex. It does not swallow it. @Around hands a ProceedingJoinPoint to the advice method, and that join point's proceed() continues the chain on a clone of the MethodInvocation (AspectJAroundAdvice lines 64–71, MethodInvocationProceedingJoinPoint lines 81–83).
proceed(): one index walks the chain
ReflectiveMethodInvocation.proceed(), which runs the chain, is short. It is the class whose name you saw in Part 10.
@Override
public @Nullable Object proceed() throws Throwable {
// We start with an index of -1 and increment early.
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher dm) {
// ...
}
else {
// It's an interceptor, so we just invoke it: The pointcut will have
// been evaluated statically before this object was constructed.
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}
currentInterceptorIndex starts at -1 (line 89). Each call to proceed() bumps the index by one and invokes the interceptor at that position; the interceptor does its work and calls proceed() again. When the index reaches the end, invokeJoinpoint() calls the original object's method by reflection (lines 189–191). Because it is recursive, execution runs front to back on the way in and back to front on the way out, like peeling the layers of an onion and putting them back.
The real output order
All five pieces of advice go into one aspect, and in the source file their declaration order is deliberately shuffled (@After → @AfterReturning → @Before → @Around → @AfterThrowing).
// An example written for this post (not Spring's code). Every pointcut is the same expression.
@Aspect
public static class LoggingAspect {
// the declaration order is deliberately shuffled (After → AfterReturning → Before → Around → AfterThrowing)
@After("execution(* ex.u10.AdviceOrderExample.GreetingService.*(..))")
public void after() {
System.out.println(" @After");
}
@AfterReturning(pointcut = "execution(* ex.u10.AdviceOrderExample.GreetingService.*(..))", returning = "result")
public void afterReturning(Object result) {
System.out.println(" @AfterReturning result=" + result);
}
@Before("execution(* ex.u10.AdviceOrderExample.GreetingService.*(..))")
public void before() {
System.out.println(" @Before");
}
@Around("execution(* ex.u10.AdviceOrderExample.GreetingService.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println(" @Around before");
try {
return pjp.proceed();
}
finally {
System.out.println(" @Around after");
}
}
@AfterThrowing(pointcut = "execution(* ex.u10.AdviceOrderExample.GreetingService.*(..))", throwing = "ex")
public void afterThrowing(Throwable ex) {
System.out.println(" @AfterThrowing ex=" + ex.getMessage());
}
}
greet("spring"), which returns normally, and fail(), which throws, were called one after the other.
== greet("spring") — returns normally
@Around before
@Before
[target] greet(spring) runs
@AfterReturning result=Hello, spring
@After
@Around after
return value: Hello, spring
== fail() — throws
@Around before
@Before
[target] fail() runs — throws an exception
@AfterThrowing ex=boom
@After
@Around after
exception caught in main: boom
The chain in this example. It goes in from top to bottom and comes back from bottom to top. The grey box is the helper interceptor inserted at the front in section 4.
Lining the output up against the chain:
| Position | Interceptor in the chain | On the way in | On the way out |
|---|---|---|---|
| ① | ExposeInvocationInterceptor |
stores the invocation in a ThreadLocal |
restores it |
| ② | AspectJAroundAdvice |
@Around before |
@Around after |
| ③ | MethodBeforeAdviceInterceptor |
@Before |
— |
| ④ | AspectJAfterAdvice |
— | @After (finally) |
| ⑤ | AfterReturningAdviceInterceptor |
— | @AfterReturning on normal return |
| ⑥ | AspectJAfterThrowingAdvice |
— | @AfterThrowing on exception, then rethrow |
| end | invokeJoinpoint |
runs the original method |
Even with the declarations shuffled, the chain was assembled in the comparator's order from section 3, Around → Before → After → AfterReturning → AfterThrowing, and because the way out is reversed, @AfterReturning prints before @After. That is the "sorted before, runs after" from the comment in section 3. In the exception case, ⑤ received the exception straight out of proceed() and passed it on without calling afterReturning, the finally blocks of ④ and ② still ran, and the exception reached main.
Easy to get wrong
- The chain differs per method, and a method with no advice has an empty chain. The
chain.isEmpty()branch in theJdkDynamicAopProxycode above is that case: noMethodInvocationis created and the original is called directly. A proxy does not slow down every method. @AfterReturningchecks the return type. If the type of thereturningparameter does not fit the actual return value, the advice is not called (AspectJAfterReturningAdvicelines 64–69). Thethrowingparameter of@AfterThrowingchecks the exception type the same way (lines 77–79).- If
@Arounddoes not callproceed(), nothing inside it runs. The chain continues by recursion, so if ② skipsproceed(), nothing from ③ down to the original method is called. This follows from the structure checked in the code; I did not run it as an example.
6. Self-invocation gets no advice
In createProxy in section 2, the target was new SingletonTargetSource(bean), the original object. The advice chain exists only inside the proxy, and the original object does not know it has been wrapped. So when the original object calls its own method through this, the proxy is not involved.
// An example written for this post (not Spring's code)
public static class TaskService {
public void outer() {
System.out.println(" [target] outer() runs, class of this: " + this.getClass().getSimpleName());
inner(); // this.inner() — does not go through the proxy
}
public void inner() {
System.out.println(" [target] inner() runs");
}
}
A @Before is placed on every method of TaskService, and outer() and inner() are each called once from outside.
== outer() called from outside
@Before outer()
[target] outer() runs, class of this: TaskService
[target] inner() runs
== inner() called from outside
@Before inner()
[target] inner() runs
Left: self-invocation. Right: a call from outside. The red box is where the advice goes missing.
The inner() called from inside outer() got no @Before. The line showing that the class of this is TaskService (the original, not the proxy) is the reason. Not covered in this post, but for this case there is the exposeProxy setting, which publishes the proxy itself in a ThreadLocal, and AopContext.currentProxy() (EnableAspectJAutoProxy line 137, AopContext lines 67–72).
If you want to go deeper
A lot was left out to keep the main thread. It is in my notes, but here only by name. The perthis and pertarget instantiation models, which create a separate aspect per target object (BeanFactoryAspectJAdvisorsBuilder lines 123–133). @DeclareParents, which adds interfaces (ReflectiveAspectJAdvisorFactory lines 157–163). Dynamic pointcuts that must look at argument values and so are re-matched on every call (isRuntime, AspectJExpressionPointcut lines 341–342; the InterceptorAndDynamicMethodMatcher branch of proceed() is for them). argBinding, which matches advice method parameter names against args() and the like in the pointcut (AbstractAspectJAdvice line 556 onwards). TargetSourceCreator, which builds a proxy with a custom TargetSource already in the before-instantiation hook (AbstractAutoProxyCreator lines 257–269). The CGLIB optimisation that pre-builds a fixed chain per method when the target is static and the configuration is frozen (CglibAopProxy lines 360–391). And the registration routes for InfrastructureAdvisorAutoProxyCreator, used by transactions and others without @EnableAspectJAutoProxy, and AspectJAwareAdvisorAutoProxyCreator, used by XML <aop:config> (AopConfigUtils lines 70–88).
Summary
| Concept | One-line summary |
|---|---|
@EnableAspectJAutoProxy |
Brings in a registrar via @Import that registers one AnnotationAwareAspectJAutoProxyCreator bean definition. There is one creator per container and it escalates to the more capable class |
| Auto-proxy creator | A BeanPostProcessor. In the "after initialisation" hook, wrapIfNecessary decides whether to wrap |
advisedBeans |
A per-bean cache of "did I wrap it?". Infrastructure classes and the aspect itself are FALSE |
| Candidate advisors | Beans of type Advisor plus the advisors built from @Aspect beans. All bean names are walked once and cached |
| Aspect → advisors | One advice method, one advisor. Methods are sorted Around → Before → After → AfterReturning → AfterThrowing |
| Deciding to wrap | canApply: wrap if the class filter passes and at least one method matches the pointcut. None at all, and the original stays |
| Sorting | @Order between aspects (smaller is further outside), kind order within one aspect. ExposeInvocationInterceptor at the very front |
| Advice chain | Only the advisors matching the called method become interceptors, cached per method. Advice that is not a MethodInterceptor is wrapped by an adapter |
proceed() |
A recursion that bumps an index and invokes each interceptor. Front to back on the way in, back to front on the way out |
@After and @AfterReturning |
@After sorts earlier, but being a finally it runs after @AfterReturning |
@AfterThrowing |
Calls the advice in a catch and rethrows with throw ex. It never swallows the exception |
| Self-invocation | The advice lives only in the proxy. this.inner() does not go through it |
What to read next
In the map from Part 1, the nine units of the main path (U01–U07, U09, U10) end with this post. The container reading blueprints, building beans and wiring them together, and AOP laying proxies on top: that is Spring's skeleton. From here on we move to the surroundings and the upper floors that assume it.
The next post, Part 12: Events, Environment, Resource — Three Abstractions Around the Container, looks at the last unit of Phase 1, which was postponed because it is not on the main path: how beans send and receive events, how configuration values are read through profiles and properties, and how files and classpath resources are handled through one shape, each abstracted behind its own interface.
- Part 1: How to Read the Source with AI, and the Analysis Map
- Part 4: How a Bean Gets Created — the Creation Flow and the Three-Level Cache
- Part 6: Lifecycle Callbacks and PostProcessors — Where to Cut into the Container
- Part 9: @Conditional, @Profile, @Import — Choosing Beans by Condition
- Part 10: How an AOP Proxy Gets Built — the JDK Way and the CGLIB Way