Controlling Execution Order in Multi‑Layer Spring AOP Aspect Nesting
When a Spring project uses more than two AOP aspects, the resulting multi‑layer nesting can cause sensitive data leaks, excessive logging, missed alerts, inconsistent responses, and random bugs; this article explains why ordering issues occur, how the onion model works, and provides concrete configuration strategies to achieve deterministic execution order.
Problem Overview
When a project contains more than two Spring AOP aspects—such as rate‑limit, blacklist, token authentication, trace, logging, timing, data‑masking, and unified response aspects—multi‑layer nesting issues appear. Typical symptoms are:
Unauthenticated requests are fully logged, causing sensitive data leakage .
Rate‑limit aspect runs after logging, generating massive log output and disk pressure.
Exception‑alert aspect runs late, resulting in missed alerts .
Response‑wrapping aspect executes before logging, so logs contain stale return values .
Local tests always show correct order, but after a production restart the order becomes random, leading to intermittent bugs .
Spring AOP Multi‑Aspect Model
Why order becomes an issue
Spring AOP builds the execution chain with dynamic proxies + responsibility chain . When multiple aspects match the same method, Spring assembles them into a chain. Without explicit ordering, Spring uses the bean loading order, which differs between IDE runs and packaged JARs, making the startup order effectively random and the root cause of production‑only bugs.
@Order priority rules
Smaller numeric value → higher priority → placed on the outer layer.
Larger numeric value → lower priority → placed on the inner layer.
Default when @Order is absent: Integer.MAX_VALUE (lowest priority, innermost).
Same @Order value: execution order is uncontrolled, determined by container loading order.
Onion model of nested aspects
Execution follows an “outer‑in‑inner‑out” pattern: outer layers execute first and finish last (stack‑like), inner layers execute later and finish earlier.
@Aspect
@Component
@Order(1)
public class AuthAspect { ... }Assume two aspects: Aspect A with @Order(1) (outer) and Aspect B with @Order(10) (inner). The execution chain mnemonic is:
Outer @Around before → Outer @Before → Inner @Around before → Inner @Before → Business method → Inner @AfterReturning → Inner @After → Inner @Around after → Outer @AfterReturning → Outer @After → Outer @Around after
Strict order of five notifications (no exception)
Outer @Around before
Outer @Before
Inner @Around before
Inner @Before
Business method execution
Inner @AfterReturning
Inner @After
Inner @Around after
Outer @AfterReturning
Outer @After
Outer @Around after
Exception flow
Outer @Before → Inner @Before → Business throws
Inner @AfterThrowing catches first
Inner @After executes
Inner @Around catch block processes or re‑throws
Outer @AfterThrowing executes
Outer @After executes last
Key conclusion: inner aspects perceive exceptions first and perform cleanup before outer aspects provide final fallback handling.
Configuring Priority
Method 1: Static @Order annotation
Zero‑intrusion, team‑wide convention.
@Aspect
@Component
@Order(1)
public class AuthAspect { /* high‑priority outer aspect */ }Method 2: Ordered interface for dynamic priority
Suitable for environment‑specific or switchable priorities. The Ordered interface’s order overrides @Order.
@Aspect
@Component
public class LimitAspect implements Ordered {
@Override
public int getOrder() {
// Production highest priority, test lower priority
return "prod".equals(getEnv()) ? -100 : 999;
}
}Full request chain
Filter → Spring MVC Interceptor → AOP outer aspect → AOP inner aspect → Controller
Three‑layer aspect example
Layer planning
Outer: Permission check aspect @Order(1) Middle: Rate‑limit/blacklist aspect @Order(5) Inner: Logging & timing aspect
@Order(10)Outer permission aspect
@Aspect
@Component
@Order(1)
public class AuthAspect {
@Pointcut("execution(* com.demo.controller.*.*(..))")
public void point() {}
@Before("point()")
public void before() { System.out.println("[Outer-Permission-Before]"); }
@Around("point()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("[Outer-Permission-Around before]");
Object res;
try { res = pjp.proceed(); }
catch (Throwable e) { System.out.println("[Outer-Permission-Catch]"); throw e; }
System.out.println("[Outer-Permission-Around after]");
return res;
}
@AfterReturning(value = "point()", returning = "r")
public void afterReturn(Object r) { System.out.println("[Outer-Permission-AfterReturning]"); }
@After("point()")
public void after() { System.out.println("[Outer-Permission-After]"); }
}Middle rate‑limit aspect
@Aspect
@Component
@Order(5)
public class LimitAspect {
@Pointcut("execution(* com.demo.controller.*.*(..))")
public void point() {}
@Before("point()")
public void before() { System.out.println("[Middle-Limit-Before]"); }
@Around("point()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("[Middle-Limit-Around before]");
Object res;
try { res = pjp.proceed(); }
catch (Throwable e) { System.out.println("[Middle-Limit-Catch]"); throw e; }
System.out.println("[Middle-Limit-Around after]");
return res;
}
@AfterReturning(value = "point()", returning = "r")
public void afterReturn(Object r) { System.out.println("[Middle-Limit-AfterReturning]"); }
@After("point()")
public void after() { System.out.println("[Middle-Limit-After]"); }
}Inner logging aspect
@Aspect
@Component
@Order(10)
public class LogAspect {
@Pointcut("execution(* com.demo.controller.*.*(..))")
public void point() {}
@Before("point()")
public void before() { System.out.println("[Inner-Log-Before]"); }
@Around("point()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("[Inner-Log-Around before]");
long start = System.currentTimeMillis();
Object res;
try { res = pjp.proceed(); }
catch (Throwable e) { System.out.println("[Inner-Log-Exception]"); throw e; }
long cost = System.currentTimeMillis() - start;
System.out.println("[Inner-Log-Around after] cost:" + cost + "ms");
return res;
}
@AfterReturning(value = "point()", returning = "r")
public void afterReturn(Object r) { System.out.println("[Inner-Log-AfterReturning] result:" + r); }
@After("point()")
public void after() { System.out.println("[Inner-Log-After]"); }
}Test controller
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/demo")
public String demo() {
System.out.println("===== Business method execution =====");
return "success";
}
}Full execution log
[Outer-Permission-Around before]
[Outer-Permission-Before]
[Middle-Limit-Around before]
[Middle-Limit-Before]
[Inner-Log-Around before]
[Inner-Log-Before]
===== Business method execution =====
[Inner-Log-AfterReturning] result:success
[Inner-Log-After]
[Inner-Log-Around after] cost:1ms
[Middle-Limit-AfterReturning]
[Middle-Limit-After]
[Middle-Limit-Around after]
[Outer-Permission-AfterReturning]
[Outer-Permission-After]
[Outer-Permission-Around after]Aspect priority specification
Blacklist / Illegal request interception – order range -100 ~ -1 – outermost – intercept early, prevent any resource waste.
Token login / Permission check – order range 1 ~ 10 – outer – validate before rate‑limit, block unauthorized traffic.
TraceID chain tracking – order range 11 ~ 20 – outer‑middle – generate trace ID early for unified logging.
Rate‑limit / Idempotent submission – order range 21 ~ 30 – middle – limit after permission passes.
Interface timing / performance monitoring – order range 31 ~ 50 – middle‑inner – measure full‑chain latency including pre‑checks.
Operation log / request‑response record – order range 51 ~ 100 – inner – log only successful business calls.
Data masking / unified response – order range 100+ – innermost – finalize response after all processing.
Common pitfalls & solutions
Missing @Order : local tests pass, but production restarts cause random log disorder and permission failures. Solution: force every aspect to declare @Order; never rely on the default.
Inner aspect swallows exception : logging aspect catches but does not re‑throw, causing outer alert and global exception handlers to miss the error. Solution: inner aspects must re‑throw after logging.
Outer response wrapper changes return value : logs show stale data because inner @After runs before outer @After. Solution: place response‑wrapping aspect outermost, logging innermost.
Identical pointcuts : multiple aspects execute repeatedly, leading to duplicate logs, DB writes, alerts. Solution: narrow pointcut definitions or use ThreadLocal flags to prevent re‑execution.
@AfterThrowing ineffective with @Around : @Around’s try‑catch consumes exception, so @AfterThrowing never runs. Conclusion: if @Around handles exceptions, the exception notification chain is considered cleared.
Same @Order value across team : order collisions cause unpredictable execution. Rule: assign each aspect category a distinct, non‑overlapping range.
Final Takeaway
Understanding the onion‑style execution chain, correctly configuring @Order (or implementing Ordered), and adhering to a unified priority scheme eliminates nondeterministic behavior, prevents hidden bugs, and ensures stable, maintainable AOP in production systems.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
