Dynamic Proxies: JDK Proxy vs CGLIB

bee2026-10-0842 min read0 views
Write a JDK dynamic proxy by hand, dissect a CGLIB bytecode subclass, and settle performance, limitations and how Spring picks between them.
1 / 116
Section
0. Thirty seconds to get it
2 / 116

The previous article said AOP works by "building a stand-in at runtime". This article takes that stand-in apart: who builds it, what it looks like, and how a call finally reaches your code. Three terms, pinned down for someone who has never written Java:

3 / 116
  • Reflection — a program's ability to operate on classes themselves while running, e.g. holding a Method object and calling method.invoke(target, args). Think of writing the method name on paper and asking someone to perform it, instead of hard-wiring which line runs at compile time.
  • Bytecode — the intermediate language the JVM reads after .java is compiled, stored in .class files. CGLIB can "conjure a subclass out of thin air" precisely because it assembles those instructions in memory, never going through javac.
  • Dynamic proxy — a class manufactured while the program is already running, shaped exactly like your target and instantiated on the spot. No such class exists anywhere in your source.
4 / 116
类比

a JDK dynamic proxy is a receptionist stand-in. You come to see the CEO; a receptionist who looks just like him hands you a card with the same title (implements the same interface). Everything you say (every method call) is noted down and relayed to the real CEO, and the answer comes back through the same desk. The CEO neither knows nor changes anything — only who receives you changed. CGLIB is more like adopting a son: it generates a subclass whose parent is your target class and tampering happens inside the overridden methods, so the parent cannot be final (no father to adopt) and the method cannot be final (overriding forbidden).

5 / 116
类比

an InvocationHandler is a parcel sorting hub. Every package (call), whatever its addressee, first arrives at this single place carrying a label with "recipient + waybill number" (Method + args). The hub decides whether to scan and register it first (before advice), deliver as-is (pass-through), or send it back (throw). That is why enhancement written once covers every method.

6 / 116
Diagram
Figure · JDK or CGLIB: two dimensions decide it
Figure · JDK or CGLIB: two dimensions decide it
7 / 116

After this article you should be able to answer:

8 / 116
  1. Why can $Proxy0 only implement interfaces rather than extend my implementation class?
  2. When is Spring forced into CGLIB, and how do the consequences of a final class differ from a final method?
  3. Why does (UserServiceImpl) proxy explode, and why does a self-invoked @Transactional fail silently?
9 / 116
Section
1. The maintenance hell of static proxies
10 / 116

Last section we hand-wrote a static proxy; its "you can never finish" property was only a vague ache then. Scale the scene up and it becomes visceral: the system has three service interfaces.

11 / 116
Code
Codejava
public interface UserService  { User findById(Long id); void save(User user); }public interface OrderService { Order create(OrderDTO dto); void cancel(Long id); }public interface PayService   { void pay(BigDecimal amount); }// Each new interface needs another proxy class; each method needs the same "forward + log" codepublic class UserServiceLogProxy  implements UserService  { /* forward every method... */ }public class OrderServiceLogProxy implements OrderService { /* forward every method... */ }public class PayServiceLogProxy   implements PayService   { /* forward every method... */ }
Notes
  • Proxies bind 1:1 to interfaces, so proxy count grows linearly with interface count
  • Every new interface method forces a near-identical forwarding snippet in the proxy
  • Add a second aspect (transactions, authorization) and proxy count multiplies further by aspect combination
12 / 116

The root problem: "forward + enhance" is identical for every interface, yet we wrote it out by hand many times. Since it has nothing to do with the interface, the program should generate it at runtime — that is the motivation for a dynamic proxy.

13 / 116
Section
2. Writing a JDK dynamic proxy by hand
14 / 116

The JDK ships a dynamic proxy mechanism built on just two classes: java.lang.reflect.Proxy and InvocationHandler. This example runs as-is:

15 / 116
Code
Codejava
package com.example.proxy;import java.lang.reflect.*;import java.util.Arrays;public class JdkProxyDemo {    public static void main(String[] args) {        UserService target = new UserServiceImpl();   // the real target        UserService proxy = (UserService) Proxy.newProxyInstance(                target.getClass().getClassLoader(),   // 1. classloader for the proxy class                new Class<?>[]{ UserService.class },  // 2. interfaces the proxy implements                new LogInvocationHandler(target)      // 3. who handles the forwarded call        );        proxy.save(new User("alice"));        System.out.println("main got: " + proxy.findById(1L));    }}class LogInvocationHandler implements InvocationHandler {    private final Object target;    LogInvocationHandler(Object target) { this.target = target; }    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        System.out.println("[before] " + method.getName() + " args=" + Arrays.toString(args));        Object result = method.invoke(target, args);   // reflective call to the real target        System.out.println("[after] " + method.getName() + " result=" + result);        return result;    }}
Notes

Attention: Proxy.newProxyInstance only works when an interface is present. Passing UserServiceImpl.class instead of UserService.class would not change the essence — the proxy still only implements interfaces and never extends the implementation class. With no interface at all, JDK proxying has no foothold.

16 / 116

Output:

17 / 116
Code
Codetext
[before] save args=[User{name='alice'}][after] save result=null[before] findById args=[1][after] findById result=User{name='alice'}main got: User{name='alice'}
Notes
  • The three arguments are: a classloader, the interfaces to implement, and the InvocationHandler that forwards calls
  • All enhancement logic lives in the single invoke method, independent of which interface or method it is
  • method.invoke(target, args) uses reflection to reach the real target — that is how "forwarding" is done
18 / 116

Why must the target implement an interface? Because in Java a class extends only one parent, and the generated proxy already extends Proxy, so it cannot also extend your implementation. It can only achieve "same type as the target" by implementing interfaces. That restriction defines its boundary and leads straight to CGLIB.

19 / 116
Section
3. Decompiled: what `$Proxy0` looks like
20 / 116

At runtime the JDK generates a proxy class in memory named $Proxy0, $Proxy1... It looks roughly like this (simplified):

21 / 116
Code
Codejava
// Generated at runtime by the JDK (illustrative, not source)public final class $Proxy0 extends Proxy implements UserService {    private static Method m3;   // corresponds to save(User)    public $Proxy0(InvocationHandler h) {        super(h);   // hand the handler to the superclass, Proxy    }    @Override    public void save(User user) {        try {            // The essence: pack "method + args" as-is and hand it to handler.invoke            super.h.invoke(this, m3, new Object[]{ user });        } catch (Throwable e) {            throw new UndeclaredThrowableException(e);        }    }}
Notes
  • The proxy class is generated at runtime; add -Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true to dump it to disk
  • It extends Proxy, so it can only be "same type" as the target via interfaces — a direct consequence of Java's single inheritance
  • Each interface method body is nearly one line: hand Method and args to h.invoke; all real logic lives in the handler
22 / 116
Animation
Animation · How $Proxy0 hands a call to the InvocationHandler
Animation · How $Proxy0 hands a call to the InvocationHandler
23 / 116

This animation slows the snippet above into six frames. Notice your own code runs for the first time only after frame three — from $Proxy0's point of view the world contains exactly two activities: pack, and hand over.

24 / 116
Section
4. CGLIB: generate a subclass, not an interface impl
25 / 116

CGLIB (Code Generation Library) takes the other road: it generates a subclass of the target class at runtime, overrides its methods and inserts callbacks. It uses ASM to manipulate bytecode directly. The core is Enhancer and MethodInterceptor:

26 / 116
Code
Codejava
package com.example.proxy;import net.sf.cglib.proxy.*;public class CglibProxyDemo {    public static void main(String[] args) {        Enhancer enhancer = new Enhancer();        enhancer.setSuperclass(OrderService.class);           // a "superclass", not an interface        enhancer.setCallback(new LogMethodInterceptor());     // the method interceptor        OrderService proxy = (OrderService) enhancer.create(); // create the subclass instance        proxy.create(new OrderDTO("A1001"));    }}class LogMethodInterceptor implements MethodInterceptor {    @Override    public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp)            throws Throwable {        System.out.println("[before] " + method.getName());        Object result = mp.invokeSuper(obj, args);   // call the superclass (real) method        System.out.println("[after] " + method.getName());        return result;    }}
Notes
  • The CGLIB proxy object is a subclass of the target, so the target needs no interface
  • mp.invokeSuper(obj, args) calls the "superclass original method", the CGLIB analogue of method.invoke(target, args)
  • Not needing an interface is exactly what closes the JDK proxy's gap
27 / 116

"Conjuring a subclass out of thin air" sounds like magic, so this six-frame animation turns it into an assembly line. Watch frame ② in particular: the bytecode is stitched together in memory by ASM, never by javac, which is why you will never find this class under target/; frame ⑤ is the moment the original parent method finally runs:

28 / 116
Animation
Animation · How CGLIB conjures a subclass
Animation · How CGLIB conjures a subclass
29 / 116

But it comes at a price: since it is inheritance, it inherits every restriction of Java inheritance. These two are the worst offenders:

30 / 116
Code
Codejava
public final class FinalService {   // final class: cannot be subclassed, CGLIB fails outright    public void hello() { }}public class NormalService {    public final void fast() { }     // final method: not intercepted; advice silently lost    private void inner() { }         // private method: likewise not intercepted}
Notes

Trap: CGLIB is blunt about a final class — it throws IllegalArgumentException: Cannot subclass final class, so you find it immediately. The genuinely nasty case is a final / private / static method: the proxy builds fine, the app runs fine, but advice on those methods silently stops firing and nothing shows up in the logs.

31 / 116
Section
5. Side-by-side comparison
32 / 116
Diagram
Figure 1 · JDK proxy vs CGLIB
Figure 1 · JDK proxy vs CGLIB
33 / 116
Table
DimensionJDK dynamic proxyCGLIB
MechanismGenerates $Proxy0 implementing interfacesGenerates a subclass of the target
PrerequisiteTarget must implement an interfaceTarget class and methods must not be final
Generation timingGenerated and cached on first callGenerated and cached on first call
PerformanceGreatly optimized since JDK 8; gap now smallFaster historically; slower to create, faster to call
Best forInterface-based design, stable APIsNo interface, or proxying a concrete class
Typical exceptionClassCastException when casting to the implIllegalArgumentException for a final class
34 / 116

One-line memory hook: JDK proxying relies on interfaces, CGLIB relies on inheritance. Whether an interface exists is the first fork in the road.

35 / 116
Section
6. How Spring picks a proxy
36 / 116

Spring AOP centralizes the choice in DefaultAopProxyFactory, which decides before creating a proxy. Simplified:

37 / 116
Code
Codejava
// Core decision in DefaultAopProxyFactory.createAopProxy (simplified)if (config.isOptimize()        || config.isProxyTargetClass()        || hasNoUserSuppliedProxyInterfaces(config)) {    // CGLIB: forced, or the target has no usable interface    return new CglibAopProxy(config);}// Has interfaces and no override -> JDKreturn new JdkDynamicAopProxy(config);
Notes
  • Default rule: target implements an interface → prefer JDK; no interface → CGLIB is the only option
  • Force switch: @EnableAspectJAutoProxy(proxyTargetClass = true) or spring.aop.proxy-target-class=true
  • CGLIB is the default since Spring Boot 2.x: AopAutoConfiguration sets proxy-target-class to true by default, so "inject by implementation class" no longer hits ClassCastException
38 / 116

Three ifs read as abstraction; drawn as a decision tree they become four questions, and each branch maps onto something you have personally seen in your own project:

39 / 116
Diagram
Figure · The decision tree Spring walks
Figure · The decision tree Spring walks
40 / 116

This rule is practical: if an aspect is configured yet you get ClassCastException, it is most likely "a JDK proxy received into an implementation-class field". Either inject by interface, or turn on proxyTargetClass.

41 / 116
Section
7. Benchmarks: don't trust the old conclusion
42 / 116

"JDK proxies are slower than CGLIB" is a widely repeated but outdated claim. Since JDK 8, the Proxy call path has been heavily optimized and the gap has shrunk to the point of being negligible in most business scenarios. Here is a simplified benchmark:

43 / 116
java
@Testvoid proxyThroughput() {    // Critical: warm up so the JIT kicks in, otherwise you measure interpretation    for (int i = 0; i < 200_000; i++) {        jdkProxy.method();        cglibProxy.method();    }    long t1 = System.nanoTime();    for (int i = 0; i < 10_000_000; i++) jdkProxy.method();    long jdk = System.nanoTime() - t1;    long t2 = System.nanoTime();    for (int i = 0; i < 10_000_000; i++) cglibProxy.method();    long cglib = System.nanoTime() - t2;    System.out.printf("JDK=%dms CGLIB=%dms%n", jdk / 1_000_000, cglib / 1_000_000);}
44 / 116

Relative magnitudes on an ordinary machine (for a sense of the trend only; numbers vary with JDK and hardware):

45 / 116
Table
MetricJDK dynamic proxyCGLIBNotes
Proxy creationSlower (reflection + bytecode gen)Slowest (ASM subclass gen)Happens once; negligible
Per-call costSlightly slowerSlightly fasterGap is small since JDK 8
Cold-start overheadLowHigherHeavier class generation
46 / 116
Note

proxy creation happens once at startup; call cost dominates at runtime. Unless you are optimizing a hot path down to nanoseconds, do not agonize over the choice for "performance" — the decisive factor is whether the target has an interface.

47 / 116
Section
8. Traps: casting and injection types
48 / 116
Trap

casting a proxy to the implementation class only works with CGLIB. A JDK proxy is a $Proxy0 that implements interfaces only and does not extend UserServiceImpl, so (UserServiceImpl) proxy throws ClassCastException. Inject by interface type (UserService) rather than implementation type (UserServiceImpl) and you sidestep it at the root.

49 / 116
Trap

CGLIB fails silently on final / private / static methods. They raise no error and fire no advice; you only avoid the issue by remembering that such methods can never be proxied. Conversely, if the target class itself is declared final, CGLIB fails outright — which is at least easier to locate.

50 / 116
Section
9. Try it: generation and calls for both proxies
51 / 116

The demo below switches between JDK and CGLIB and shows both the "proxy signature shape" and the "advice chain" at once. Seen side by side, it is clear that one implements interfaces and the other subclasses, yet both intercept calls the same way in the end.

52 / 116
Kernel lab
TeaVMGenerating and calling both proxiesidle
Switch between JDK and CGLIB and compare the proxy signature and advice chain
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
53 / 116
Animation
Animation · A call through the proxy
Animation · A call through the proxy
54 / 116
Section
10. Decision: should a new project force proxyTargetClass
55 / 116
Decision
Decisiona brand-new Spring Boot 3 project where the team agreed to "inject Services into Controllers and other Services via interface types", but occasionally someone injects an implementation class for convenience. Should proxy creation be forced to `proxyTargetClass=true` (CGLIB everywhere)?
56 / 116
Section
11. Hands-on labs: build both stand-ins once
57 / 116

Run the four experiments in order. The first is the main course — switch between JDK and CGLIB to compare proxy signatures and advice chains, while error shows how the chain behaves when the target throws:

58 / 116
Kernel lab
TeaVMGenerating and calling both proxiesidle
jdk / cglib compare signatures, self shows self-invocation failure, error shows @AfterThrowing on the exception path
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
59 / 116

Only once a stand-in exists does weaving mean anything. weave puts "no aspect / aspect woven / inside the chain / what it costs" side by side for one method, turning Section 3's vocabulary into something visible at runtime:

60 / 116
Kernel lab
TeaVMBefore and after weavingidle
plain → aspect to see the extra layers, chain to dive inside, cost for the price you pay
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
61 / 116

Who puts the case on the phone? autoptr walks the auto-proxy creator intercepting during post-processing, running the candidate check, then producing the object through a proxy factory — making "the AOP proxy is born at after-init" (article 9) an observable timeline:

62 / 116
Kernel lab
TeaVMHow the auto-proxy creator picks your beanidle
bpp for interception timing, candidate for the match test, factory for the JDK/CGLIB fork
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
63 / 116

Finally the combination question you must know: when several aspects hit one method, @Order decides which way the onion is layered — in and out. txaspect covers exactly the @Transactional-versus-custom-aspect trap:

64 / 116
Kernel lab
TeaVMAspect nesting orderidle
compare order vs reverse, inner for the nested timeline, txaspect for who wraps whom
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
65 / 116

That last one is about several stand-ins stacked; we never answered what sits inside a single one. advices hangs all five advice types on one method and shows how they are packed into the proxy's call chain:

66 / 116
Kernel lab
TeaVMThe advice chain is what the stand-in carriesidle
Pick order to watch aspects interleave, then normal to recount why @Around's first half beats @Before
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
67 / 116

One step further back in time: at which exact second does the raw object get swapped for the stand-in? The answer is after initialisation (postProcessAfterInitialization) — the bean is built, its properties are filled, and the shell goes on during the last formality before it is discharged:

68 / 116
Kernel lab
TeaVMWhen exactly the stand-in swaps inidle
Walk the lifecycle in order and watch the object returned by the post-init hook is not the one that went in
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
69 / 116

Section 3 showed the $Proxy0 source sketch and Section 4 animated CGLIB, but "how one call actually travels from your hands into the handler" still has to be walked line by line. Here is the JDK route as a stepping bench: press next and watch the line marked (4) — your code executes for the first time two stack frames after the call site:

70 / 116
Stepper
StepperStepping through $Proxy78: one trip of svc.save(user)1 / 8
Press next in order; your class first appears at line (4), and the real object is only reached at (5)
Code under debug
1UserService svc = ctx.getBean(UserService.class); // svc is really jdk.proxy2.$Proxy78
2svc.save(new User("alice")); // (1) this line of yours is the call site
3// ---- the camera moves into $Proxy78 ----
4public void save(User u) { // (2) the method body generated on the spot
5 super.h.invoke(this, m3, new Object[]{ u }); // (3) pack method id + args, hand off
6// ---- the camera moves into your LogInvocationHandler ----
7System.out.println("[before] " + method.getName()); // (4) pre-work: your code starts here
8Object r = method.invoke(target, args); // (5) reflective call on the real object
9System.out.println("[after] " + r); // (6) post-work
10return r; // (7) back through $Proxy78 to the caller
11} // (8) a checked exception not declared here gets wrapped
Variables now
svc classjdk.proxy2.$Proxy78
Proxy.isProxyClasstrue
targetUserServiceImpl@1a2b3c
Call stack
1JdkProxyDemo.main
1Be suspicious from the very first line: the variable is typed as the interface, yet getBean handed back a class you never wrote. The real target sits quietly in a handler field with no path leading to it.
71 / 116

Six JDK / CGLIB fingerprints, each with a different meaning — position tricks won't help you here, every right-hand entry is distinct:

72 / 116
Match
MatchWhose fingerprint is this?Matched 0/7 · Missed 0
The left column holds symbols and error texts from real logs; claim what each one proves
Pick a card on the left first
73 / 116

From here you can type the commands yourself. This console talks to the same container running in your browser and every line of output is computed by the kernel — boot, then beans to read each class-name suffix, and the lab lines to walk the three routes:

74 / 116
Console
75 / 116
Section
12. Sandbox: three conditions decide which proxy you get
76 / 116

"Which proxy did Spring actually build for me" is not mysticism — it is a combination of exactly these three conditions:

77 / 116
Sandbox
SandboxWill this be a JDK or a CGLIB proxy
Result
# DefaultAopProxyFactory: has interface and not forced -> JDK
proxy = com.sun.proxy.$Proxy78 implements UserService
inject by interface: OK
Verdict: the textbook default combination
The cleanest pairing: interface-oriented design + JDK proxy
78 / 116

Suggested usage: treat the first cell as the baseline, then move exactly one switch at a time. You will discover the two iron laws yourself: ① once proxyTargetClass=true, whether an interface exists stops mattering for the proxy type; ② the receiving variable's type must be compatible with the stand-in produced — that single fact is the entire origin of ClassCastException.

79 / 116
Section
13. Error quick-reference
80 / 116

Half of the errors here explode loudly; the other half are silent — and those are the hard ones.

81 / 116

Start with the stack trace people copy most often:

82 / 116
text
java.lang.ClassCastException: class com.sun.proxy.$Proxy78 cannot be cast to classcom.example.service.UserServiceImpl (com.sun.proxy.$Proxy78 andcom.example.service.UserServiceImpl are in unnamed module of loader 'app')    at com.example.controller.UserController.<init>(UserController.java:23)
83 / 116

How to read it: the first half says the stand-in was built by the JDK (com.sun.proxy.$Proxy78); the second half says you tried to receive it with the implementation type. Those can never be compatible — $Proxy0 extends Proxy and implements interfaces only; it shares no inheritance line with your UserServiceImpl.

84 / 116
Table
Error text (searchable fragment)Real cause30-second self-rescueWhere to dig deeper
java.lang.ClassCastException: class com.sun.proxy.$Proxy78 cannot be cast to class com.example.UserServiceImplA JDK proxy (interface present, CGLIB not forced) received or cast into the implementation typePrefer changing the field to the interface type; if you truly must inject by implementation, set spring.aop.proxy-target-class=true (already the Boot default)Section 6 here
org.springframework.cglib.core.CodeGenerationException: java.lang.IllegalArgumentException: Cannot subclass final class com.example.FinalServiceThe target class is final, so CGLIB cannot create a subclassRemove final; extract an interface so the JDK route applies; or disable proxying for that one beanSection 4 here
@Transactional on a final method: startup fine, no warning, yet the update commits after an exceptionA subclass proxy cannot override a final method, so the advice is silently droppedRemove final; print getClass().getName() to confirm a proxy exists, then audit each method modifierArticle 31
Advice never runs on private / static methodsPrivate and static methods are not interception points for a proxy (static belongs to the class; the proxy holds an instance)Make it a public instance method; if the logic must stay static, move it to a utility called by an advised methodArticle 11, Section 7
this.save(x) inside the same class kills @Transactional / @Cacheable (no error at all)this points at the raw object, not the container's stand-in, so the call bypasses the proxyExtract to another bean; inject your own proxy; or ((MyService) AopContext.currentProxy()).save(x) together with @EnableAspectJAutoProxy(exposeProxy = true)Articles 14 and 15
java.lang.reflect.UndeclaredThrowableException thrown from a proxied call siteInvocationHandler.invoke threw a checked exception not declared on the interface method, so $Proxy0 had to wrap itIn the handler, rethrow RuntimeException/Error untouched and wrap the rest deliberately; or add throws to the interface methodSection 3 here
NoClassDefFoundError: net/sf/cglib/proxy/MethodInterceptor (while hand-writing a CGLIB demo)The sample uses the legacy cglib package names, whereas Spring 5+ ships the relocated org.springframework.cglib.*Switch to org.springframework.cglib.proxy.Enhancer, or simply depend on spring-boot-starter-aopSection 4 here
Weird equals / hashCode / toString behaviour on the proxyThese three are routed to the InvocationHandler too, and the AOP machinery treats them speciallyNever infer business identity from toString(); compare objects by an explicit ID fieldArticle 14
85 / 116
提示

unsure which stand-in you hold? Print svc.getClass().getName(): $Proxy means JDK, $$SpringCGLIB$$0 means CGLIB, and neither means you were never proxied at all.

86 / 116

This article's most famous ClassCastException has a remarkably short stack — the hard part is not reading it but accepting why $Proxy78 is not a UserServiceImpl. Point at the frame you think is guilty:

87 / 116
Triage
Error triageClassCastException: $Proxy78 cannot be cast to UserServiceImpl
Scene of the crime: receiving a JDK stand-in as the implementation type

A colleague flips spring.aop.proxy-target-class back to false (or you are mid-migration from Boot 1.x), and UserController's constructor parameter is the implementation class. The app dies as soon as it starts.

APPLICATION FAILED TO START
java.lang.ClassCastException: class com.sun.proxy.$Proxy78 cannot be cast to class com.example.service.UserServiceImpl (com.sun.proxy.$Proxy78 and com.example.service.UserServiceImpl are in unnamed module of loader 'app')
at com.example.controller.UserController.<init>(UserController.java:23)
at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:211)
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:111)
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:315)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1355)
Click the frame you blame — guessing is allowed
No pressure: guess the exception first, then which line actually made the call.
88 / 116
Section
14. Checkpoints
89 / 116
Quiz
Check yourself`(UserServiceImpl) ctx.getBean(UserService.class)` throws ClassCastException. Most likely reason?
Pick one — you get feedback right away
90 / 116
Quiz
Check yourselfWhich situation makes advice fail SILENTLY (no error, no execution)?
Pick one — you get feedback right away
91 / 116
Section
15. Exercises
92 / 116
Section
Tier 1 · Follow along
93 / 116

Build a JDK dynamic proxy yourself, then dump the $Proxy0 the JVM manufactures and actually read it — this single step creates the felt sense that "the stand-in is a real class". Fully runnable code:

94 / 116
java
package com.example.proxy;public interface UserService {    String find(Long id);}public class UserServiceImpl implements UserService {    @Override    public String find(Long id) {        return "user-" + id;    }}
95 / 116
java
package com.example.proxy;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.util.Arrays;public class JdkProxyLab {    public static void main(String[] args) throws Throwable {        UserService target = new UserServiceImpl();        UserService proxy = (UserService) Proxy.newProxyInstance(                target.getClass().getClassLoader(),                new Class<?>[]{ UserService.class },                new InvocationHandler() {                    @Override                    public Object invoke(Object p, Method m, Object[] a) throws Throwable {                        System.out.println("[handler] call received: " + m.getName()                                + " args=" + Arrays.toString(a));                        Object r = m.invoke(target, a);       // reflection reaches the impl                        System.out.println("[handler] returned: " + r);                        return r;                    }                });        System.out.println("[class] " + proxy.getClass().getName());        System.out.println("[ifaces] " + Arrays.toString(proxy.getClass().getInterfaces()));        System.out.println("[isProxy?] " + Proxy.isProxyClass(proxy.getClass()));        System.out.println("[call ] " + proxy.find(7L));        // verify the "cannot cast to the impl" law with your own hands        try {            UserServiceImpl bad = (UserServiceImpl) proxy;            System.out.println("[bad] the cast actually worked?" + bad);        } catch (ClassCastException e) {            System.out.println("[bad] " + e.getMessage());        }    }}
96 / 116

Expected output:

97 / 116
text
[class] jdk.proxy2.$Proxy7[ifaces] [interface com.example.UserService][isProxy?] true[handler] call received: find args=[7][handler] returned: user-7[call ] user-7[bad] class jdk.proxy2.$Proxy7 cannot be cast to class com.example.UserServiceImpl ...
98 / 116

Now write the generated proxy class to disk (the Java 9+ flag):

99 / 116
text
-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true
100 / 116

After the run you get jdk/proxy2/$Proxy7.class in the working directory. Decompiling it with javap -p jdk.proxy2.$Proxy7 shows h.invoke(this, ...) inside every interface method body — line for line what Section 3 illustrated.

101 / 116
Section
Tier 2 · Variant
102 / 116

Goal: prove that advice on a final method dies silently.

103 / 116

Hint: duplicate UserServiceImpl.find as public final String findCached(Long id), print the method name inside the handler, then call both methods once through the interface. To check the Spring case, add @Transactional public final void tx() to the implementation and call it from outside.

104 / 116

You should observe: non-final find passes through the handler every time; findCached, exposed through the interface, is still intercepted (the JDK proxy only looks at interfaces) — but under CGLIB, tx() triggers not one piece of advice, with no error and no warning. That is precisely why this section keeps insisting: print getClass().getName(), then audit method modifiers one by one.

105 / 116
Section
Tier 3 · Build one
106 / 116

Build a toy "mini IoC + AOP" container: using JDK dynamic proxies only, give every interface in the registry a "logging + timing" layer, switchable via -Dxxx.proxy=jdk|none.

107 / 116

Acceptance checklist:

108 / 116
  • [ ] Beans live in a Map<String, Object>; on retrieval, wrap them with Proxy.newProxyInstance when an enhancement applies
  • [ ] Two consecutive getBean calls return the same proxy instance (cache the proxy, do not rebuild it)
  • [ ] The proxy casts fine to the interface type but fails against the implementation type — encode that difference as a one-line test assertion
  • [ ] Your handler special-cases equals / hashCode / toString, and you explain why it must
  • [ ] You can answer: if some implementation has no interface at all, can this toy container still enhance it? What would you do instead?
109 / 116
Section
16. Self-check
110 / 116
自检

can you recite the three arguments of Proxy.newProxyInstance (classloader, interface array, handler) and say why the second may only contain interfaces?

111 / 116
自检

what is the single line inside a $Proxy0 method body? (h.invoke(this, method, args)) — write it down and you understand JDK proxying completely.

112 / 116
自检

what are Spring's three reasons to choose CGLIB? (forced proxyTargetClass, optimize, or no usable interface)

113 / 116
自检

how does the failure mode of a final class differ from that of a final method? (the former throws at startup, the latter dies silently)

114 / 116
自检

what are the two escape routes from ClassCastException: cannot be cast to ...Impl? (inject by interface; or enable proxyTargetClass)

115 / 116
口诀

JDK leans on interfaces, CGLIB on inheritance; a final class explodes on the spot, a final method goes quiet; casting the stand-in to the person always fails, and a self-call is never heard.

116 / 116
Summary

dynamic proxies are the engine of Spring AOP, and there are only two routes — the JDK proxy uses Proxy.newProxyInstance to build a $Proxy0 that implements interfaces and forwards calls to an InvocationHandler; CGLIB uses Enhancer to build a subclass of the target and intercepts with a MethodInterceptor. The decisive factor is whether the target has an interface; Spring defaults to JDK with interfaces and CGLIB without, and Spring Boot 2.x defaults to CGLIB. Two traps are enough to remember: casting to the implementation class only works with CGLIB, and final / private / static methods cannot be intercepted by CGLIB.