28 Simplifying Complex Problems Proxy Hides Many Underlying Details for You

28 Simplifying Complex Problems Proxy Hides Many Underlying Details for You #

In the previous introduction to the implementation of DubboProtocol, we learned that the Protocol layer and the Cluster layer, which are exposed, are internal concepts of Dubbo and cannot be directly used by the business layer. In order to seamlessly integrate the business logic with Dubbo, we need to bridge the gap between the business logic and the internal concepts of Dubbo. This is where the functionality of dynamically generating proxy objects comes into play. The Proxy layer in the Dubbo architecture is located as shown below (although the Proxy layer is far from the Protocol layer in the architecture diagram, the actual code implementation of Proxy is located in the dubbo-rpc-api module):

Drawing 0.png

Location of the Proxy layer in the Dubbo architecture

When the Consumer makes a call, Dubbo will use dynamic proxy to convert the business interface implementation object into the corresponding Invoker object, which will then be used in the Cluster layer and the Protocol layer. When the Provider exposes a service, there is also a conversion between the Invoker object and the business interface implementation object, which is also achieved through dynamic proxy.

Common solutions for implementing dynamic proxy are: JDK dynamic proxy, CGLib dynamic proxy, and Javassist dynamic proxy. These solutions are still widely used. For example, Hibernate uses Javassist and CGLib at the lower level, Spring uses CGLib and JDK dynamic proxy, and MyBatis uses JDK dynamic proxy and Javassist.

In terms of performance, Javassist and CGLib have similar implementations, both of which are more efficient than JDK dynamic proxy. The specific performance advantage depends on the specific machine, JDK version, and the specific implementation of the test benchmark.

Dubbo provides two ways to implement proxies, namely JDK dynamic proxy and Javassist. We can see the corresponding factory classes in the proxy package, as shown in the following figure:

Drawing 1.png

Location of the core implementation of ProxyFactory

After understanding the necessity of Proxy and the two proxy generation methods provided by Dubbo, let’s delve into the implementation of the Proxy layer.

ProxyFactory #

Regarding the ProxyFactory interface, we have already introduced it in Lesson 23, so let’s briefly review it here. ProxyFactory is an extension interface that defines two core methods: getProxy(), which creates a proxy object for the Invoker object passed in; and getInvoker(), which encapsulates the proxy object as an Invoker object.

@SPI("javassist")

public interface ProxyFactory {

    // Creates a proxy object for the given Invoker object

    @Adaptive({PROXY_KEY})

    <T> T getProxy(Invoker<T> invoker) throws RpcException;

    @Adaptive({PROXY_KEY})

    <T> T getProxy(Invoker<T> invoker, boolean generic) throws RpcException;

    // Encapsulates the given proxy object as an Invoker object

    @Adaptive({PROXY_KEY})

    <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) throws RpcException;

}

Based on the @SPI annotation on ProxyFactory, we know that the default implementation uses Javassist to create code objects.

AbstractProxyFactory is the abstract class of the proxy factory, and its inheritance relationship is shown in the following figure:

Drawing 2.png

Inheritance diagram of AbstractProxyFactory

AbstractProxyFactory #

AbstractProxyFactory mainly handles the interfaces that need to be proxied, and the specific implementation is in the getProxy() method:

public <T> T getProxy(Invoker<T> invoker, boolean generic) throws RpcException {

    Set<Class<?>> interfaces = new HashSet<>();// Records the interfaces to be proxied

    // Get the interfaces specified in the 'interfaces' parameter of the URL

    String config = invoker.getUrl().getParameter(INTERFACES);

    if (config != null && config.length() > 0) {

        // Split the 'interfaces' parameter on commas to get the interface collection

        String[] types = COMMA_SPLIT_PATTERN.split(config);

        for (String type : types) { // Record information about these interfaces

            interfaces.add(ReflectUtils.forName(type));

        }

    }

    if (generic) { // Handling of generic interfaces

        if (!GenericService.class.isAssignableFrom(invoker.getInterface())) {

            interfaces.add(GenericService.class);

        }

        // Get the interface specified in the 'interface' parameter of the URL

        String realInterface = invoker.getUrl().getParameter(Constants.INTERFACE);

        interfaces.add(ReflectUtils.forName(realInterface));

    }

    // Get the interface specified in the 'type' field of the Invoker

    interfaces.add(invoker.getInterface());

    // Add the default interfaces: EchoService and Destroyable

    interfaces.addAll(Arrays.asList(INTERNAL_INTERFACES));

    // Call the overloaded getProxy() method of the abstract class
return getProxy(invoker, interfaces.toArray(new Class<?>[0]));

After obtaining the interfaces that need to be proxied from multiple places, AbstractProxyFactory calls the getProxy() method implemented by the subclass to create the proxy object.

The implementation of getProxy() in JavassistProxyFactory is relatively simple, directly delegated to the Proxy utility class in the dubbo-common module to generate the proxy class. Now let’s analyze the entire process of generating the proxy class.

Proxy #

In the dubbo-common module, the getProxy() method in Proxy provides the core implementation for dynamically creating the proxy class. This process is quite long, so in order to help you understand it better, we will break it down and analyze it step by step.

First, it searches the proxy class cache PROXY_CACHE_MAP (a WeakHashMap<ClassLoader, Map<String, WeakReference<Proxy>>>), where the first level key is a ClassLoader object, the second level key is a concatenation of the interfaces obtained above, and the value is a WeakReference to the cached proxy class.

The characteristic of WeakReference is that the referenced object’s lifetime is between two garbage collections (GCs). In other words, when the garbage collector scans an object that only has weak references, it will reclaim that object regardless of whether the current memory space is sufficient or not. (Since the garbage collector is a low-priority thread, it may not quickly discover objects that only have weak references.)

The characteristic of WeakReference makes it particularly suitable for use in memory caches where data can be recovered. The results of searching the cache can be summarized as follows:

  • If the information is not found in the cache, it will add a PENDING_GENERATION_MARKER placeholder to the cache. The current thread will create the proxy class and eventually replace the placeholder.
  • If a PENDING_GENERATION_MARKER placeholder is found in the cache, it means that another thread is already generating the corresponding proxy class. The current thread will block and wait.
  • If a complete proxy class is found in the cache, it will return directly without generating the dynamic proxy class.

The following code snippet shows the relevant code for querying the cache in the Proxy.getProxy() method:

public static Proxy getProxy(ClassLoader cl, Class<?>... ics) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < ics.length; i++) {
        String itf = ics[i].getName();
        if (!ics[i].isInterface()) {
            throw new RuntimeException(itf + " is not an interface.");
        }
        Class<?> tmp = Class.forName(itf, false, cl);
        if (tmp != ics[i]) {
            throw new IllegalArgumentException("...");
        }
        sb.append(itf).append(';');
    }
    String key = sb.toString();
    final Map<String, Object> cache;
    synchronized (PROXY_CACHE_MAP) {
        cache = PROXY_CACHE_MAP.computeIfAbsent(cl, k -> new HashMap<>());
    }
    Proxy proxy = null;
    synchronized (cache) {
        do {
            Object value = cache.get(key);
            if (value instanceof Reference<?>) {
                proxy = (Proxy) ((Reference<?>) value).get();
                if (proxy != null) {
                    return proxy;
                }
            }
            if (value == PENDING_GENERATION_MARKER) {
                cache.wait();
            } else {
                cache.put(key, PENDING_GENERATION_MARKER);
                break;
            }
        } while (true);
    }
    // Further logic for dynamically generating the proxy class
}

After completing the caching lookup, let’s take a look at the process of generating the proxy class.

In the first step, we call the ClassGenerator.newInstance() method to create a ClassPool corresponding to the ClassLoader. ClassGenerator encapsulates the basic operations of Javassist and defines many fields to temporarily store information about the proxy class. In its toClass() method, this stored information is used to dynamically generate the proxy class. Let’s briefly explain these fields:

  • mClassName (String type): The class name of the proxy class.
  • mSuperClass (String type): The name of the superclass of the proxy class.
  • mInterfaces (Set type): The interfaces implemented by the proxy class.
  • mFields (List type): The fields in the proxy class.
  • mConstructors (List type): Information about all constructors in the proxy class, including the specific implementation of the constructors.
  • mMethods (List type): Information about all methods in the proxy class, including the specific implementation of the methods.
  • mDefaultConstructor (boolean type): Indicates whether the default constructor generated by the proxy class.

In the toClass() method of ClassGenerator, the above fields are used to generate the proxy class using Javassist. The specific implementation is as follows:

public Class<?> toClass(ClassLoader loader, ProtectionDomain pd) {
    if (mCtc != null) {
        mCtc.detach();
    }
    // When inheriting from the superclass, use this ID as a suffix to prevent naming conflicts
    long id = CLASS_NAME_COUNTER.getAndIncrement(); 
    CtClass ctcs = mSuperClass == null ? null : mPool.get(mSuperClass);
    if (mClassName == null) {
        mClassName = (mSuperClass == null || javassist.Modifier.isPublic(ctcs.getModifiers()) ? 
            ClassGenerator.class.getName() :
            mSuperClass + "$sc") + id;
    }
    mCtc = mPool.makeClass(mClassName); // Create CtClass for generating the proxy class
    if (mSuperClass != null) {
        mCtc.setSuperclass(ctcs); // Set the superclass of the proxy class
    }
    // Add the DC interface to the proxy class by default
    mCtc.addInterface(mPool.get(DC.class.getName())); 
    if (mInterfaces != null) {
        for (String cl : mInterfaces) {
            mCtc.addInterface(mPool.get(cl)); // Set the interfaces implemented by the proxy class
        }
    }
    if (mFields != null) {
        for (String code : mFields) {
            mCtc.addField(CtField.make(code, mCtc)); // Set the fields of the proxy class
        }
    }
    if (mMethods != null) {
        for (String code : mMethods) {
            if (code.charAt(0) == ':') {
                mCtc.addMethod(CtNewMethod.copy(getCtMethod(mCopyMethods.get(code.substring(1))),
                    code.substring(1, code.indexOf('(')), mCtc, null));
            } else {
                mCtc.addMethod(CtNewMethod.make(code, mCtc));
            }
        }
    }
    if (mDefaultConstructor) {
        mCtc.addConstructor(CtNewConstructor.defaultConstructor(mCtc)); // Generate the default constructor
    }
    if (mConstructors != null) {
        for (String code : mConstructors) {
            if (code.charAt(0) == ':') {
                mCtc.addConstructor(CtNewConstructor.copy(getCtConstructor(mCopyConstructors.get(code.substring(1))),
                    mCtc, null));
            } else {
                String[] sn = mCtc.getSimpleName().split("\\$+"); // inner class name include $.
                mCtc.addConstructor( CtNewConstructor.make(code.replaceFirst(SIMPLE_NAME_TAG, sn[sn.length - 1]), mCtc));
            }
        }
    }
    return mCtc.toClass(loader, pd);
}

In the second step, we get an ID value from the PROXY_CLASS_COUNTER field (AtomicLong type) as the suffix of the proxy class. This is mainly to avoid naming conflicts due to duplicate class names.

In the third step, we iterate through all the interfaces, get the methods defined in each interface, and process each method as follows:

  1. Add the method’s description to the worked set (Set type) to check for duplicates.
  2. Add the corresponding Method object to the methods list (List type).
  3. Get the parameter types and return type of the method, build the method body and return statement.
  4. Add the constructed method to the mMethods collection in ClassGenerator for caching.

Here’s the relevant code snippet:

long id = PROXY_CLASS_COUNTER.getAndIncrement();
String pkg = null;
ClassGenerator ccp = null, ccm = null;
ccp = ClassGenerator.newInstance(cl);
Set<String> worked = new HashSet<>();
List<Method> methods = new ArrayList<>();
for (int i = 0; i < ics.length; i++) {
    if (!Modifier.isPublic(ics[i].getModifiers())) {
        String npkg = ics[i].getPackage().getName();
        if (pkg == null) {
            pkg = npkg;
        } else {
            if (!pkg.equals(npkg)) {
                throw new IllegalArgumentException("non-public interfaces from different packages");
            }
        }
    }
    ccp.addInterface(ics[i]); // Add the interface to the ClassGenerator
    for (Method method : ics[i].getMethods()) {
        String desc = ReflectUtils.getDesc(method);
        // Skip duplicate methods and static methods
        if (worked.contains(desc) || Modifier.isStatic(method.getModifiers())) {
            continue;
        }
        if (ics[i].isInterface() && Modifier.isStatic(method.getModifiers())) {
            continue;
        }
        worked.add(desc); // Add the method description to the worked set to remove duplicates
        int ix = methods.size();
        Class<?> rt = method.getReturnType(); // Get the return type of the method
        Class<?>[] pts = method.getParameterTypes(); // Get the parameter types of the method
        // Create method body
        StringBuilder code = new StringBuilder("Object[] args = new Object[").append(pts.length).append("];");
        for (int j = 0; j < pts.length; j++) {
            code.append(" args[").append(j).append("] = ($w)$").append(j + 1).append(";");
        }
        code.append(" Object ret = handler.invoke(this, methods[").append(ix).append("], args);");
        if (!Void.TYPE.equals(rt)) { // Generate return statement
            code.append(" return ").append(asArgument(rt, "ret")).append(";");
        }
public Result invoke(Invocation invocation) throws RpcException {
    try {
        Object[] args;
        Class<?>[] parameterTypes;
        if (invocation.getArguments() != null) {
            args = invocation.getArguments();
            parameterTypes = new Class<?>[args.length];
            for (int i = 0; i < args.length; i++) {
                parameterTypes[i] = args[i].getClass();
            }
        } else {
            args = new Object[0];
            parameterTypes = new Class<?>[0];
        }
        Method method = proxy.getClass().getMethod(invocation.getMethodName(), parameterTypes);
        return new RpcResult(method.invoke(proxy, args));
    } catch (Throwable e) {
        throw new RpcException("Failed to invoke proxy method " + invocation.getMethodName() + ", cause: " + e.getMessage(), e);
    }
}
public String getName() {
    return ((com.xxx.Demo) $1).name;
}

public void setName(String value) {
    ((com.xxx.Demo) $1).name = value;
}

第二步,public 方法会构造一个 invokeMethod() 方法,实现对相应方法的调用。例如,有一个名为“sayHello”的 public 方法,则会生成如下代码:

public Object invokeMethod(Object o, String n, Class[] p, Object[] v) throws java.lang.reflect.InvocationTargetException{
    com.xxx.Demo w;
    try {
        w = ((com.xxx.Demo) $1);
    } catch (Throwable e) {
        throw new IllegalArgumentException(e);
    }
    try {
        if ("sayHello".equals(n) && p.length == 1) {
            return ($w)w.sayHello((java.lang.String)v[0]);
        }
    } catch (Throwable e) {
        throw new java.lang.reflect.InvocationTargetException(e);
    }
    throw new org.apache.dubbo.rpc.NoSuchMethodException("Not found method \"" + n + "\" in class com.xxx.Demo.");
}

第三步,makeWrapper() 方法会根据类名、字段名、方法名等信息,生成 Wrapper 实现类的代码字符串。然后调用 newInstance() 方法将这个字符串写入到一个 .class 文件中,并通过反射加载成 Class 对象,最后调用 newInstance() 方法生成 Wrapper 实例。

整个分析过程还是比较复杂的,希望你能理解 Wrapper 的作用以及 getWrapper() 方法的实现过程,这也是实现 Dubbo 代理工厂的一个关键步骤,后续的代码逻辑会在 Wrapper 中实现。


// Generated getPropertyValue() method

public Object getPropertyValue(Object o, String n){
    DemoServiceImpl w; 
    try{ 
        w = ((DemoServiceImpl)$1); 
    }catch(Throwable e){ 
        throw new IllegalArgumentException(e); 
    }
    if( $2.equals("name") ){
        return ($w)w.name; 
    }
}

// Generated setPropertyValue() method

public void setPropertyValue(Object o, String n, Object v){ 
    DemoServiceImpl w; 
    try{
        w = ((DemoServiceImpl)$1); 
    }catch(Throwable e){ 
        throw new IllegalArgumentException(e); 
    }
    if( $2.equals("name") ){ 
        w.name=(java.lang.String)$3; 
        return; 
    }
}

In the second step, the public methods are processed and added to the invokeMethod method. Taking the DemoServiceImpl in the Demo example (i.e., the demo module in dubbo-demo) as an example, the generated invokeMethod() method implementation is as follows:

public Object invokeMethod(Object o, String n, Class[] p, Object[] v) throws java.lang.reflect.InvocationTargetException {

    org.apache.dubbo.demo.provider.DemoServiceImpl w;

    try {

        w = ((org.apache.dubbo.demo.provider.DemoServiceImpl) $1);

    } catch (Throwable e) {

        throw new IllegalArgumentException(e);

    }

    try {

        // omitted getter/setter methods

        if ("sayHello".equals($2) && $3.length == 1) {

            return ($w) w.sayHello((java.lang.String) $4[0]);

        }

        if ("sayHelloAsync".equals($2) && $3.length == 1) {

            return ($w) w.sayHelloAsync((java.lang.String) $4[0]);

        }

    } catch (Throwable e) {

        throw new java.lang.reflect.InvocationTargetException(e);

    }

    throw new NoSuchMethodException("Not found method");

}

In the third step, after filling in the relevant information of the Wrapper implementation class, the makeWrapper() method will create the Wrapper implementation class through ClassGenerator. The mechanism is similar to the Proxy creation process discussed earlier.

Summary #

This lesson mainly introduces the “Proxy” related content in the dubbo-rpc-api module. First, we start with the ProxyFactory.getProxy() method and detailedly explain the underlying principles of creating dynamic proxy classes using both the JDK approach and the Javassist approach, as well as the implementation of the InvokerInvocationHandler used in them. Then we use the ProxyFactory.getInvoker() method to explain in detail the generation process and core principles of the Wrapper.

The following diagram shows the importance of Proxy and Wrapper in Dubbo:

Dubbo_28的图(待替换).png

Diagram of Proxy and Wrapper in remote invocation

The Proxy on the consumer side shields complex network interactions, cluster strategies, and Dubbo internals such as Invoker, and provides business interfaces to the upper layers. The Wrapper on the provider side converts personalized business interface implementations into Dubbo internal Invoker interface implementations. It is precisely because of the existence of the Proxy and Wrapper components that Dubbo can seamlessly convert internal interfaces and business interfaces.

If you have any thoughts on the “Proxy” related content, feel free to leave a comment and share with me. In the next lesson, we will have an additional meal and introduce the relevant content of the HTTP protocol supported in Dubbo.