Table of contents
Open Table of contents
Article body
Let us revisit the proxy pattern and some misconceptions I had about it.
When learning Spring at university, I remember that many features depended on JDK dynamic proxies. As I wrote more code, I adopted the layered structure still common today.
controller -> service -> dao: each layer calls the next. When a controller invokes the service layer, the actual business logic is in the serviceImpl implementation.
Misconception 1: I thought serviceImpl was the dynamic proxy for service, and service was the proxy class. I believed this for a surprisingly long time. In fact, serviceImpl is the target being proxied, while $Proxy
How I resolved it: inject XXXService with @Autowired in XXXController. During startup, Spring must instantiate the service dependency before constructing the controller bean. Debugging showed that without annotations such as @Transactional, it injects the ordinary service implementation. Such annotations can be implemented with JDK dynamic proxies: to apply transaction aspects to ServiceImpl, Spring generates a proxy for it. Set breakpoints in the methods below to follow proxy creation step by step. You can also see why names specified with @Service use a lowercase initial letter.
org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessAfterInitialization
org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#wrapIfNecessary
Misconception 1, note 1: this is a proxy for ServiceImpl, not for Service. That distinction matters.
Misconception 1, note 2: if several ServiceImpl classes implement Service, use @Qualifier to tell Spring which named bean to inject.
Background of the proxy pattern:
How would JVM A call a method in JVM B? This is one setting where the proxy pattern arose.
Early RMI, discussed in Head First’s proxy chapter, uses the following approach—a remote proxy:
-
Start a shared registry accessible to JVM A and JVM B.
-
When JVM B starts, register its service implementation with Naming.rebind(“ServiceB”,ServiceImpl).
-
In JVM A, look up the class with Naming.lookup(“ServiceB”) and receive the ServiceImpl instance through its interface using Java polymorphism:
ServiceB service = Naming.lookup("ServiceB");
// The lookup returns an implementation of the interface, conceptually like:
ServiceB service = new ServiceBImpl();
- Invoke the relevant method on the obtained service instance.
Does that sound a little like a modern microservice registry?
A virtual proxy:
Suppose obtaining an object requires expensive construction. Until construction finishes, report the current state to its caller, then return the object as soon as it is ready.
-
Expose only a method for obtaining the object.
-
Put the actual retrieval and other operations inside that method.
-
To the caller, the object simply comes from invoking that method.
The essential logic looks like this:
// Interface
public Interface A(){
String printMessage();
}
// Proxy exposed to the caller
public class Aproxy implements A {
ArealImpl aRealImpl;
public String printMessage(){
if(TIME_NOT_REACHED){
// Other operations
}else{
aRealImpl = new ArealImpl();
return aRealImpl.printMessage();
}
return "Nothing obtained";
}
}
// Actual implementation that obtains the object
public class ArealImpl implements A {
public String printMessage(){
return "Obtain the object";
}
}
Does this virtual proxy resemble a JDK dynamic proxy?
Recommendation: reread the proxy chapter in Head First carefully until you understand it fully.
With the pattern’s background in mind, let us use JDK dynamic proxies.
JDK dynamic proxies in detail:
There are two usages: based on an interface implementation, or based on an interface alone.
- Based on an implementation (Impl)
public interface ProxyService {
String print(String message);
}
public class ProxyServiceImpl implements ProxyService {
@Override
public String print(String message) {
return message;
}
Implement a custom handler.
public class CommonProxyHandler implements InvocationHandler {
private Object target;
public CommonProxyHandler(Object tarject) {
this.target = tarject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(target,args);
}
}
Then use it:
@SpringBootTest
public class CommonProxyTest {
@Test
public void testCommonProxy(){
// Java polymorphism lets us refer to a ProxyServiceImpl instance through the interface
ProxyService proxyService = new ProxyServiceImpl();
// Pass the instantiated interface implementation, ProxyServiceImpl, to the handler
CommonProxyHandler commonProxyHandler = new CommonProxyHandler(proxyService);
// Argument 1: classloader
// Argument 2: interfaces implemented by the target
// Argument 3: handler object
// Returns a proxy implementing those interfaces
ProxyService proxyInstance = (ProxyService) Proxy.newProxyInstance(ProxyServiceImpl.class.getClassLoader(),
proxyService.getClass().getInterfaces(), commonProxyHandler);
String s = proxyInstance.print("我是一个小代理");
System.out.println(s);
}
}
This is similar to most introductory JDK proxy examples.
Can you think of questions raised by the example?
-
What is the generated proxy’s type: ProxyService or ProxyServiceImpl?
-
Why pass ProxyServiceImpl to CommonProxyHandler? Can we pass ProxyService, the interface, instead?
-
What are the roles of the classloader, interface array, and custom handler passed to Proxy.newProxyInstance?
-
Should invoke do nothing but method.invoke?
Start with question 3. To understand those arguments, inspect newProxyInstance.
There are two main steps: create the proxy class, then create an instance of it.

The JDK first searches its cache for a generated implementation of the target interface, returning it if present and creating it otherwise. Step into getProxyClass0 with the classloader and interface array we supplied; this is where they are used.

Next, ProxyClassFactory.apply() inside factory.get() generates the proxy class file and loads it into the JVM.

generateProxyClass generates the proxy bytecode.
getProxyClass0 is now complete: using our interface and classloader, it creates a class implementing the interface and loads it into the JVM.
Use ProxyGenerator.generateProxyClass locally to inspect the generated proxy:
@SpringBootTest
public class GenerateProxyClassTest {
@Test
public static void main(String[] args) throws Exception{
String path = "/Users/jacksparrow414/Downloads/$Proxy1.class";
ProxyService proxyService = new ProxyServiceImpl();
CommonProxyHandler commonProxyHandler = new CommonProxyHandler(proxyService,path);
ProxyService proxyInstance = (ProxyService) Proxy.newProxyInstance(ProxyServiceImpl.class.getClassLoader(), ProxyServiceImpl.class.getInterfaces(), commonProxyHandler);
byte[] proxies = ProxyGenerator.generateProxyClass("$Proxy1", ProxyServiceImpl.class.getInterfaces());
FileOutputStream outputStream = null;
outputStream = new FileOutputStream(path);
outputStream.write(proxies);
outputStream.flush();
outputStream.close();
}
}
Find the generated $Proxy1 class file and open it in IDEA.
// The generated proxy implements ProxyService and has a type named $Proxy followed by a number
public final class $Proxy1 extends Proxy implements ProxyService {
private static Method m1;
private static Method m2;
private static Method m0;
private static Method m3;
// Construct the object by passing the handler to its constructor
public $Proxy1(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
// Implements the interface method
public final String print(String var1) throws {
try {
// Calling the proxy invokes the custom handler’s invoke method
return (String)super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
m3 = Class.forName("com.example.mybatis.demomybatis.service.ProxyService").getMethod("print", Class.forName("java.lang.String"));
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
The generated class implements the target interface. To call methods, we need an instance, and its constructor takes an InvocationHandler.
This explains why we supply a custom handler: the proxy’s constructor receives it when creating the instance.
The methods also show that interface calls delegate to the handler’s invoke method, which is why we override invoke.
Question 3 is answered, as is question 1: the concrete type is neither ProxyService nor ProxyServiceImpl, but $Proxy
For question 4, method.invoke receives the proxyServiceImpl object and directly invokes its method. If invoke contains only that one line, it adds nothing over calling proxyServiceImpl.method() directly. Normally, add shared operations unrelated to individual business logic:
public class CommonProxyHandler implements InvocationHandler {
private Object target;
public CommonProxyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Start the transaction manager
The first argument to method.invoke is the target object, target. Passing proxy instead causes repeated calls to the proxy’s own invoke method.
Object invoke = method.invoke(target, args);
Commit/roll back the transaction
return invoke;
}
}
Adding common operations before and after business logic is similar to Spring AOP.
Because the proxy implements the interface, Java polymorphism allows its object to be referenced through that interface type and to call the interface methods.
We will return to question 2 last.
Now the second approach:
- Based on an interface (Interface)
public interface SchoolService {
String getString(String message);
}
public class ProxyHandler<T> implements InvocationHandler {
private Class<T> proxyInterface;
public ProxyHandler(Class<T> proxyInterface) {
this.proxyInterface = proxyInterface;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
String s = null;
System.out.println(proxyInterface.getName());
if ("getString".equals(method.getName())) {
s = args[0].toString();
System.out.println(s);
}
return s;
}
}
This handler has no method.invoke call. As explained above, that call requires an object implementing the interface. Here no implementation exists apart from the proxy itself, so using it would fail; an interface needs an implementation.
The operations within invoke effectively provide what an implementation of the interface would do.
Usage:
@SpringBootTest
public class NoImplProxyTest {
@Test
public static void main(String[] args) {
ProxyHandler proxyHandler = new ProxyHandler(SchoolService.class);
SchoolService proxyInstance = (SchoolService) Proxy
.newProxyInstance(SchoolService.class.getClassLoader(),
new Class[]{SchoolService.class}, proxyHandler);
proxyInstance.getString("测试接口没有实现类的代理");
}
}
You can also inspect the proxy generated from an interface alone:
@SpringBootTest
public class GenreateProxyOnlyInterfaceTest {
@Test
public static void main(String[] args) throws Exception{
String path = "/Users/jacksparrow414/Downloads/$Proxy2.class";
ProxyHandler proxyHandler = new ProxyHandler(SchoolService.class);
SchoolService proxyInstance = (SchoolService) Proxy
.newProxyInstance(SchoolService.class.getClassLoader(),
new Class[]{SchoolService.class}, proxyHandler);
byte[] proxies = ProxyGenerator.generateProxyClass("$Proxy2",new Class[]{SchoolService.class});
FileOutputStream outputStream;
outputStream = new FileOutputStream(path);
outputStream.write(proxies);
outputStream.flush();
outputStream.close();
}
}
Proxy generated from the interface:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
import com.example.mybatis.demomybatis.service.SchoolService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy2 extends Proxy implements SchoolService {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
public $Proxy2(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String getString(String var1) throws {
try {
return (String)super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("com.example.mybatis.demomybatis.service.SchoolService").getMethod("getString", Class.forName("java.lang.String"));
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
Whether based on an interface or its implementation, the generated proxy has the same structure and implements the interface. Actual behavior depends on the custom handler.
Thus, either can be supplied for question 2. When using the interface alone, however, you cannot use method.invoke for the implementation; write the operations yourself.
Also, the custom handler’s constructor can take multiple arguments, according to the scenario.
How do the use cases differ between implementation-based and interface-only JDK proxies?
- In my view, implementation-based proxies closely relate to business operations. They extract shared operations and run them before or after business methods, keeping one copy of common code that applies to every operation—for example, Spring AOP, transaction annotations, and logging.
method.invoke() executes the actual business method.
- Interface-only proxies abstract a standardized workflow rather than attaching to existing business logic. MyBatis mapperProxy and Feign’s feignInvocationHandler are classic examples of highly abstracted frameworks for a particular flow.
A few final points:
-
Dynamically generated Java proxies are usually named $Proxy
, easily recognized during debugging. In an InvocationHandler implementation, method.invoke() inside invoke is where ServiceImpl actually executes. Java calls the target implementation’s method; put extra operations before or after it. -
method.invoke takes the object that owns the method and the argument values: method.invoke(“OBJECT_OWNING_THE_METHOD”, methodArguments), not the interface itself.
Remember that passing an interface alone is useless here and causes an error. An abstract interface cannot carry out a concrete action without an instance implementing it.
- Consider try-catch around method.invoke, since Java may throw an exception and prevent the call. An error might look like:
Method threw 'java.lang.reflect.UndeclaredThrowableException' exception. Cannot evaluate com.sun.proxy.$Proxy<number>....
- If the handler’s invoke contains only method.invoke and no custom operations before or after it, it adds nothing over calling ServiceImpl directly.
Apart from the first remote-proxy example, which clearly resembles the pattern, the other proxy examples essentially add operations before or after actual business logic. They are nevertheless classified as proxy patterns.
A small realization: Spring AOP makes remarkably sophisticated use of proxies. Its core is a proxy pattern, and transaction management, asynchronous operations, and related features build on the same foundation.
Simplify something complex, then take that simple idea as far as it can go. That is impressive!
The next article examines how MyBatis uses JDK proxies.