05 Dynamic Proxy Hiding Rpc Processing Flow Through Interface Oriented Programming

05 Dynamic Proxy - Hiding RPC processing flow through interface-oriented programming #

Hello, I am He Xiaofeng. In the previous lecture, I shared about network communication, which is actually quite simple to understand. RPC is used to solve communication between two applications, and the network is the “bridge” between two machines. Only when the bridge is built can we transmit request data from one end to the other. In terms of network communication, you just need to remember one keyword - reliable transmission.

Now, continuing from the previous content, let’s talk about the application of dynamic proxy in RPC.

If I ask you, do you know about dynamic proxy? You may be able to tell me about the purpose and benefits of dynamic proxy without any problem. Now, let me ask you this: have you used dynamic proxy in your projects? At this point, some people may hesitate. So, let me ask you in a different way: have you implemented unified interception in your projects? For example, authorization authentication, performance statistics, etc. You may immediately recall that you have implemented it and that you know that you can use Spring’s AOP functionality to achieve it.

That’s right, let’s think further. How do we achieve the effect of unified interception in Spring AOP? And how can we achieve decoupling between non-business logic and business logic without modifying the original code? The key here is to use dynamic proxy technology to enhance the bytecode and intercept method calls in order to add the additional processing logic we need before and after the method calls.

Now, coming back to the point, what is the relationship between dynamic proxy and RPC?

The Magic of Remote Invocation #

Let me give you a specific scenario, and you may understand it.

In a project, when we need to use RPC (Remote Procedure Call), our common practice is to first request the interface from the service provider. We then add the interface as a dependency to our project through Maven or other tools. When writing business logic, if we need to call the provider’s interface, we simply inject the interface into the project through dependency injection, and then directly call the methods of the interface in our code.

As we all know, the interface does not contain the actual business logic; it resides in the service provider’s application. But by calling the interface methods, we do get the desired results. It feels a bit magical, doesn’t it? Now let’s think about how this magic is achieved in RPC.

The key technology used here is dynamic proxy, as mentioned earlier. RPC automatically generates a proxy class for the interface. When we inject the interface into our project, it is actually bound to the proxy class generated for this interface during runtime. This way, when the interface method is called, it is actually intercepted by the generated proxy class. This allows us to add remote invocation logic within the generated proxy class.

By using this “switching columns” technique, we can shield users from the details of remote invocation, achieving an experience similar to calling a local service. The overall process is illustrated in the following diagram:

Implementation Principle #

Dynamic proxy plays a magical role in RPC. Now, let me reveal to you how it works. After that, it will be easy for you to apply it.

Let’s take Java as an example and look at a specific example. The code is as follows:

/**
 * The interface to be proxied
 */
public interface Hello {
    String say();
}

/**
 * The real object to be invoked
 */
public class RealHello {

    public String invoke(){
        return "I'm a proxy";
    }
}

/**
 * JDK proxy class generation
 */
public class JDKProxy implements InvocationHandler {
    private Object target;

    JDKProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] paramValues) {
        return ((RealHello)target).invoke();
    }
}

/**
 * Test example
 */
public class TestProxy {

    public static void main(String[] args){
        // Create a proxy
        JDKProxy proxy = new JDKProxy(new RealHello());
        ClassLoader classLoader = ClassLoaderUtils.getCurrentClassLoader();
        // Save the generated proxy class to a file
        System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
        // Generate the proxy class
        Hello test = (Hello) Proxy.newProxyInstance(classLoader, new Class[]{Hello.class}, proxy);
        // Method invocation
        System.out.println(test.say());
    }
}

The purpose of this code is to generate a dynamic proxy class for the Hello interface and call the say() method of the interface. But the real return value comes from the invoke() method inside RealHello. You see, with just 50 lines of code, this function is implemented, which is quite interesting, isn’t it?

Since the focus is on the generation of proxy classes, let’s take a look at what happens inside Proxy.newProxyInstance?

Let’s take a look at the following flowchart. You can refer to the JDK source code for the specific code details (the class and method mentioned above can be located directly). I have organized it based on the 1.7.X version.

In the part where bytecode is generated, that is, in the ProxyGenerator.generateProxyClass() method, we can see from the code that the parameter saveGeneratedFiles is used to control whether to save the generated bytecode to the local disk. In order to understand the essence of the proxy more intuitively, we need to set the value of this parameter to true. However, the value of this parameter is controlled by the Property with the key “sun.misc.ProxyGenerator.saveGeneratedFiles”. The dynamically generated classes will be saved in the com/sun/proxy directory under the root directory of the project. Now, let’s find the $Proxy0.class generated just now and open the class file with a decompiler tool. You will see the following code:

package com.sun.proxy;

import com.proxy.Hello;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Hello {
  private static Method m3;
  
  private static Method m1;
  
  private static Method m0;
  
  private static Method m2;
  
  public $Proxy0(InvocationHandler paramInvocationHandler) {
    super(paramInvocationHandler);
  }
  
  public final String say() {
    try {
      return (String)this.h.invoke(this, m3, null);
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }
  
  public final boolean equals(Object paramObject) {
    try {
      return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }
  
  public final int hashCode() {
    try {
      return ((Integer)this.h.invoke(this, m0, null)).intValue();
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }
  
  public final String toString() {
    try {
      return (String)this.h.invoke(this, m2, null);
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }
  
  static {
    try {
      m3 = Class.forName("com.proxy.Hello").getMethod("say", new Class[0]);
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
      return;
    } catch (NoSuchMethodException noSuchMethodException) {
      throw new NoSuchMethodError(noSuchMethodException.getMessage());
    } catch (ClassNotFoundException classNotFoundException) {
      throw new NoClassDefFoundError(classNotFoundException.getMessage());
    } 
  }
}

We can see that the $Proxy0 class has a say() method with the same signature as Hello. The this.h inside it is bound to the JDKProxy object passed in just now. So when we call Hello.say(), it is actually forwarded to JDKProxy.invoke(). At this point, the whole magic process becomes transparent.

Implementation methods #

In the Java field, besides the default InvocationHandler provided by the JDK, there are also many third-party frameworks that can accomplish proxy functionality, such as Javassist and Byte Buddy.

Strictly speaking, the default proxy functionality provided by the JDK has certain limitations. It requires the class being proxied to be an interface. The reason for this is that the generated proxy class will inherit from the Proxy class, but Java does not support multiple inheritance.

This limitation is crucial in the context of RPC applications because, for the service invoker, programming is typically done towards interfaces when using RPC, as we discussed earlier. The biggest issue with using the JDK’s default proxy functionality is performance. The generated proxy class uses reflection to invoke methods, and this approach is relatively slower compared to direct method calls. However, it should be noted that starting from JDK 8, there has been significant performance improvement in reflective invocations. Thus, there is still room for expectations.

Compared to the JDK’s built-in proxy functionality, Javassist specializes in manipulating low-level bytecode. Consequently, using Javassist is not as straightforward, and generating dynamic proxy classes can be somewhat complex. However, the advantage is that the use of bytecode generation with Javassist eliminates the need for reflective method invocations, resulting in better performance. One thing to be aware of when using Javassist is that once a proxy class is generated using CtClass, it will be frozen and no further modifications are allowed. Otherwise, an error will occur when generating it again.

On the other hand, Byte Buddy is a newcomer in this area. It is utilized in many excellent projects, such as Spring and Jackson, to accomplish low-level proxying. Compared to Javassist, Byte Buddy provides a more user-friendly API and higher code readability. Most importantly, the generated proxy classes using Byte Buddy execute faster than those generated by Javassist.

Although the aforementioned three frameworks differ significantly in usage, their core principles are quite similar. The difference lies only in how the proxy classes are generated and how method invocations are handled within the generated proxy classes. Moreover, it is precisely because of these subtle differences that different proxy frameworks exhibit different performance capabilities. Therefore, when designing an RPC framework, it is necessary to make some comparisons and select a framework based on their pros and cons as well as your specific requirements and use cases.

Summary #

Today we introduced the application of dynamic proxy in RPC. Although it is just a specific implementation technique, I believe that understanding how method calls are intercepted is essential to clarify how we achieve programming against interfaces in RPC and help users shield the details of RPC calls. Ultimately, we aim to provide users with a programming experience for remote invocation that is similar to calling local methods.

Since dynamic proxy is a specific technology framework, there are several considerations to take into account when selecting it:

  • Because proxy classes are generated at runtime, the speed of generating proxy classes and the size of generated bytecode will affect its performance – the smaller the generated bytecode, the fewer resources it will consume.
  • The generated proxy classes are used for intercepting interface method requests, so the execution efficiency of the generated proxy classes needs to be high when calling interface methods.
  • Lastly, from our usage perspective, we certainly hope to choose a proxy class framework that is convenient to use, such as API design comprehensibility, community activity, and dependency complexity, among other factors.

Finally, I want to emphasize again: although dynamic proxy may seem like a small technical detail in RPC, it is this innovation that allows users to not focus on the details. In fact, we do the same in our daily interface design – we try our best to shield the details from the callers and make the caller’s integration as simple as possible. For example, when designing an interface for publishing products, you don’t need to expose certain details to the users, such as how the product data is stored.

Reflection #

Please imagine, if we didn’t have dynamic proxies to help us intercept method calls, how would the user accomplish RPC calls?

Feel free to leave a message and share your answer with me. Also, you are welcome to share this article with your friends and invite them to join the learning. See you in the next class!