skip to content

How do you control whether Spring uses CGLIB vs JDK proxies, and what are the tradeoffs of Spring Boot forcing CGLIB by default?

level: principalimportance: should knowfreq 35%

answer

  1. proxyTargetClass true=CGLIB false=prefer JDK
  2. spring.aop.proxy-target-class default true in Boot
  3. JDK proxy = interface type only → impl injection fails
  4. CGLIB uniform but final/subclass limits
  5. AspectJ weaving for final/private/static/fields

basics

~10 s

Set proxyTargetClass=true to force CGLIB or false to prefer JDK proxies (on @EnableAspectJAutoProxy, @EnableTransactionManagement, etc., or via spring.aop.proxy-target-class). Boot defaults to CGLIB so injecting concrete types always works, at the cost of subclassing constraints.

solid answer

~40 s

The switch is proxyTargetClass on the AOP/transaction/cache @Enable annotations, or globally spring.aop.proxy-target-class in Boot. true forces CGLIB even when interfaces exist; false lets Spring use JDK proxies when the bean implements interfaces. Boot sets it true by default. The upside: uniform behavior, and you can autowire the concrete class type (a JDK proxy only implements the interface, so injecting the impl type fails). The downsides: you inherit CGLIB's subclassing limits (no final classes, final/private/static methods unadvised), the proxy doesn't share the interface-only contract so leaking implementation types is easier, and there's slightly more generated code. Coding to interfaces with JDK proxies gives cleaner contracts but breaks if code injects the impl type or the bean has no interface. For libraries and consistency, Boot's CGLIB default is the pragmatic choice; enforce non-final on advised classes.

code

java · 14 lines
java
// Force JDK dynamic proxies (only works if beans implement interfaces):
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = false)
@EnableTransactionManagement(proxyTargetClass = false)
class AopConfig { }

// Or globally in application.yml (Boot default is true):
// spring:
//   aop:
//     proxy-target-class: false

// Gotcha this avoids/creates: with a JDK proxy, this FAILS —
//   @Autowired FooServiceImpl impl;   // proxy is only assignable to FooService
// With CGLIB (Boot default) it works, because the proxy extends FooServiceImpl.

go deeper

for a junior

Know the proxyTargetClass flag exists to force CGLIB.

for a middle

Know Boot defaults to CGLIB and that JDK proxies need interfaces.

for a senior

Explain the concrete-type injection failure and the fallback-to-CGLIB behavior of false.

for a principal

Weigh abstraction boundaries vs uniformity, set org-wide conventions, and know when to escalate to AspectJ weaving.

**The control knobs.** - `@EnableAspectJAutoProxy(proxyTargetClass = ...)`, `@EnableTransactionManagement(proxyTargetClass = ...)`, `@EnableCaching(proxyTargetClass = ...)`, `@EnableAsync` etc. each carry a `proxyTargetClass` flag. - In Spring Boot, the global property **`spring.aop.proxy-target-class`** (default **`true`**) governs the AOP auto-configuration. Boot's `TransactionManagementConfigurationSelector`/auto-config also honors it, so Boot apps use CGLIB across the board unless you set it to `false`. - `true` → always CGLIB (subclass), even when interfaces are present. `false` → Spring uses a JDK dynamic proxy **if** the bean implements at least one interface, else it falls back to CGLIB anyway. **Why Boot defaults to CGLIB.** The historical footgun with JDK proxies: the injected bean is only assignable to the *interface*, not the concrete class. So `@Autowired FooServiceImpl` (or any code depending on the concrete type / a non-interface method) fails at wiring or invocation time. Because developers frequently, and sometimes unknowingly, depend on concrete types, Boot chose CGLIB by default so the proxy *is-a* subclass of the concrete type and both `Foo` and `FooImpl` injection points resolve. It also gives uniform semantics regardless of whether a given bean happens to have an interface. **Tradeoffs of CGLIB-by-default.** - **Pros:** consistent behavior; concrete-type injection works; all public methods (not just interface methods) can be advised; no accidental 'this method isn't on the interface so it's unproxied' surprises. - **Cons:** inherits subclassing constraints — `final` classes can't be proxied (startup failure), and `final`/`private`/`static` methods are silently unadvised; Kotlin needs the all-open plugin; slightly heavier generated classes and a dependence on Objenesis for instantiation; it's easier to leak/rely on implementation types instead of programming to interfaces, which weakens abstraction boundaries. **JDK-proxy (interface) approach tradeoffs.** - **Pros:** enforces coding to interfaces, cleaner contracts, no subclassing limits, no `final` concerns, lighter reflection-based proxy. - **Cons:** requires an interface; only interface-declared methods are advised; breaks when any code injects/depends on the concrete impl type; two-object identity semantics differ. **Design guidance at scale.** - Accept Boot's CGLIB default for application services for uniformity; make advised classes/methods non-final (in Kotlin, rely on `kotlin-spring`). - If you deliberately design around interfaces and want the abstraction enforced, set `spring.aop.proxy-target-class=false` and be disciplined about never injecting impl types. - Remember both strategies share the **self-invocation** limitation and the need for advised methods to be at least protected/public and non-static. - For advice on final/private/static methods or fields, neither Spring proxy works — you'd need **AspectJ** compile-time or load-time weaving (`@EnableLoadTimeWeaving` / AspectJ weaver), which modifies bytecode directly rather than proxying. **Verification.** Use `AopUtils.isCglibProxy` / `isJdkDynamicProxy` in tests to pin the strategy, and assert startup succeeds (final-class regressions surface as `AopConfigException`/proxy-creation errors).

  • With proxyTargetClass=false, why can autowiring the concrete implementation type fail?
    A JDK dynamic proxy implements only the target's interfaces, so its runtime type is not assignable to the concrete impl class. Injection points typed to the impl (or calling non-interface methods) can't be satisfied. CGLIB avoids this because the proxy subclasses the impl.
  • You need advice on a final method or a field access. What's the option beyond Spring proxies?
    Switch to AspectJ compile-time or load-time weaving (@EnableLoadTimeWeaving with the AspectJ weaver), which rewrites bytecode directly instead of proxying, so it can advise final/private/static methods, constructors, and field access.

saying these in an interview costs you the question

  • Saying proxyTargetClass=false always yields JDK proxies (it falls back to CGLIB when there's no interface)
  • Thinking Boot defaults to JDK proxies (it defaults to CGLIB / proxy-target-class=true)
  • Believing CGLIB can advise final methods if you just set proxyTargetClass=true

context