skip to content

Between JDK dynamic proxies and CGLIB, which method-visibility and finality constraints apply to @Transactional, and how do the two strategies differ?

level: middleimportance: should knowfreq 35%

answer

  1. JDK proxy = interface, public only
  2. CGLIB = subclass, no private/final/static
  3. Boot default = CGLIB (proxyTargetClass=true)
  4. Both: public-only + no self-invocation
  5. AspectJ transcends both

basics

~20 s

Both only advise public methods in proxy mode. JDK proxies need an interface and advise only public interface methods; CGLIB subclasses the class and can't override private, final, or static methods. Either way, private/final/self-invoked calls are skipped.

solid answer

~40 s

Spring has two proxy strategies. **JDK dynamic proxies** implement your bean's interfaces, so they can only intercept methods declared on an interface — inherently public. **CGLIB** generates a runtime subclass and overrides methods, which requires the class to be non-final and each advised method to be overridable: not `private`, not `final`, not `static`. In proxy mode Spring layers a public-only policy on top of both, so protected and package-private @Transactional is ignored regardless of strategy. Spring Boot defaults to **CGLIB** (`proxyTargetClass=true`) so proxying works even without an interface. Practical upshot for @Transactional: keep methods public and non-final, and remember neither strategy can intercept a **self-invocation** (`this.method()`), because that call never leaves the target object to pass through the proxy.

code

java · 23 lines
java
// Force JDK dynamic proxies -> only interface (public) methods are advised.
@Configuration
@EnableTransactionManagement(proxyTargetClass = false)
class JdkProxyConfig {}

interface PaymentApi {
    void pay(long id);            // advised via JDK proxy (interface method)
}

@Service
class PaymentService implements PaymentApi {

    @Override
    @Transactional
    public void pay(long id) {     // OK: public, on the interface
        settle(id);                // self-invocation -> NOT re-proxied
    }

    @Transactional
    public void settle(long id) {   // annotation here is bypassed when
        // ...                      // called as this.settle() above
    }
}

go deeper

for a junior

Enough to say both need public methods.

for a middle

Should contrast interface-based JDK proxies vs subclass-based CGLIB and know Boot defaults to CGLIB.

for a senior

Should tie in publicMethodsOnly policy and self-invocation applying to both.

for a principal

Should discuss when to force JDK proxies (interface-driven design) versus adopting AspectJ.

### The two proxy strategies Spring AOP (which powers @Transactional in proxy mode) can build a proxy two ways: **JDK dynamic proxy** (`java.lang.reflect.Proxy`) - Requires the target to implement at least one **interface**. - The proxy is a new object implementing those interfaces and delegating to your bean. - It can therefore only intercept **methods declared on an interface** — which are always **public**. Methods not on any interface are invisible to it. **CGLIB** (bytecode subclassing) - Generates a **subclass** of your concrete class at runtime and **overrides** methods to insert advice. - Requires the **class** to be non-`final` (else it can't subclass) and each advised method to be **overridable**: not `private`, not `final`, not `static`. - Works without any interface, which is why Spring Boot prefers it. ### Default and how to control it Since Spring Boot 2.0 the default is **CGLIB** (`spring.aop.proxy-target-class=true`, and `@EnableTransactionManagement(proxyTargetClass=true)`/`@EnableAspectJAutoProxy` equivalents). You can force JDK proxies by setting `proxyTargetClass=false`, but then every proxied bean **must** have an interface, and only interface methods are advised. ### The shared, non-negotiable rules Regardless of strategy, in **proxy mode** @Transactional is applied only to: - **public** methods (Spring's `publicMethodsOnly=true` policy — see the AnnotationTransactionAttributeSource), and - for CGLIB additionally, methods that are **not final/static** (technical override limit); private is impossible for both. And for **both** strategies, **self-invocation is not intercepted**: when code inside the bean calls `this.otherTransactionalMethod()`, the call goes straight to the real instance, never through the proxy, so no new transaction starts. This is the single most common real-world manifestation of the whole family of bugs. ### Comparison (mental model) - Needs interface? JDK: **yes**; CGLIB: **no**. - Advises non-public? Neither, in proxy mode. - Advises final method? JDK: n/a (interface methods aren't final in effect); CGLIB: **no**. - Advises self-invocation? **Neither**. - Default in Spring Boot? **CGLIB**. ### Practical guidance - Default (CGLIB) is fine for services without interfaces; just keep @Transactional methods **public, non-final, non-static**, and call them from a different bean. - If you deliberately use JDK proxies, put the transactional method on the **interface**. - To transcend all of this (non-public, final, self-invocation), move to **AspectJ weaving** — but that's a heavier architectural commitment.

  • You set proxyTargetClass=false but your @Transactional bean has no interface. What happens?
    Spring can't build a JDK dynamic proxy without an interface, so it either falls back to CGLIB or the advice isn't applied, depending on config. The reliable fix is to give the bean an interface or leave the default CGLIB in place.
  • Does either proxy strategy fix the self-invocation problem?
    No. Neither JDK nor CGLIB intercepts this.method() calls, because the call never passes through the proxy. Only AspectJ weaving, or refactoring the method onto a separate injected bean, makes a self-invoked transactional method work.

context