DEV Community

Spring proxies: JDK dynamic vs CGLIB

You put @Transactional on a method and a database transaction opens before it runs and commits when it returns - yet you never wrote a line to start or end one. You add @Cacheable , and the second call with the same arguments skips your method body entirely. You didn't touch the code inside those methods. So where does the extra behaviour actually run? It runs inside a proxy: a stand-in object that Spring slips between whoever calls your bean and the real bean itself. You meet proxies the moment you use any Spring annotation that wraps behaviour around a method - transactions, caching, security, retries, async. Most of the time you never see it. This article is about what that stand-in is, the two very different ways Spring builds one at runtime, and the traps that appear once you know it sits in the path. A proxy is just a stand-in Forget Spring for a moment. Suppose you have a class that does real work behind an interface: public interface PaymentService { void charge(String account, long cents); } public class RealPaymentService implements PaymentService { public void charge(String account, long cents) { // actually move the money } } Now say you want to log every charge, but you can't edit RealPaymentService . You could write a second class that implements the same interface, holds the real one inside, and adds the logging: public class LoggingPaymentService implements PaymentService { private final PaymentService target; LoggingPaymentService(PaymentService target) { this.target = target; } public void charge(String account, long cents) { System.out.println("charging " + cents); target.charge(account, cents); // forward to the real object } } Hand callers a LoggingPaymentService and they can't tell the difference - it is a PaymentService . That wrapper is a proxy: same surface, extra behaviour, forwarding to the real object underneath, which we'll call the target. Everything Spring does with proxies is this pattern. The only twist is that you don't write the wrapper by hand. Spring manufactures one for you, at runtime, for a class it has never seen before. Why Spring needs them The logging above has nothing to do with payments. It would apply the same way to refunds, lookups, anything. A behaviour that cuts across many unrelated methods like that is a cross-cutting concern. Transactions, security checks, caching, and metrics are all cross-cutting - you don't want to paste the same boilerplate into every method body. Spring's answer is to keep your method clean and push the concern into a proxy. You annotate the method; Spring wraps the bean; the annotation's behaviour lives in the wrapper. That leaves one real question: how does Spring build that wrapper on the fly? There are two mechanisms, and the difference between them is the whole point of this topic. Way one: a JDK dynamic proxy Java has shipped a proxy tool since version 1.3: java.lang.reflect.Proxy . Give it a set of interfaces and it generates - in memory, at runtime - a brand-new class that implements them. That generated object is a JDK dynamic proxy. Every call on it is funnelled into a single method you write, an InvocationHandler : PaymentService proxy = (PaymentService) Proxy.newProxyInstance( real.getClass().getClassLoader(), new Class[]{ PaymentService.class }, // interfaces to mimic (proxyObj, method, args) -> { System.out.println("before " + method.getName()); Object result = method.invoke(real, args); // forward to target System.out.println("after " + method.getName()); return result; }); The lambda is the handler. When someone calls proxy.charge(...) , Java routes it into the lambda with method = charge and args = [...] . You run your extra work, call method.invoke(real, args) to reach the real object, and return its result. It's the hand-written wrapper from before - except the class was generated for you. The catch hides in that second argument: new Class[]{ PaymentService.class } . A JDK dynamic proxy can only mimic interfaces. The generated class implements PaymentService , but it is not a RealPaymentService - it's a synthetic class that merely shares the interface. So this style works only when your bean has an interface to stand behind. Way two: a CGLIB proxy What if the bean has no interface - just a plain class? Java's built-in proxy can't help, so Spring reaches for a library called CGLIB. Instead of implementing an interface, CGLIB generates a subclass of your class at runtime and overrides its methods: Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(RealPaymentService.class); // subclass the real class enhancer.setCallback((MethodInterceptor) (obj, method, args, proxyRef) -> { System.out.println("before " + method.getName()); Object result = proxyRef.invokeSuper(obj, args); // call the original method System.out.println("after " + method.getName()); return result; }); PaymentService proxy = (PaymentService) enhancer.create(); The shape is identical - do something, forward, do something - but the mechanism differs. The proxy here is a subclass, so it genuinely is-a RealPaymentService . invokeSuper calls the original parent method, which is CGLIB's way of reaching the real behaviour. Because it works by subclassing, CGLIB needs no interface at all. That is its entire reason to exist. How Spring picks one Spring makes this choice for you. The classic rule, still true in plain Spring Framework, is simple: - Bean implements at least one interface โ†’ JDK dynamic proxy against that interface. - Bean has no interface โ†’ CGLIB subclass. You can force CGLIB even when interfaces exist, with a flag: @EnableTransactionManagement(proxyTargetClass = true) That proxyTargetClass = true says "subclass the class, don't proxy the interface." Spring Boot took this further: since Boot 2.0 the default is CGLIB for everything, interface or not. The reasoning is practical - a JDK proxy exposes only the interface, so any code that injects the concrete class breaks (more on that shortly). Subclass proxies avoid that whole category of surprise, so Boot standardised on them. Gotcha 1: self-invocation Now the traps - and nearly all of them fall out of one fact: the proxy only wraps calls that arrive from outside the bean. A call the bean makes to itself never leaves the object, so it never passes through the proxy. Picture a bean where one method calls another: @Service public class Orders { public void placeAll(List orders) { for (Order o : orders) { save(o); // internal call - stays inside 'this' } } @Transactional public void save(Order o) { // expected to run in its own transaction } } You'd expect every save to run in a transaction. It doesn't. When placeAll calls save , that is a plain this.save(o) - it happens inside the real object, underneath the proxy. The proxy never sees it, so the @Transactional wrapping is skipped. Only a call to save from another bean, which enters through the proxy, gets a transaction. This is the single most common Spring surprise. @Transactional , @Cacheable , @Async - all of them silently do nothing on self-invocation, and the code looks completely correct. The fix is to make the call go through the proxy: move the annotated method into a separate bean, or inject the bean into itself and call through that injected reference. Gotcha 2: final and private methods CGLIB works by subclassing and overriding. Java won't let you override a final method or subclass a final class - so CGLIB simply can't wrap them. There's no error, just no proxying: a @Transactional on a final method quietly does nothing. private methods hit the same wall for a different reason. A subclass can't override a private method, and an interface can't declare one, so neither proxy style can wrap it. Keep proxied methods public and non-final and you stay clear of this trap. Gotcha 3: inject the interface, not the class With a JDK dynamic proxy, recall that the proxy implements the interface but is not your class. So this injection blows up: @Autowired RealPaymentService payments; // fails if a JDK proxy is in play The proxy can be assigned to PaymentService but not to RealPaymentService , so Spring throws at startup. Inject the interface instead: @Autowired PaymentService payments; // fine - the proxy is-a PaymentService This is exactly the breakage that Boot's "CGLIB everywhere" default sidesteps: a subclass proxy is the concrete type, so both forms work. Gotcha 4: the constructor runs oddly CGLIB builds its subclass through a route that doesn't call your constructor the normal way - your bean's constructor can run twice, or field initialisers may not have run when you'd expect. The practical rule: don't put real work or important side effects in the constructor of a proxied bean. Do that setup in an @PostConstruct method, which Spring calls once on the fully built instance. The one model to keep A Spring proxy is a stand-in that wraps your bean so annotations can add behaviour around your methods without touching their bodies. It's built one of two ways: JDK dynamic proxies implement your interface; CGLIB proxies subclass your class - and Spring Boot defaults to CGLIB so the concrete type still resolves. Every proxy gotcha falls out of two facts: the wrapper only sees calls that enter from outside, so self-invocation escapes it; and it can only wrap what it can override or implement, so final and private escape it. Hold those two facts and the rest is just detail. Top comments (0)

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.