Skip to content
JackSparrow414
Go back

Relearning MyBatis (Part 4): JDK Dynamic Proxies in Detail

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.class is the proxy. I had not understood the meaning and background of the pattern.

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:

  1. Start a shared registry accessible to JVM A and JVM B.

  2. When JVM B starts, register its service implementation with Naming.rebind(“ServiceB”,ServiceImpl).

  3. 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();
  1. 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.

  1. Expose only a method for obtaining the object.

  2. Put the actual retrieval and other operations inside that method.

  3. 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.

  1. 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?

  1. What is the generated proxy’s type: ProxyService or ProxyServiceImpl?

  2. Why pass ProxyServiceImpl to CommonProxyHandler? Can we pass ProxyService, the interface, instead?

  3. What are the roles of the classloader, interface array, and custom handler passed to Proxy.newProxyInstance?

  4. 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.

JDK Proxy.newProxyInstance source creating a proxy class and instantiating the proxy object

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.

JDK dynamic proxy source retrieving a cached proxy class and calling factory.get

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

ProxyClassFactory.apply source generating and loading proxy class bytecode

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, which implements ProxyService.

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:

  1. 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?

  1. 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.

  1. 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:

  1. 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.

  2. 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.

  1. 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>....
  1. 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.


Share this post:

Continue this series

Relearning MyBatis

  1. Relearning MyBatis (Part 1)
  2. Relearning MyBatis (Part 2)
  3. Relearning MyBatis (Part 3)
  4. Relearning MyBatis (Part 4): JDK Dynamic Proxies in DetailYou are here
  5. Relearning MyBatis (Part 5)

Comments

Questions, corrections, and experiences are welcome. Sign in with GitHub to comment; both language versions share this discussion.

Comments are available on the live site only.