Spring AOP & Cross-Cutting Concerns

Pointcut Expressions

18 min Lesson 5 of 13

Pointcut Expressions

Knowing how to write an advice method is only half the story. The other half — and the part that separates maintainable aspects from fragile ones — is telling Spring exactly which join points that advice should intercept. That is the job of a pointcut expression. Master the expression language and you gain surgical control over your cross-cutting concerns without touching a single line of business code.

What a Pointcut Expression Actually Is

A pointcut expression is a string written in Spring AOP's expression language (borrowed from AspectJ). It is evaluated at context startup: Spring inspects every bean, resolves the expression against each method, and builds a proxy for any bean whose methods match. At runtime the proxy intercepts only the matching calls — everything else passes through without overhead.

You attach an expression either directly on an advice annotation or on a dedicated @Pointcut method, then reference that method by name:

@Aspect @Component public class AuditAspect { // Named, reusable pointcut @Pointcut("execution(* com.example.service.*.*(..))") public void serviceLayer() {} // body is always empty @Before("serviceLayer()") public void auditBefore(JoinPoint jp) { System.out.println("Calling: " + jp.getSignature().getName()); } }
Always declare shared pointcuts as named @Pointcut methods. If you paste the same expression string into five advice annotations, a one-character rename of a package breaks all five silently at startup — but only the one pointcut you forgot to update.

The execution() Designator

execution() is the workhorse of Spring AOP. It matches method execution join points based on a method signature pattern. The full grammar is:

execution( [modifiers-pattern] return-type-pattern [declaring-type-pattern.] method-name-pattern(param-pattern) [throws-pattern] )

Square brackets mark optional parts. In practice you will use three or four segments. Here is a progressive set of real examples, each more selective than the last:

// 1. Every public method of every class — broadest possible match execution(public * *(..)) // 2. Every method whose name starts with "find", anywhere execution(* find*(..)) // 3. Every method in the service package (not sub-packages) execution(* com.example.service.*.*(..)) // 4. Every method in the service package AND all sub-packages execution(* com.example.service..*.*(..)) // 5. Only methods named "save" that return void, anywhere execution(void save(..)) // 6. Specific method with exact parameter types (no wildcards) execution(* com.example.service.OrderService.placeOrder( com.example.model.Order, com.example.model.User)) // 7. Methods taking exactly one argument of any type execution(* *(*) ) // 8. Methods returning a List — note the fully qualified type execution(java.util.List<?> *(..) )

Pattern tokens you need to know:

  • * — matches any single segment (one package level, one type name, one method name, one return type). Does not span dots.
  • .. — in the package position matches any number of sub-packages; in the parameter position matches any number and type of parameters.
  • () — no arguments exactly.
  • (*) — exactly one argument of any type.
  • (..) — zero or more arguments of any type (most common).
Prefer specificity over breadth. An expression like execution(* *(..)) matches every method on every proxied bean, including infrastructure beans you did not intend to intercept. Start with the fully-qualified package and widen only as needed.

The within() Designator

within() restricts matching to join points within a type (class or set of classes). It does not care about method signatures — only where the method lives. This makes it the right choice when you want to intercept all activity inside a layer or a specific class, regardless of method names or return types:

// All methods in exactly this class within(com.example.service.PaymentService) // All methods in every class in the repository package within(com.example.repository.*) // All methods in the repository package and all sub-packages within(com.example.repository..*) // All classes annotated with @Service (Spring 6 / AspectJ annotation matching) within(@org.springframework.stereotype.Service *)

The last form — matching on a type-level annotation — is particularly powerful. Every class you annotate with @Service is automatically covered without listing package names. If you later add a service in a new package, the pointcut picks it up with zero changes.

execution() vs within() — Choosing the Right Tool

These two designators often overlap, but they are not interchangeable:

  • Use execution() when the signature is what matters — a specific return type, a naming convention like find* or save*, or a particular parameter list.
  • Use within() when the location is what matters — "intercept everything in the service layer" or "intercept everything in this specific class".
  • Use both combined when you need both constraints: a method name pattern within a specific package.
// Combined: only "find*" methods inside the repository package @Pointcut("execution(* find*(..)) && within(com.example.repository.*)") public void repositoryFinders() {}
Logical operators in pointcut expressions: use && (AND), || (OR), and ! (NOT) to compose expressions. In XML configuration you must write and, or, not because XML escapes the ampersand — but in annotations the Java string && works directly.

Annotation-Based Pointcuts with @annotation()

A common real-world pattern is to define a custom annotation and then write a pointcut that matches any method carrying that annotation. This gives library authors and framework designers a clean opt-in hook:

// 1. Define the marker annotation package com.example.annotation; import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Auditable {} // 2. Point at it from the aspect @Pointcut("@annotation(com.example.annotation.Auditable)") public void auditableMethods() {} @Before("auditableMethods()") public void recordAuditEvent(JoinPoint jp) { // runs only for @Auditable methods } // 3. Opt individual methods in @Service public class InvoiceService { @Auditable public Invoice generateInvoice(Order order) { ... } public Invoice fetchInvoice(Long id) { ... } // NOT intercepted }

This pattern is far more maintainable than package-based expressions when you need fine-grained control because the annotation travels with the method even if it moves packages.

Common Mistakes and How to Avoid Them

Spring AOP only intercepts Spring-managed beans. A pointcut expression that matches a method on a plain new-constructed object will never fire, because there is no proxy. Always inject your beans through Spring's IoC container.
  • Self-invocation is not intercepted. If OrderService.methodA() calls this.methodB() internally, the AOP proxy is bypassed for methodB. The fix is to inject the bean into itself (Spring 6 handles the circular dependency) or refactor into a separate bean.
  • Private and package-private methods are never matched by Spring AOP (it relies on JDK dynamic proxies or CGLIB, both of which only override public/protected methods). If your pointcut seems to not fire, check method visibility first.
  • The .. in a package pattern matches zero or more segments, so com.example.. matches com.example itself and any nested packages. This is a common source of accidentally broad matches.

Validating Your Expressions

Rather than guessing whether a pattern matches, write a quick integration test and log join points:

@Aspect @Component @Profile("dev") // only active in development public class PointcutDebugAspect { @Before("execution(* com.example..*.*(..)) && !within(PointcutDebugAspect)") public void logMatch(JoinPoint jp) { System.out.println("[AOP DEBUG] Matched: " + jp.getSignature()); } }

Gate it behind a Spring Profile so it never reaches production. Run the app, exercise your features, and examine the console output. If a method you expected to match does not appear — check visibility, bean management, and self-invocation.

Summary

execution() gives you signature-level precision: match on return type, method name, and parameter types. within() gives you location-level precision: match everything inside a type or package. Combine them with &&, ||, and !, and use @annotation() for opt-in interception. Name shared expressions with @Pointcut methods for reusability and keep expressions as specific as your use case warrants. In the next lesson you will apply these tools to the most flexible advice type: @Around.