CGLIB can technically override protected and package-private methods, so why does Spring still ignore @Transactional on them in proxy mode?
answer
- Two limits: CGLIB-technical vs public-only-policy
- publicMethodsOnly=true in PROXY mode
- JDK-proxy parity is the reason
- AspectJ mode -> publicMethodsOnly=false
- Same rules for @Async/@Cacheable
basics
~10 sIt's a deliberate Spring policy, not a pure technical limit. In proxy mode Spring's AnnotationTransactionAttributeSource only reads transactional metadata from public methods, so protected/package-private annotations are ignored even though CGLIB could override them.
solid answer
~40 sThere are two different limits at play. The hard technical limit is CGLIB: it truly cannot override private or final methods. But protected and package-private methods *could* be overridden by a CGLIB subclass. Spring nonetheless ignores @Transactional on them in **proxy mode** by policy — `AnnotationTransactionAttributeSource` is constructed with `publicMethodsOnly = true`, so it only extracts transaction attributes from public methods. The rationale is consistency and safety: JDK dynamic proxies (the other proxy strategy) can only ever advise public interface methods, so Spring standardizes on public-only across both proxy types to avoid behavior that changes when you flip `proxyTargetClass`. If you genuinely need non-public transactional methods, switch to **AspectJ mode** (`@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)`), where `publicMethodsOnly` is false and bytecode weaving applies advice regardless of visibility.
code
java · 11 lines// Proxy mode (default): protected/package-private @Transactional is ignored,
// because AnnotationTransactionAttributeSource uses publicMethodsOnly=true.
@Configuration
@EnableTransactionManagement // mode = AdviceMode.PROXY by default
class ProxyConfig {}
// AspectJ mode: weaves advice into bytecode -> advises non-public,
// final, and self-invoked methods (publicMethodsOnly=false).
@Configuration
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
class WeavingConfig {}go deeper
May only know 'must be public'; the why is beyond scope.
Should distinguish the CGLIB technical limit from the public-only rule.
Should cite publicMethodsOnly and the JDK-proxy-parity rationale, plus AspectJ as the override.
Should reason about API design tradeoffs and the systemic impact across all proxy-based AOP annotations.
### Two separate constraints, often conflated 1. **Technical (CGLIB) limit** — CGLIB generates a runtime subclass and overrides methods. It **cannot** override `private` methods (not inherited) or `final` methods (override forbidden). This is a JVM-level fact. 2. **Policy limit (public-only)** — For `protected` and package-private methods, a CGLIB subclass in the *same generated package* could technically override them. Yet Spring still ignores `@Transactional` there in proxy mode. This is a **deliberate design decision**, not a technical wall. ### Where the policy lives Spring's `AnnotationTransactionAttributeSource` has a `publicMethodsOnly` flag. In **proxy mode** (`AdviceMode.PROXY`, the default from `@EnableTransactionManagement`), it is created with `publicMethodsOnly = true`. Its `getTransactionAttribute(...)` therefore returns `null` for any non-public method, so the `TransactionInterceptor` never wraps it. In **AspectJ mode** (`AdviceMode.ASPECTJ`), the same source is created with `publicMethodsOnly = false`, so protected/package-private (and self-invoked) methods are advised. ### Why standardize on public-only - **Proxy-strategy independence.** Spring supports both **JDK dynamic proxies** (interface-based, can only ever see public interface methods) and **CGLIB** (subclass-based). If CGLIB advised protected methods but JDK proxies couldn't, then merely toggling `proxyTargetClass`/`@EnableTransactionManagement(proxyTargetClass=...)` would change which methods are transactional — a nasty, invisible behavior shift. Public-only keeps semantics identical across both. - **Encapsulation intent.** A transactional boundary is part of a component's *public contract*; making it a service entry point (public) matches that intent. - **Predictability.** One simple, teachable rule ("only public, non-final, external calls") beats a matrix of visibility × proxy-strategy × package exceptions. ### The escape hatch: AspectJ weaving `@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)` (plus the `spring-aspects` dependency and load-time or compile-time weaving) replaces proxies with **bytecode weaving**: the advice is woven directly into the class. This bypasses all the proxy limitations — non-public methods, final methods, and self-invocation all become transactional. The cost is build/agent setup (`-javaagent:spring-instrument.jar` for LTW) and a steeper mental model, so most teams stay on proxy mode and just keep methods public. ### Interview-grade nuances - The **self-invocation** problem is orthogonal: even a public @Transactional method won't start a transaction if called as `this.method()` from within the same bean, because the call skips the proxy. AspectJ also fixes this. - `@Async`, `@Cacheable`, and other proxy-based AOP annotations share the exact same public-only, non-final, no-self-invocation constraints — the mechanism is identical. - Since Spring Framework 6.0 there's stricter startup validation in some setups, but plain @Transactional on a non-public method still typically stays silent in proxy mode.
- Name another Spring annotation with the exact same public/non-final/no-self-invocation constraints.@Async, @Cacheable/@CacheEvict, @Retryable, and @Validated on methods — all are implemented with the same proxy-based AOP, so they share the identical limitations. @Transactional is just the most famous.
- How would you make a protected method transactional without changing its visibility?Switch to AspectJ mode via @EnableTransactionManagement(mode = AdviceMode.ASPECTJ) with spring-aspects and load-time/compile-time weaving. That weaves the advice into the bytecode and applies it regardless of method visibility.