namalzi

← All posts

Digging into Spring's Source 11: How @Aspect Gets Discovered and How the Advice Chain Runs

Spring · Published

What the single line @EnableAspectJAutoProxy registers in the container, how an @Aspect class is turned into advisors, how the container decides which beans to wrap in a proxy, and in what order @Around, @Before, @After, @AfterReturning and @AfterThrowing run when a proxied method is called — all checked against the real source and real example output.

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.

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);
		// ...
	}

spring-context/src/main/java/org/springframework/context/annotation/AspectJAutoProxyRegistrar.java lines 41–57

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.

The path from @EnableAspectJAutoProxy through AspectJAutoProxyRegistrar and AopConfigUtils to the internalAutoProxyCreator bean definition, which refresh() registers as a BeanPostProcessor so that it decides whether to wrap in every bean's after-initialisation hook 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.

The inheritance chain ProxyProcessorSupport, AbstractAutoProxyCreator, AbstractAdvisorAutoProxyCreator, AspectJAwareAdvisorAutoProxyCreator, AnnotationAwareAspectJAutoProxyCreator, with the methods each level is responsible for 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;
	}

spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java lines 321–345

The flow of wrapIfNecessary. If the bean is marked FALSE in advisedBeans or is an infrastructure class, the original is returned as-is; otherwise the applicable advisors are looked up, and if there is at least one, advisedBeans is set to TRUE and createProxy builds and returns a proxy Yellow is the advisedBeans cache, blue is the procedure, green is the end where the proxy becomes the bean.

Reading from the top:

  1. A bean already judged is done. advisedBeans is a map that remembers "did I wrap this bean?" (line 134). If it says FALSE, the advisors are not looked up again.
  2. Infrastructure classes are never wrapped. isInfrastructureClass is true when the bean is an Advice, Pointcut, Advisor or AopInfrastructureBean (lines 359–368). AnnotationAwareAspectJAutoProxyCreator adds "or an @Aspect class" (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.
  3. Look up the advisors that apply. getAdvicesAndAdvisorsForBean is abstract in this class (lines 611–612) and the next level implements it. If none applies it returns DO_NOT_PROXY, which is null (line 103), and the original stays the bean.
  4. If some apply, build the proxy. createProxy goes to buildProxy, which creates the ProxyFactory from Part 10, adds the advisors and calls getProxy (lines 440–494). The target is wrapped in a SingletonTargetSource: 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

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

spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAdvisorAutoProxyCreator.java lines 96–110

  1. Collect candidates (findCandidateAdvisors): gather every advisor in the container. This is the subject of this section.
  2. Keep the ones that apply (findAdvisorsThatCanApply): keep only those that can apply to this bean class. Section 4.
  3. Extend (extendAdvisors): add helper advisors if needed. The default implementation is empty (lines 175–176).
  4. Sort (sortAdvisors): decide the execution order. The default is AnnotationAwareOrderComparator, which looks at @Order and Ordered (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;

spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java lines 96–144

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

buildAspectJAdvisors walks every bean name looking for a type annotated with @Aspect; ReflectiveAspectJAdvisorFactory.getAdvisors sorts the advice methods and builds, for each method, an advisor that bundles a pointcut and the advice, then stores them in advisorsCache 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;
	}

spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java lines 168–175

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);
	}

spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java lines 80–95

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);
				// ...
			}
			// ...
		}

spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java lines 268–299

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

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

  1. First it compares with AnnotationAwareOrderComparator (lines 66–68). The order value of an advisor that came from an @Aspect comes from the @Order on the aspect class (or its Ordered implementation), and is LOWEST_PRECEDENCE if there is none (BeanFactoryAspectInstanceFactory lines 128–143, InstantiationModelAwarePointcutAdvisorImpl lines 180–182). Smaller comes first.
  2. 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 0 for all of them (lines 136–144), so this comparison always says "equal". The order within one aspect therefore stays as the kind order that getAdvisorMethods sorted 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.

The three steps the candidate advisors from section 3 go through. ① findAdvisorsThatCanApply asks canApply for each candidate: the ClassFilter filters the class, and the advisor is kept if any one method of the class or its interfaces matches. ② extendAdvisors puts ExposeInvocationInterceptor.ADVISOR at index 0 if there is any AspectJ advisor. ③ sortAdvisors uses AspectJPrecedenceComparator: between aspects by the class's @Order (none = last), and within one aspect by kind (Around, Before, After, AfterReturning, AfterThrowing). The example's resulting list is ExposeInvocationInterceptor, then FirstAspect @Order(1)'s @Before and @After, then SecondAspect @Order(2)'s @Before and @After. When called, it prints @Order(1) @Before → @Order(2) @Before → place() → @Order(2) @After → @Order(1) @After 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

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));
						// ...
					}
				}
			}
			// ...
		}

spring-aop/src/main/java/org/springframework/aop/framework/DefaultAdvisorChainFactory.java lines 69–109

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();
	}

spring-aop/src/main/java/org/springframework/aop/framework/adapter/MethodBeforeAdviceInterceptor.java lines 54–58

	@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;
	}

spring-aop/src/main/java/org/springframework/aop/framework/adapter/AfterReturningAdviceInterceptor.java lines 54–59

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);
		}
	}

spring-aop/src/main/java/org/springframework/aop/framework/ReflectiveMethodInvocation.java lines 154–181

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 for a greet call on the proxy: ExposeInvocationInterceptor, AspectJAroundAdvice, MethodBeforeAdviceInterceptor, AspectJAfterAdvice, AfterReturningAdviceInterceptor and AspectJAfterThrowingAdvice in turn, reaching the original greet at the end 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

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

Calling outer on the proxy from main runs the advice and then the original outer; the original outer calling this.inner bypasses the proxy, so there is no advice. Calling inner on the proxy directly from main runs the advice 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.

More posts