@AspectJ in Full: Pointcut Expressions and Five Advice Types
Plain words first. Your business code keeps growing the same pile of chores that have nothing to do with business: logging, timing, permission checks, alerts. Left alone, that code has to be hand-copied into every method — dozens of methods, dozens of copies, and one rule change means a project-wide hunt. An aspect does exactly one thing: pull those repeated chores out of the methods, write them once, and let the container slip them around the call at runtime. The business method does not change a single character and never learns it was wrapped.
To use this you must separate five terms. A join point is the moment of one real method invocation; a pointcut is an expression that decides "which join points are mine"; advice is the extra code you actually want to run; an aspect is the class packaging pointcut + advice; weaving is the act of attaching that advice onto the call path. Their relationship in one sentence: an aspect = where to intercept (pointcut) + what to do once intercepted (advice), and weaving connects them to the call.
putting a case on a phone explains advice and aspects. The phone (your business object) is never opened or modified; slide a case (the aspect) on and it gains shock protection, a kickstand and a card slot — features the phone maker never shipped, yet present on every unit of that model. @Before is the lens ring on top, @Around is the full bezel wrapping the whole device. You can peel the case off anytime and the phone is untouched. Two costs to remember: buttons feel mushy through the case (performance), and a bare phone you new yourself cannot take a case at all.
a parcel sorting centre explains pointcut evaluation. When a parcel (a method call) arrives, the machine reads three screens to pick its chute: first city and compound (which package, which class), then street number and recipient (method name, return type), finally parcel size (the argument list). Every screen must agree — matching only the city ships nothing. The sinister part of a wrong pointcut: a mis-sort never errors, the parcel just quietly never enters a chute, which is the headline symptom in Section 7's table.

After this article you should be able to answer three questions:
- What does each symbol in
execution( com.example.service...*(..))control, and what breaks if you drop one dot? - Which advice enters first and leaves last among the five, and why can only
@Aroundrewrite the return value or swallow an exception? - What are the four usual causes of a silently dead aspect, and what is the first thing to look at so you know it is not your imagination?
Before writing an aspect you must flip the switch. In plain Spring it is usually this line:
@Configuration@EnableAspectJAutoProxypublic class AopConfig {}@EnableAspectJAutoProxyregisters a component namedAnnotationAwareAspectJAutoProxyCreator- That component is a
BeanPostProcessor(more precisely, also aSmartInstantiationAwareBeanPostProcessor). Its job: after a bean finishes initialization, decide whether it is a target of any aspect, and if so build a proxy for it - The
AutoProxyin its name says it: you never create the proxy; it happens behind the scenes — which is why, once aspects are active, what you get at runtime is usually a proxy
Tip: under Spring Boot you normally need zero configuration. When AopAutoConfiguration detects AspectJ support on the classpath it auto-enables @AspectJ auto-proxying (properties spring.aop.auto=true and spring.aop.proxy-target-class default to true). You just annotate classes with @Aspect and @Component.
Pointcuts are the part that needs the most practice. The core is execution, whose full syntax is best remembered as a skeleton:
execution( modifier? returnType declaringType?.methodName(params) throws? )Only "return type, method name, parameters" are required; the rest can be omitted or replaced with * and ... These five examples cover the vast majority of everyday cases:
// Example 1: any method of any class in the service package, any return type, any args@Pointcut("execution(* com.example.service.*.*(..))")// Example 2: only public methods of UserService@Pointcut("execution(public * com.example.service.UserService.*(..))")// Example 3: void return type, method name starts with save@Pointcut("execution(void com.example.service.*.save*(..))")// Example 4: com.example and all subpackages, class name ends with Service@Pointcut("execution(* com.example..*Service.*(..))")// Example 5: exactly findById with a Long parameter@Pointcut("execution(* com.example.service.UserService.findById(Long))")- Example 1:
matches "any return type", the first.matches "any class in that package", and(..)means "any number of any args" - Example 2: adds the
publicmodifier; a hard-coded class name targets just that one class - Example 3:
save*is a prefix wildcard, sosave,saveOrderandsaveAllall match - Example 4: the double dots in
com.example..mean "this package and all subpackages" — the key to "why doesn't my pointcut reach subpackages?" - Example 5: writing a concrete type
Longrequires an exact signature match, whereas(..)matches any args
Beyond execution there are designators for "lazier" scenarios; they differ in the dimension they match:
| Designator | Matches on | Example | When to use |
|---|---|---|---|
within | Type / package | within(com.example.service..*) | Coarse interception of a whole package or class |
this | Proxy type | this(com.example.UserService) | Test which type the generated proxy is |
target | Target type | target(com.example.UserService) | Test the original target's type |
args | Method arguments | args(Long, ..) | Match by arg type/count and bind args |
@annotation | Method annotation | @annotation(com.example.anno.Log) | Intercept only methods carrying an annotation |
@within | Type annotation | @within(com.example.anno.Loggable) | Intercept all methods of annotated classes |
this and target are the easiest to confuse. this tests whether the proxy is of a type, while target tests whether the original target object is. They diverge with introductions; in everyday business code they are mostly equivalent, but interviews love this distinction.
Several pointcuts can be joined with logical operators, reading almost like prose:
// Methods in the service package AND carrying @Log@Pointcut("execution(* com.example.service..*(..)) && @annotation(com.example.anno.Log)")// Exclude internals: not the methods under com.example.service.internal@Pointcut("execution(* com.example.service..*(..)) && !within(com.example.service.internal..*)")// Either condition@Pointcut("@annotation(com.example.anno.Log) || @within(com.example.anno.Loggable)")&&is intersection,||is union,!is complement — note these are not Java's short-circuit operators but the pointcut combination syntax&&binds tighter than||; when mixing them, always add parentheses, or you will get surprising results!within(...)is the most useful exclusion trick: circle a package first, then subtract what you do not need, instead of enumerating class names
Trap: the difference between .. and is the most common slip. com.example.service. matches one level only, while com.example.service..* matches all levels. If a pointcut stubbornly misses classes in a subpackage, first check whether you dropped a dot.
Advice is the code inserted at the join points a pointcut matches. Spring AOP offers five, and the crux is their "permissions":
| Advice | Change args | Change return | Swallow exception | Typical use |
|---|---|---|---|---|
@Before | No | No | No | Argument validation, call logging |
@AfterReturning | No | No (read only) | No | Log success, post-call statistics |
@AfterThrowing | No | No | No (catch but not swallow) | Error logging, alerting |
@After | No | No | No | Resource cleanup (both paths) |
@Around | Yes | Yes | Yes | Transactions, caching, timing, retries |
In one line: only @Around is the all-rounder. It receives a ProceedingJoinPoint, so it can change arguments, decide whether to invoke the target, rewrite the return value, or even swallow exceptions. The other four can only "observe" and "record".
That sentence is what this picture argues: the left side may look at everything and touch nothing, the right side can touch everything and therefore owns the consequences. The selection rule is never "which one is more advanced" but "does this job need to change the outcome":

How does an advice method get "information about the current call"? Through parameter binding. Here is a complete aspect class:
@Aspect@Componentpublic class LogAspect { private static final Logger log = LoggerFactory.getLogger(LogAspect.class); @Pointcut("execution(* com.example.service..*(..))") public void serviceMethods() {} @Before("serviceMethods()") public void before(JoinPoint jp) { log.info("calling {}.{}", jp.getSignature().getDeclaringTypeName(), jp.getSignature().getName()); } @AfterReturning(pointcut = "serviceMethods()", returning = "result") public void afterReturning(JoinPoint jp, Object result) { log.info("{} returned: {}", jp.getSignature().getName(), result); } @AfterThrowing(pointcut = "serviceMethods()", throwing = "ex") public void afterThrowing(JoinPoint jp, Throwable ex) { log.error("{} threw: {}", jp.getSignature().getName(), ex.getMessage()); } @After("serviceMethods()") public void after(JoinPoint jp) { log.info("{} finished", jp.getSignature().getName()); } @Around("serviceMethods()") public Object around(ProceedingJoinPoint pjp) throws Throwable { long start = System.nanoTime(); try { return pjp.proceed(); // must be called, or the target never runs } finally { log.info("{} took {}ms", pjp.getSignature().getName(), (System.nanoTime() - start) / 1_000_000); } }}JoinPointis the common entry for all non-around advice; it exposes the signature, args and target@AfterReturning(returning = "result")binds the return value to the parameter namedresult— the names must match exactly@AfterThrowing(throwing = "ex")binds the thrown exception to the parameterex@AroundusesProceedingJoinPoint, whoseproceed()is the gate that "lets the call through" to the target
Key point: the names in returning / throwing are parameter names. A typo will not fail compilation but will fail binding at startup or runtime. Keep the names aligned and read carefully.
Here is how that name-matching actually happens, in six frames. Watch frame ④: the string in the annotation and the parameter names are compared one by one — no expression language is involved at all, it is literally names:

Because no expression language is involved here, plenty of beginners assume returning = "result" and the #{} inside @Cacheable(key = "#user.id") are the same syntax, and then write horrors like returning = "#result", which fail in silence. That other thing is SpEL, a separate language: it has its own root object (#root / #this), collection selection and projection, ternary and safe-navigation operators, and completely different failure messages. The last mode shows the three classic miswrites side by side:
With all advice attached to one method, what is the order? Run it once and the output is clear:

@Around before@Before==== target method runs ====@AfterReturning@After@Around after / return- The first half of
@Aroundenters first and its second half leaves last, like a pair of parentheses wrapping the other advice - On a successful return it is
@AfterReturning; the moment an exception is thrown,@AfterReturningis replaced by@AfterThrowing @Afterruns on both paths, making it ideal for cleanup
When several aspects match the same method, order is decided by @Order:
@Aspect@Order(1) // smaller number = more "outer"@Componentpublic class AuthAspect { /* authorization, should run first */ }@Aspect@Order(2)@Componentpublic class LogAspect { /* logging, runs after authorization */ }- Smaller
@Order= more outer:AuthAspect's@Beforeruns first and its@Afterruns last - Without
@Order, the order is undefined — never rely on source order - To make "a failed auth short-circuit and skip subsequent logging", put the auth aspect on the outer ring
"My aspect is written but never runs" is the number one aspect problem. Walk this table line by line and you will locate it eighty percent of the time:
| Symptom | Cause | Fix |
|---|---|---|
| Works when called by another bean, not via internal self-call | Self-invocation bypasses the proxy | Extract to another bean, or inject a self-proxy |
| Advice never fires on private / final methods | Such methods cannot be proxied | Make them public and non-final; or use AspectJ |
| Starts cleanly but nothing is intercepted | Wrong pointcut (package level, signature) | Turn on debug logging and check matching |
The target is an object you newed | Non-container beans get no proxy | Hand it to the container (@Component / @Bean) |
ClassCastException when casting to the impl | JDK proxy received as an implementation class | Inject by interface, or enable proxyTargetClass |
The first tool is always logging: set logging.level.org.springframework.aop=DEBUG and the container prints which aspects and proxy type each bean received, with a "does not match" reason when a pointcut misses.
if an @Around advice never calls pjp.proceed(), the target method never executes — yet nothing errors, it just "does not happen". Such a bug is brutal to trace, because everything before and after looks normal and only the business logic vanished. Treat proceed() as mandatory inside @Around, and wrap it in try/finally so post logic always runs and exceptions always propagate.
The demo below visualizes the advice chain's execution order. Switch to the "exception path" and run it again to see how @AfterThrowing replaces @AfterReturning and how @After runs on both paths:
That one shows how the proxy forwards; the ordering table in Section 6 deserves a demo that is about ordering itself. advices hangs all five advice types on one method — run normal, then error, and the difference between the two paths shows up immediately:

Everything above was prose; this section is all clickable. The four demos follow "see the symptom, then the mechanism, then the order", and each lets you switch parameters.
The first answers the parcel sorting analogy from Section 0: the same package text means different things inside execution and within. Pick "execution parts" to watch segment-by-segment matching; pick "Why nothing matched" for the five causes of silent misses — the one beginners stall on hardest, because it raises no error at all:
Animation is still too quick for "check it slot by slot". Spread the same evaluation over a stepping bench: a candidate method and one expression on the left, "which slot is being judged right now, and with what verdict" on the right. Press next and watch step ⑤ — the whole expression has already short-circuited here; the last two slots were never even evaluated:
String c = "com.example.service.impl.UserServiceImpl.save(User)"; // (1) the candidate method@Pointcut("execution(* com.example.service.*.*(..))") // (2) the expression you wroteslot 1 return type * -> any return type counts as a hit // (3)slot 2 declaring type com.example.service.* // (4) one star eats direct children only; impl is a grandchild -> false // (5)slot 3 method name * -> never reached at all // (6)slot 4 argument list (..) -> never reached either // (7)@Pointcut("execution(* com.example.service..*.*(..))") // (8) fix: one dot becomes two| candidate | UserServiceImpl#save(User) |
| declaring type | com.example.service.impl.UserServiceImpl |
| pointcut | execution(* com.example.service.*.*(..)) |
matchOrNotevaluate executionThe second settles "does the annotation go on the method or the class". @annotation and @within differ by one word yet behave oppositely, and this branch also names an invisible killer: Java does not inherit method annotations onto the implementing class, so annotating only the interface method usually matches nothing:
The third is the before-versus-after weaving contrast: the very same userService.save("Alice") call has one stack frame and a body mixed with logging, try/catch and timing when no aspect exists; once woven, three cross-cutting layers wrap it while the business code is unchanged by a single line. Switch to "What it costs" to see the price of that case (slower startup, and four cases nothing can advise):
The fourth resolves the question left open at the end of Section 6: when several aspects hit one method, who sits outside. "Nested timeline" lists the full chain step by step (Around1-before → Around2-before → Before → target → AfterReturning → After → Around2-after → Around1-after); "Reversed order" shows why forgetting @Order turns into "fine on my laptop, red in CI":

Four demos done; what remains is turning "read an expression" into "say instantly who it catches". The table in Section 2 sorts designators by dimension; this round trains something different — expression in, coverage out — and since no two entries on either side repeat, position tricks will not save you:
From here, type the commands yourself. This console talks to the same container running in your browser, and every line of output is computed by the kernel — boot, then beans to see who got a shell, and lab pointcut nomatch is the full version of the stepping bench above:
On the left, change the four slots one at a time — return type, package depth, method name, argument list; on the right you immediately get which methods this matches and why the rest are missed. Choosing the "dropped a dot" and "() mistaken for (..)" cells once by hand beats memorising ten rules:
execution(* com.example.service..*.*(..))Match: save(User) / findById(Long) / deleteAll() — impl sub-package includedCoverage: every proxyable public method of those classes
the one-star cell is where most accidents happen. Rule of thumb: **a single between dots eats exactly one segment*, while .. eats "this package and every descendant"; and the .. inside the argument parentheses means something else entirely — "any number of arguments of any type". One symbol, two meanings depending on position, is the hardest part of execution to memorise, and the origin of row three in Section 13's table.
Every "error text" below can be pasted verbatim into a search box — do not paraphrase it:
| Error text (fragment) | Real cause | 30-second rescue | Read deeper in | ||
|---|---|---|---|---|---|
| No exception at all, advice simply never runs (symptom: not one log line) | A pointcut evaluating to false is a legitimate outcome; Spring concludes "you should not be intercepted" and quietly does nothing | Temporarily widen to @Around("execution( (..))"): if that fires, your expression is the culprit; then set logging.level.org.springframework.aop=TRACE and count candidate advisors | This article, Section 11 pointcut demo · #14 AOP internals | ||
Invalid binding type: long. Method: public void ...afterThrowing(long) / ConflictingAdviceReqArgException | The name in returning = "result" / throwing = "ex" does not equal the parameter name, or two parameters of the same type are bound in one advice | Copy-paste the parameter variable name into the annotation instead of typing it; a Throwable binding must be the last parameter | Section 5 · #15 AOP practice | ||
IllegalArgumentException: error at anonymous pointcut expression around this / Can't parse advice pointcut designator | Syntax error in the pointcut string: unbalanced parentheses, a dangling &&, or an annotation written as a simple name instead of its fully qualified name | Check three things: brackets balanced, @annotation(...) holds a fully qualified class name, and parentheses added wherever && and ` | ` mix | Section 3 | |
Cannot find class [com.example.anno.Log] for annotation [@Log] | The annotation class referenced by the pointcut is not compiled, lives in another module, or its package is misspelled | Copy the annotation's fully qualified name from the IDE and paste it into the pointcut string — never type it by hand | Section 12 sandbox · #2 Maven | ||
ClassCastException: class com.sun.proxy.$Proxy42 cannot be cast to class com.example.service.UserServiceImpl | The target has an interface so a JDK proxy was built, but the injection point is declared as the implementation class | Inject by interface type; or force CGLIB: spring.aop.proxy-target-class=true under Boot (already the default), @EnableAspectJAutoProxy(proxyTargetClass = true) in plain Spring | #12 Dynamic proxy · #14 AOP internals | ||
Advice on a private / final / static method never fires, again without an error | Proxies cannot see private methods, CGLIB cannot override final ones, static calls do not belong to an instance | Make it a public non-final instance method; if you truly need private-method interception, switch to AspectJ compile-time weaving | Section 7 · #14 AOP internals section 7 | ||
No qualifying bean of type 'com.example.aspect.LogAspect' available | @Aspect is only a descriptor — it does not make the class a bean; without @Component (or a @Bean registration) the creator never finds it | Add @Component and confirm component scanning covers it; when findCandidateAdvisors() returns an empty list, every aspect goes silent at once | #14 AOP internals section 6 checklist | ||
Advices vanish when a method is called internally via this.method() | this is the raw object, not the proxy, so the call never enters the advice chain | Extract to another bean; or enable exposeProxy = true and use AopContext.currentProxy(); or inject your own proxy | #12 Dynamic proxy · #15 AOP practice | ||
Classes in sub-packages never match (com.example.service..(..) is useless) | A single * matches one level only, so impl sits outside it | Change the package slot to com.example.service...(..) — two dots | Section 3 trap · Section 12 sandbox, first cell | ||
After writing () instead of (..), every method with arguments stops matching | () demands exactly zero arguments; (..) is what means "any number of any type" | Memorise: (..) any / () must be empty / (*) exactly one / (Long) exactly one Long | Section 2 example 5 · Section 12 sandbox |
mixing up and .. is the shared root of the two most frequent rows above. eats exactly one segment (one package name, one type name, one argument), while .. in the type slot eats "any depth of sub-package" and in the argument slot eats "any number of any arguments". So com.example..Service.(..) reads: under com.example at any depth, every class whose name ends with Service, all methods, any arguments. For long expressions, say each segment out loud instead of trusting your gut.
Row one of that table fails in silence, but a malformed pointcut has the opposite fate: it explodes in your face, with a stack whose upper frames belong to Spring while the actual culprit sits inside AspectJ's parser. Practise on it:
You only edited the package segment of one execution expression, and now the application refuses to start. The message names neither your class nor the offending character.
Goal: in three minutes, produce a complete aspect whose logs you can actually see, confirming AOP works in your project. Drop these files into a fresh Spring Boot project (the build needs spring-boot-starter-aop, or at least spring-boot-starter plus org.aspectj:aspectjweaver).
package com.example;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.ConfigurableApplicationContext;import org.springframework.stereotype.Service;@SpringBootApplicationpublic class AspectDemoApplication { public static void main(String[] args) { try (ConfigurableApplicationContext ctx = SpringApplication.run(AspectDemoApplication.class, args)) { UserService svc = ctx.getBean(UserService.class); svc.save("Alice"); System.out.println("what we actually got: " + svc.getClass().getName()); } }}@Serviceclass UserService { public void save(String name) { System.out.println("==== target method runs: saving " + name + " ===="); }}package com.example.aspect;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;@Aspect@Component // drop this line and the aspect is never discoveredpublic class TraceAspect { private static final Logger log = LoggerFactory.getLogger(TraceAspect.class); @Pointcut("execution(* com.example..*Service.*(..))") public void serviceMethods() {} @Before("serviceMethods()") public void before(JoinPoint jp) { log.info("[BEFORE] {}", jp.getSignature().toShortString()); } @Around("serviceMethods()") public Object around(ProceedingJoinPoint pjp) throws Throwable { long start = System.nanoTime(); log.info("[AROUND-IN] {} args={}", pjp.getSignature().getName(), java.util.Arrays.toString(pjp.getArgs())); try { return pjp.proceed(); // mandatory: without it the target never runs } finally { log.info("[AROUND-OUT] {} took={}ms", pjp.getSignature().getName(), (System.nanoTime() - start) / 1_000_000); } } @AfterReturning(pointcut = "serviceMethods()", returning = "result") public void afterReturning(JoinPoint jp, Object result) { log.info("[RETURNED] {} -> {}", jp.getSignature().getName(), result); } @After("serviceMethods()") public void after(JoinPoint jp) { log.info("[AFTER] {}", jp.getSignature().getName()); }}Expected output (the ==== line comes from System.out, the rest from slf4j, whose prefix carries the timestamp and logger name):
[AROUND-IN] save args=[Alice][BEFORE] UserServiceBean.save(..)==== target method runs: saving Alice ====[RETURNED] save -> null[AFTER] save[AROUND-OUT] save took=1mswhat we actually got: com.example.UserService$$SpringCGLIB$$0[AROUND-IN]precedes[BEFORE]: the front half of@Aroundis the outermost parenthesissave -> null: the target isvoid, so the value bound by@AfterReturningis naturallynull- The last line shows
$$SpringCGLIB$$: what you fetched from the container is already the proxy, not the object you constructed
Change exactly one thing each time and read the conclusion:
- Turn the pointcut into
com.example..Service.(..)(drop one dot). You will observe:UserServicestill matches while it sits directly incom.example, but the moment it moves intocom.example.service.implevery advice dies, silently. Compare with theone-starcell of Section 12 and "a singleeats one level" becomes obvious. - Delete
return pjp.proceed();from the@Aroundand keep only the logging. You will observe: the==== target method runs ====line disappears entirely and the result becomesnull, while the program throws nothing at all. That is the live version of the second quiz in Section 14. - Add a second
MetricAspectonUserService#savewith@Order(2)(same body, prefix[METRIC]) and mark TraceAspect@Order(1). You will observe the sequenceAROUND-IN(Trace) → AROUND-IN(Metric) → BEFORE → target → AFTER → AROUND-OUT(Metric) → AROUND-OUT(Trace); swap the two numbers and both entry and exit flip as a block. Now delete both@Orderannotations and restart a few times — the order begins to drift, which is precisely how ghost bugs are bred. - Make
saveprivate. You will observe: all advice vanishes again, still with no error. See the proxy boundaries in #14 AOP internals.
Build a slow-call attribution aspect: not just timing, but tiered by threshold and aggregated.
- Define
@SlowWatch(value = "place order", warnMs = 200, errorMs = 1000)with@Target(METHOD)and@Retention(RUNTIME) - Write
@Around("@annotation(slowWatch)"):INFOon the normal path,WARNpastwarnMs,ERRORpasterrorMs, always including method name, the annotation's value, elapsed time and argument count - Accumulate per-method call counts and total time in a
ConcurrentHashMap<String, LongAdder>, and print a "Top 3 slowest methods" summary (via@Scheduledor on shutdown) - Never swallow: catch, record a failure count, then rethrow the original
Acceptance checklist: ① drive one method to 50ms / 300ms / 1200ms and see INFO / WARN / ERROR respectively; ② on an exception the caller still receives the original throwable (verify with @AfterThrowing or a try/catch that the stack was not replaced); ③ annotate a private method with @SlowWatch and explain in your own words why nothing happens without guessing; ④ remove @Component from the aspect and confirm business output is byte-for-byte identical — proof it really is only a case slipped over the phone.
without looking, name the five slots of execution( com.example.service...*(..)) from left to right, and say which two may be omitted.
what do (..), (), (*) and (Long) each match, and which one is most often confused with the first?
which element does @annotation(com.example.anno.Log) inspect versus @within(com.example.anno.Log)? What happens if the annotation only sits on the interface method?
in one successful call, what is the execution order of the five advice types, and which one gets substituted when an exception is thrown?
what happens when two aspects carry no @Order at all, and why is that called a bug that only reproduces on another machine?
Mantra: **the pointcut says where to grab, the advice says what to do once grabbed, `@Around` must call proceed, `..` eats sub-packages while `*` eats one level.**
this section walked through the spots that trip up aspect development — the execution skeleton is "modifier returnType class?.method(params) throws?", where .. and * decide matching depth; within / this / target / args / @annotation / @within each match a different dimension; among the five advice types only @Around can change args, change the return value, and swallow exceptions; in ordering, @Around acts like parentheses around the other four, and multiple aspects queue by @Order (smaller is more outer). Two iron rules to finish: @Around must call proceed(), and when an aspect does not fire, check the four usual suspects — self-invocation, private/final methods, a wrong pointcut, and non-container objects.