How do the annotation-presence designators relate to how Spring matches @Transactional and @Cacheable, and what subtleties (inheritance, interfaces, runtime designators) should you be aware of at scale?
answer
- class-level=@within, method-level=@annotation, method wins
- real impl: TransactionAttributeSourcePointcut / CacheOperationSourcePointcut (static)
- AnnotatedElementUtils/MergedAnnotations → meta-annotations + interface lookup
- method annotations not inherited; self-invocation bypasses proxy
- prefer static designators; @target/@args → early beans + warnings
basics
~20 s@Transactional/@Cacheable are matched by dedicated Spring pointcuts that, like @annotation plus @within, look for the annotation on the method and fall back to the class. They add merged-annotation and meta-annotation support that a raw @annotation pointcut lacks.
solid answer
~40 sConceptually, class-level @Transactional behaves like @within (all methods of the annotated type match) and method-level like @annotation (with method precedence over class). But Spring does not literally use those AspectJ strings — it uses purpose-built static pointcuts (TransactionAttributeSourcePointcut backed by AnnotationTransactionAttributeSource; CacheOperationSourcePointcut for caching) that resolve attributes via AnnotatedElementUtils/MergedAnnotations. That gives capabilities raw @annotation lacks: meta-annotations (a custom @AppTx composed of @Transactional), attribute inheritance from class to method, and lookup across interfaces/superclasses. Subtleties: Java method annotations aren't inherited, and proxy-based self-invocation bypasses the proxy so annotations don't fire on internal calls. Prefer static designators (@annotation/@within) over runtime ones (@target/@args) at scale: runtime designators can't be resolved statically, forcing early bean instantiation and BeanPostProcessor-ordering warnings. Keep annotations RUNTIME-retained.
code
java · 16 lines// A composed (meta-)annotation: Spring's @Transactional support recognizes it
// via merged-annotation lookup; a raw @annotation(Transactional) pointcut would NOT.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Transactional(readOnly = true, timeout = 5)
public @interface ReadTx {}
@Service
class ReportService {
@ReadTx // recognized as @Transactional(readOnly=true, timeout=5)
public Report build() { ... }
public void orchestrate() {
build(); // SELF-INVOCATION: bypasses the proxy -> NO tx applied!
}
}go deeper
Know @Transactional works via an annotation-matching proxy and doesn't fire on self-calls.
Map class-level to @within and method-level to @annotation with method precedence; know retention requirements.
Explain Spring uses dedicated static pointcuts + attribute sources enabling meta-annotations and interface/class lookup; discuss self-invocation and inheritance gotchas.
Reason about static vs runtime designator trade-offs at scale (early instantiation, BPP ordering), composed-annotation strategy via MergedAnnotations, and when to reach for AspectJ weaving over proxies.
## The conceptual mapping The four annotation-presence designators express the ideas Spring's declarative features rely on: - **Method-level annotation ⇒ `@annotation`-like.** `@Transactional`/`@Cacheable` on a method advise that method. - **Class-level annotation ⇒ `@within`-like.** `@Transactional` on a class advises *all* its methods (declared-in semantics), with method-level annotations taking **precedence** when both are present. So mentally: 'class-level = @within, method-level = @annotation, method wins'. ## What Spring actually uses (not the literal AspectJ strings) Spring's transaction and cache infrastructure do **not** register an `@Aspect` with `@annotation(...)`/`@within(...)` expressions. They register dedicated advisors: - **Transactions:** `BeanFactoryTransactionAttributeSourceAdvisor` + `TransactionAttributeSourcePointcut` (a `StaticMethodMatcherPointcut`) backed by `AnnotationTransactionAttributeSource`. - **Caching:** `BeanFactoryCacheOperationSourceAdvisor` + `CacheOperationSourcePointcut` backed by `AnnotationCacheOperationSource`. These pointcuts are **static** matchers: for a candidate method they ask the *attribute source* 'is there transaction/cache metadata here?', checking the method first, then the declaring class, walking superclasses and **interfaces**, using `AnnotatedElementUtils`/`MergedAnnotations`. ## Capabilities beyond a raw @annotation pointcut Because of the attribute-source + merged-annotation machinery, `@Transactional`/`@Cacheable` matching supports things a plain `@annotation(Transactional)` pointcut does **not**: 1. **Meta-annotations / composed annotations.** A custom `@interface AppTx` itself annotated with `@Transactional` is recognized; a raw `@annotation(Transactional)` would only match the literal annotation. 2. **Class-to-method attribute inheritance.** Attributes declared at class level apply to methods (with method overrides). Raw designators don't merge attributes. 3. **Interface/superclass lookup.** Spring can find `@Transactional` declared on an interface method or a superclass in ways plain Java reflection (and thus a naive `@annotation` matcher) would miss, since **method annotations are not inherited by Java** and **type annotations are inherited only with `@Inherited` (never from interfaces)**. ## Key subtleties at scale ### 1. Non-inheritance of method annotations Java never inherits **method-level** annotations. Placing `@Transactional` on a method and overriding it in a subclass without re-annotating loses it for a naive matcher. Spring's attribute sources mitigate this, but custom `@annotation`-based aspects do not. ### 2. Interface vs class proxying With JDK dynamic proxies (interface-based), `@Transactional` on the *implementation class* is still found by Spring's attribute source, but if you write your **own** `@annotation`/`@within` aspect you must account for whether the annotation is on the interface or the class and which the proxy exposes. ### 3. Proxy self-invocation All proxy-based advice — Spring's own and yours — only triggers when the call goes **through the proxy**. An internal `this.method()` call to another annotated method in the same bean **bypasses** the proxy, so `@Transactional`/`@Cacheable`/your `@annotation` advice does **not** fire. This is the single most common production surprise. ### 4. Static vs runtime designators — the scaling decision Prefer `@annotation` and `@within` (both **static**) for cross-cutting concerns. Avoid `@target`/`@args`/`this`/`target`/`args` unless you truly need runtime type/argument information: they cannot be resolved at proxy-creation time, so Spring must consider every bean as a potential match. This can: - **Instantiate beans early**, before all `BeanPostProcessor`s are ready, yielding *'not eligible for getting processed by all BeanPostProcessors'* log warnings and occasional ordering bugs. - Add a **per-invocation runtime check** cost on hot paths. Mirror Spring's own choice: its transaction/cache pointcuts are **static** method matchers. ### 5. Retention Every annotation used in any of these designators must be `@Retention(RUNTIME)`; type-level ones need `@Target` allowing `TYPE`, method-level ones `METHOD`. ## Practical guidance - Design custom cross-cutting annotations as **method-level, RUNTIME-retained**, matched with `@annotation`, and consider a class-level variant matched with `@within` for 'apply to all methods'. - Support **meta-annotations** by using `AnnotatedElementUtils.findMergedAnnotation` inside advice if you need Spring-like composition. - Document the **self-invocation** limitation for consumers. - Reserve `@target`/`@args` for genuine runtime needs and pair them with a static designator to bound candidates.
- Why doesn't @Transactional fire on an internal method call within the same bean?Spring's declarative transactions are proxy-based; advice runs only when the call passes through the proxy. A self-invocation (this.method()) targets the raw object directly, bypassing the proxy, so no transaction advice is applied. Fixes: self-inject the proxy, split into two beans, or use AspectJ load-time weaving.
- Why does Spring use a static method-matcher pointcut for @Transactional rather than @target?Transaction metadata is a property of the method/declaring type, resolvable statically at proxy-creation time. A static matcher avoids per-invocation runtime checks and avoids forcing early bean instantiation, keeping proxy creation predictable and performant across a large bean graph.
saying these in an interview costs you the question
- Claiming Spring literally uses @annotation/@within strings for @Transactional
- Saying method annotations are inherited so subclass overrides keep @Transactional
- Believing self-invocation still triggers @Transactional/@Cacheable
- Recommending @target/@args broadly without noting early-instantiation/BeanPostProcessor warnings
- Thinking a raw @annotation pointcut recognizes meta/composed annotations