skip to content

When architecting pointcuts across a large codebase, how do within(), this() and target() differ in matching cost and expressiveness, and how would you combine them?

level: principalimportance: should knowfreq 20%

answer

  1. static tier: within/execution — free per call
  2. dynamic tier: this/target/args — can bind
  3. narrow cheap first, then dynamic test
  4. marker interfaces over package coupling
  5. target() over this(); document proxy policy

basics

~20 s

within() is a static, type/package match resolved at proxy creation — cheap, coarse, no binding. this()/target() are dynamic type tests on the proxy/target, can bind objects, but are per-call in the general case. Scope broadly and cheaply with within()/execution(), then AND a dynamic this()/target() only where you need object identity or binding.

solid answer

~50 s

Think in two tiers. Static PCDs — within() (declaring type/package) and execution() (method signature) — are resolved from type metadata at proxy-creation time, so they cost nothing per invocation and even decide whether a bean gets proxied at all. Dynamic PCDs — this(), target(), args() — test object/argument types and can bind them into advice; in the general case they imply a runtime check per call, though Spring often statically resolves them when the type is known. Architecturally: use within()/execution() to scope an aspect to a module or method shape cheaply, and add this()/target() only when you need the proxy/real object identity, a marker-interface capability match, or binding. Prefer target() over this() for concrete classes to stay proxy-agnostic, and standardise on marker interfaces so pointcuts read as intent (target(Auditable)) rather than package coupling. Keep expressions few, named via @Pointcut, and centralised.

code

java · 15 lines
java
@Aspect @Component
class PointcutLibrary {
    // Static, cheap scoping — decided at proxy creation.
    @Pointcut("within(com.example..service..*)") void inServices() {}
    @Pointcut("execution(public * *(..))")       void anyPublic() {}

    // Dynamic capability match + binding, layered on top of the cheap scope.
    @Pointcut("target(auditable)") void auditableTarget(Auditable auditable) {}

    @Around("anyPublic() && inServices() && auditableTarget(auditable)")
    Object audit(ProceedingJoinPoint pjp, Auditable auditable) throws Throwable {
        // 'auditable' bound only for the small set matched by the static scope.
        return pjp.proceed();
    }
}

go deeper

for a junior

Beyond scope; just know within() is cheap/coarse and this()/target() check objects.

for a middle

Combine within()/execution() with a target() capability match correctly.

for a senior

Explain static vs dynamic matching cost and layer PCDs so dynamic tests run on a small set.

for a principal

Define codebase-wide pointcut governance: marker interfaces, centralised @Pointcut library, proxy-strategy policy, and self-invocation escape hatches.

## Two tiers of pointcut matching ### Static (resolved once, at proxy creation) - `within(TypePattern)` — matches by **declaring type / package**; supports wildcards (`..*`). - `execution(signature)` — matches by **method signature**. - `@within`, `@annotation` (annotation presence) — also static. Because these depend only on **type metadata**, Spring evaluates them when it decides whether to create a proxy for a bean and which methods to advise. **No per-invocation cost.** They are also the ones that let Spring skip proxying beans entirely (a whole-graph performance win). ### Dynamic (may be evaluated per call) - `this(Type)` — proxy `instanceof`. - `target(Type)` — target `instanceof`. - `args(Types)` — argument `instanceof`. These can depend on **runtime state** (especially `args`, whose actual argument type can vary). Spring optimises: when the type is statically known it can resolve `this`/`target` at proxy creation, but conceptually treat them as potentially per-call and, crucially, **they gate the runtime advice** rather than merely proxy creation. ## Expressiveness trade-offs | PCD | Granularity | Wildcards | Binds | Cost | |-----|-------------|-----------|-------|------| | `within` | type / package | yes | no | static (cheap) | | `execution` | method signature | yes | no (use args for binding) | static (cheap) | | `this` | proxy type (single) | no | yes (proxy) | dynamic-ish | | `target` | target type (single) | no | yes (real obj) | dynamic-ish | `within()` cannot express method shape or bind; `this()/target()` cannot express packages/wildcards but can carry object identity/binding and express capability via marker interfaces. ## Combination patterns 1. **Scope + shape** (both static, cheapest): ``` execution(public * *(..)) && within(com.example..service..*) ``` 2. **Scope + capability** (static narrows, dynamic selects): ``` within(com.example..*) && target(com.example.Auditable) ``` The `within` prunes candidate beans cheaply; `target(Auditable)` selects only those implementing the marker and, in binding form, hands you the object. 3. **Signature + argument binding**: ``` execution(* com.example..*.place(..)) && args(orderId,..) ``` Always put a static PCD first/alongside so the dynamic test runs on a small candidate set. ## Governance guidance for a large codebase - **Centralise pointcuts** in a dedicated class of `@Pointcut` definitions and reference them by name; avoid string duplication. - **Prefer marker interfaces** (`target(Auditable)`, `target(TenantScoped)`) over package patterns for capability aspects — refactors that move packages won't silently break matching, and the intent is explicit. - **Prefer `target()` to `this()`** for concrete types to avoid the JDK-proxy silent-miss; use `this()` only when you genuinely need the proxy reference. - **Be deliberate about proxy strategy**: interface + JDK proxy vs CGLIB (`proxyTargetClass`) changes what `this()` matches; document one policy (Spring Boot defaults to CGLIB). - **Watch self-invocation**: no PCD helps when the call never crosses the proxy; if internal calls must be advised, consider AspectJ load-time weaving instead of Spring's proxy AOP. - **Minimise dynamic PCDs on hot paths**; if you only need a type match, `within()`/`execution()` avoids any runtime test. ## When to reach for each - Cross-cutting a **layer/module** → `within()` (+ `execution()`). - Advising by **capability / marker interface** or needing the object → `target()` (binding), occasionally `this()`. - Filtering by **argument values/types** → `args()`. The principal-level answer is really about *tiering*: match as much as possible statically and cheaply, and reserve dynamic, binding PCDs for the minority of join points that genuinely need object identity or arguments.

  • Why prefer within()/execution() to gate an aspect before adding target()?
    The static PCDs are resolved from type metadata at proxy-creation time with no per-invocation cost and even let Spring skip proxying non-matching beans. target() is a dynamic type test that in the general case runs per call; ANDing it after a cheap static scope shrinks the candidate set so the dynamic check runs rarely.
  • What is a limitation none of within()/this()/target() can overcome in Spring AOP?
    Self-invocation: an internal this.method() call from inside the target never crosses the proxy, so no pointcut can intercept it. Solutions are structural — inject the proxy (AopContext.currentProxy()/self-reference) or switch to AspectJ load-time weaving.

context