Dynamic proxy trilogy (a): Dynamic mode and proxy implementation principle

One, background

Proxy mode, more or less in the actual project will be used as one of the commonly used design patterns to. Framework of the current mainstream projects, there are a lot of figure proxy mode.

Proxy mode, the proxy class with the same class that implements the interface body, the proxy class holds a reference entity class, and the client receiving the proxy class to the external entity reference injection, and proxy function entity classes.
NOTE: This external injection form are described in a specific technical terms: DI

Proxy mode generic class Pictured:

According to the proxy class generation mode, it is static or dynamic presence generated at run before the run, proxy mode can be divided into static and dynamic proxy agent.


Second, the static agent

Prior to really understand the dynamic proxy, it is necessary to briefly review the general process under static agent.

A specific example a direct look.
1, define the interface:

package com.corn.proxy.pstatic;

public interface Subject {

    String action();

}
复制代码

2, the definition of the main categories:

package com.corn.proxy.pstatic;

public class RealSubject implements Subject {

    @Override
    public String action() {
        System.out.println("action in RealSubject");
        return "action done";
    }
}
复制代码

3, define static proxy class:

package com.corn.proxy.pstatic;

public class ProxySubject implements Subject{

    private Subject realSubject;

    public ProxySubject(Subject realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public String action() {
        // 主体action前执行
        System.out.println("do sth before RealSubject action");
        String result = this.realSubject.action();
        // 主体action后执行
        System.out.println("do sth after RealSubject action");

        return result;
    }
}
复制代码

4, the client entity and injected visit:

package com.corn.proxy.pstatic;


public class Client {

    public static void main(String[] args) {
        Subject realSubject = new RealSubject();
        ProxySubject proxySubject = new ProxySubject(realSubject);

        proxySubject.action();
    }
}
复制代码

run, output:

do sth before RealSubject action
action in RealSubject
do sth after RealSubject action
复制代码

The code also corresponds to the process of the proxy mode class structure in FIG.


Third, the dynamic proxy

Essentially, the dynamic proxy agent is to follow the above-described general relationship model class diagram, as compared with the static agent, which 动态is mainly reflected in: 1, to generate the specific proxy class (ProxySubject) is generated dynamically at runtime instead of compile time static already exist;
2, agency relationship specific proxy class (ProxySubject) with the proxy class (ProxySubject hold a reference RealSubject's), is to find ways to inject into the dynamic of;
3, the specific proxy class (ProxySubject) function is a proxy class the agent is within the dynamically generated proxy class, find a way to dynamically call the corresponding method of the proxy class.

Whether it is dynamically generated specific proxy classes, or with the relationship of the proxy class, as well as the agency calls the method of the proxy class, the middle, have used two key intermediaries, namely Proxy and InvocationHandler.

Proxy class, which provides a static method dynamically generated proxy class and holds a reference to achieve InvocationHandler interface. At the same time, all the generated proxy class is also a subclass of the Proxy class.

InvocationHandler interface contains only one abstract method name: invoke, such InvocationHandler interface implementation class to the specific implementation, the entity is held by the method of the proxy class entity (RealSubject), by reflection, to call the corresponding implementation.

: Thus, the process performed on the dynamic proxies generally to
invoke instances of objects when the client proxy class to generate dynamic Proxy static method, the dynamic proxy class calls the interface method corresponding to internal calls inside thereof held interface InvocationHandler method, and the corresponding method is actually a call to the agent entity.

The overall relationship between the class if the class is represented by FIG, with slightly different generic class of the proxy mode in FIG.

Corresponding code implementation code as follows:
1, define the interface Subject:

package com.corn.proxy.pdynamic;

public interface Subject {

    String action();

}
复制代码

2, the definition of the main categories:

package com.corn.proxy.pdynamic;

public class RealSubject implements Subject{

    @Override
    public String action() {
        System.out.println("action in RealSubject");
        return "action done";
    }
}
复制代码

3, InvocationHandler defined class that implements the interface:

package com.corn.proxy.pdynamic;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class ProxyInvocationHandler implements InvocationHandler {

    protected Subject subject;

    public ProxyInvocationHandler(Subject subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("do something before in ProxyInvocationHandler");
        return method.invoke(subject, args);
    }

}
复制代码

4, the client entity and injected visit:

package com.corn.proxy.pdynamic;

import java.lang.reflect.Proxy;

public class Client {

    public static void main(String[] args) {
        Subject realSubject = new RealSubject();
        ProxyInvocationHandler proxyInvocationHandler = new ProxyInvocationHandler(realSubject);

        Subject proxyRealSubject = (Subject) Proxy.newProxyInstance(realSubject.getClass().getClassLoader(),
                realSubject.getClass().getInterfaces(), proxyInvocationHandler);

        proxyRealSubject.action();
    }
}
复制代码

run, output:

do something before in ProxyInvocationHandler
action in RealSubject
复制代码

Fourth, implement the principle of

It can already be seen from the above dynamic proxy class diagram and the execution flow analysis, dynamic proxy key, and that by InvocationHandler Proxy media, dynamically generated at runtime dynamic proxy class, the generated dynamic proxy class interfaces Subject still achieved, and invoke a callback class method InvocationHandler implemented method call, invoke method implementation class InvocationHandler by reflection, a corresponding method is the callback proxy entity.

Look around a bit.

In the dynamic proxy generation process, Java and Android engineering projects in a little bit different, starting with a look at the specific angle source Java implementation of the project.

Android studio in New Java project not directly, but by way of establishing Java Library, established in Android Java library project. By default, Android Studio AS associated JDK is built, can not see the source of this, you need to download the JDK into their own path, this path src directory contains the source code. Specific modifications as follows:
1. Check local JDK directory

/usr/libexec/java_home -V
复制代码

Export

Matching Java Virtual Machines (1):
    1.8.0_162, x86_64:	"Java SE 8"	/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home

/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home
复制代码

2, Android Studio replace JDK settings File >> Other Setting >> Default Project Structure, the default settings in the JDK Location

/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home
复制代码

change into:

/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home
复制代码

After setup is complete, you can directly see the JDK source.

ProxyInvocationHandler interface has only one method:

public interface InvocationHandler {

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
复制代码

The following highlights look Proxy class newProxyInstance process.

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
{
    Objects.requireNonNull(h);

    final Class<?>[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }

    /*
     * Look up or generate the designated proxy class.
     */
    Class<?> cl = getProxyClass0(loader, intfs);

    /*
     * Invoke its constructor with the designated invocation handler.
     */
    try {
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }

        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
        return cons.newInstance(new Object[]{h});
    } catch (IllegalAccessException|InstantiationException e) {
        throw new InternalError(e.toString(), e);
    } catch (InvocationTargetException e) {
        Throwable t = e.getCause();
        if (t instanceof RuntimeException) {
            throw (RuntimeException) t;
        } else {
            throw new InternalError(t.toString(), t);
        }
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString(), e);
    }
}
复制代码

Wherein getProxyClass0 embodied corresponding call is:

/**
 * Generate a proxy class.  Must call the checkProxyAccess method
 * to perform permission checks before calling this.
 */
private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }

    // If the proxy class defined by the given loader implementing
    // the given interfaces exists, this will simply return the cached copy;
    // otherwise, it will create the proxy class via the ProxyClassFactory
    return proxyClassCache.get(loader, interfaces);
}
复制代码

So, we follow the corresponding ProxyClassFactory:

/**
 * A factory function that generates, defines and returns the proxy class given
 * the ClassLoader and array of interfaces.
 */
private static final class ProxyClassFactory
    implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
    // prefix for all proxy class names
    private static final String proxyClassNamePrefix = "$Proxy";

    // next number to use for generation of unique proxy class names
    private static final AtomicLong nextUniqueNumber = new AtomicLong();

    @Override
    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

        Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
        for (Class<?> intf : interfaces) {
            /*
             * Verify that the class loader resolves the name of this
             * interface to the same Class object.
             */
            Class<?> interfaceClass = null;
            try {
                interfaceClass = Class.forName(intf.getName(), false, loader);
            } catch (ClassNotFoundException e) {
            }
            if (interfaceClass != intf) {
                throw new IllegalArgumentException(
                    intf + " is not visible from class loader");
            }
            /*
             * Verify that the Class object actually represents an
             * interface.
             */
            if (!interfaceClass.isInterface()) {
                throw new IllegalArgumentException(
                    interfaceClass.getName() + " is not an interface");
            }
            /*
             * Verify that this interface is not a duplicate.
             */
            if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                throw new IllegalArgumentException(
                    "repeated interface: " + interfaceClass.getName());
            }
        }

        String proxyPkg = null;     // package to define proxy class in
        int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

        /*
         * Record the package of a non-public proxy interface so that the
         * proxy class will be defined in the same package.  Verify that
         * all non-public proxy interfaces are in the same package.
         */
        for (Class<?> intf : interfaces) {
            int flags = intf.getModifiers();
            if (!Modifier.isPublic(flags)) {
                accessFlags = Modifier.FINAL;
                String name = intf.getName();
                int n = name.lastIndexOf('.');
                String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                if (proxyPkg == null) {
                    proxyPkg = pkg;
                } else if (!pkg.equals(proxyPkg)) {
                    throw new IllegalArgumentException(
                        "non-public interfaces from different packages");
                }
            }
        }

        if (proxyPkg == null) {
            // if no non-public proxy interfaces, use com.sun.proxy package
            proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
        }

        /*
         * Choose a name for the proxy class to generate.
         */
        long num = nextUniqueNumber.getAndIncrement();
        String proxyName = proxyPkg + proxyClassNamePrefix + num;

        /*
         * Generate the specified proxy class.
         */
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
            proxyName, interfaces, accessFlags);
        try {
            return defineClass0(loader, proxyName,
                                proxyClassFile, 0, proxyClassFile.length);
        } catch (ClassFormatError e) {
            /*
             * A ClassFormatError here means that (barring bugs in the
             * proxy class generation code) there was some other
             * invalid aspect of the arguments supplied to the proxy
             * class creation (such as virtual machine limitations
             * exceeded).
             */
            throw new IllegalArgumentException(e.toString());
        }
    }
}
复制代码

Among them, the key process is:

/*
 * Generate the specified proxy class.
 */
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
    proxyName, interfaces, accessFlags);
try {
    return defineClass0(loader, proxyName,
                        proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
   ...
}
复制代码

Generating a corresponding binary code by a dynamic proxy class generateProxyClass ProxyGenerator class method, and is loaded by the ClassLoader, by reflection, to generate a corresponding class instance.

ProxyGenerator class is directly integrated in rt.jar, sun.misc package name, extension type. Android projects because of JDK version, the default is not integrated, how it is here in Android to achieve it?

Similarly, the use of dynamic proxies directly in the Android project, invoke the process of tracking the source.

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
{
    Objects.requireNonNull(h);

    final Class<?>[] intfs = interfaces.clone();
    // Android-removed: SecurityManager calls
    /*
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }
    */

    /*
     * Look up or generate the designated proxy class.
     */
    Class<?> cl = getProxyClass0(loader, intfs);

    /*
     * Invoke its constructor with the designated invocation handler.
     */
    try {
        // Android-removed: SecurityManager / permission checks.
        /*
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }
        */

        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        if (!Modifier.isPublic(cl.getModifiers())) {
            // BEGIN Android-changed: Excluded AccessController.doPrivileged call.
            /*
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
            */

            cons.setAccessible(true);
            // END Android-removed: Excluded AccessController.doPrivileged call.
        }
        return cons.newInstance(new Object[]{h});
    } catch (IllegalAccessException|InstantiationException e) {
        throw new InternalError(e.toString(), e);
    } catch (InvocationTargetException e) {
        Throwable t = e.getCause();
        if (t instanceof RuntimeException) {
            throw (RuntimeException) t;
        } else {
            throw new InternalError(t.toString(), t);
        }
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString(), e);
    }
}
复制代码

Clearly, Android JDK in newProxyInstance method was modified partially achieved. But the main execution path remains the same.

/**
 * Generate a proxy class.  Must call the checkProxyAccess method
 * to perform permission checks before calling this.
 */
private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }

    // If the proxy class defined by the given loader implementing
    // the given interfaces exists, this will simply return the cached copy;
    // otherwise, it will create the proxy class via the ProxyClassFactory
    return proxyClassCache.get(loader, interfaces);
}
复制代码

The same, ProxyClassFactory class.

/**
 * A factory function that generates, defines and returns the proxy class given
 * the ClassLoader and array of interfaces.
 */
private static final class ProxyClassFactory
    implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
    // prefix for all proxy class names
    private static final String proxyClassNamePrefix = "$Proxy";

    // next number to use for generation of unique proxy class names
    private static final AtomicLong nextUniqueNumber = new AtomicLong();

    @Override
    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

        Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
        for (Class<?> intf : interfaces) {
            /*
             * Verify that the class loader resolves the name of this
             * interface to the same Class object.
             */
            Class<?> interfaceClass = null;
            try {
                interfaceClass = Class.forName(intf.getName(), false, loader);
            } catch (ClassNotFoundException e) {
            }
            if (interfaceClass != intf) {
                throw new IllegalArgumentException(
                    intf + " is not visible from class loader");
            }
            /*
             * Verify that the Class object actually represents an
             * interface.
             */
            if (!interfaceClass.isInterface()) {
                throw new IllegalArgumentException(
                    interfaceClass.getName() + " is not an interface");
            }
            /*
             * Verify that this interface is not a duplicate.
             */
            if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                throw new IllegalArgumentException(
                    "repeated interface: " + interfaceClass.getName());
            }
        }

        String proxyPkg = null;     // package to define proxy class in
        int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

        /*
         * Record the package of a non-public proxy interface so that the
         * proxy class will be defined in the same package.  Verify that
         * all non-public proxy interfaces are in the same package.
         */
        for (Class<?> intf : interfaces) {
            int flags = intf.getModifiers();
            if (!Modifier.isPublic(flags)) {
                accessFlags = Modifier.FINAL;
                String name = intf.getName();
                int n = name.lastIndexOf('.');
                String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                if (proxyPkg == null) {
                    proxyPkg = pkg;
                } else if (!pkg.equals(proxyPkg)) {
                    throw new IllegalArgumentException(
                        "non-public interfaces from different packages");
                }
            }
        }

        if (proxyPkg == null) {
            // if no non-public proxy interfaces, use the default package.
            proxyPkg = "";
        }

        {
            // Android-changed: Generate the proxy directly instead of calling
            // through to ProxyGenerator.
            List<Method> methods = getMethods(interfaces);
            Collections.sort(methods, ORDER_BY_SIGNATURE_AND_SUBTYPE);
            validateReturnTypes(methods);
            List<Class<?>[]> exceptions = deduplicateAndGetExceptions(methods);

            Method[] methodsArray = methods.toArray(new Method[methods.size()]);
            Class<?>[][] exceptionsArray = exceptions.toArray(new Class<?>[exceptions.size()][]);

            /*
             * Choose a name for the proxy class to generate.
             */
            long num = nextUniqueNumber.getAndIncrement();
            String proxyName = proxyPkg + proxyClassNamePrefix + num;

            return generateProxy(proxyName, interfaces, loader, methodsArray,
                                 exceptionsArray);
        }
    }
}
复制代码

The key, we found

// Android-changed: Generate the proxy directly instead of calling
// through to ProxyGenerator.
...
return generateProxy(proxyName, interfaces, loader, methodsArray,
                     exceptionsArray);
复制代码

Tracking look into the specific implementation process

@FastNative
private static native Class<?> generateProxy(String name, Class<?>[] interfaces,
                                             ClassLoader loader, Method[] methods,
                                             Class<?>[][] exceptions);
// END Android-changed: How proxies are generated.
复制代码

Original, Android JDK to the generateProxy were processed using a local direct method.

So, when generating a dynamic proxy, if you encounter thread-safety problem?
ProxyClassFactory code, the following process:

private static final class ProxyClassFactory
        implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
    // prefix for all proxy class names
    private static final String proxyClassNamePrefix = "$Proxy";

    // next number to use for generation of unique proxy class names
    private static final AtomicLong nextUniqueNumber = new AtomicLong();
    
    ...
    
    /*
     * Choose a name for the proxy class to generate.
     */
    long num = nextUniqueNumber.getAndIncrement();
    String proxyName = proxyPkg + proxyClassNamePrefix + num;

    /*
     * Generate the specified proxy class.
     */
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
        proxyName, interfaces, accessFlags);
    try {
        return defineClass0(loader, proxyName,
                            proxyClassFile, 0, proxyClassFile.length);
    } catch (ClassFormatError e) {
        ...
    }
}
复制代码

Obviously, thread-safe file naming, prevent the thread-safety issues under the same interface for use multithreading, dynamic proxies that may arise.

So, generate dynamic proxy in the end look like? We can call the system of direct ProxyGenerator.generateProxyClass try.

package com.corn.proxy.pdynamic;


import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Proxy;

import sun.misc.ProxyGenerator;

public class Client {

    public static void main(String[] args) {
        Subject realSubject = new RealSubject();
        ProxyInvocationHandler proxyInvocationHandler = new ProxyInvocationHandler(realSubject);

        Subject proxyRealSubject = (Subject) Proxy.newProxyInstance(realSubject.getClass().getClassLoader(),
                realSubject.getClass().getInterfaces(), proxyInvocationHandler);

        proxyRealSubject.action();

        String proxyName = "ProxySubject";
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, RealSubject.class.getInterfaces());

        try (FileOutputStream fos = new FileOutputStream("/Users/corn/T/ProxySubject.class")){
            fos.write(proxyClassFile);
            fos.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
复制代码

RUN, generates the corresponding directory files ProxySubject.class, bytecodes corresponding to the content can be seen by a tool.

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import com.corn.proxy.pdynamic.Subject;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class ProxySubject extends Proxy implements Subject {
    private static Method m1;
    private static Method m2;
    private static Method m0;
    private static Method m3;

    public ProxySubject(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);
        }
    }

    public final String action() throws  {
        try {
            return (String)super.h.invoke(this, m3, (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");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m3 = Class.forName("com.corn.proxy.pdynamic.Subject").getMethod("action");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}
复制代码

Sure enough, the dynamic proxy generated achieved Subject interface, and the corresponding action method calls invoke method of InvocationHandler instance of a class.


V. Conclusion

Acting as a dynamic proxy mode, avoiding direct defined prior to run static proxy class for the case requires a lot of proxy class, it is very useful. At the same time, we should also see that in the whole process, in fact, the dynamic and static proxy agent overall is still the same. Acting both in the process of creating dynamic proxy class, or call the proxy methods, are used in the process of reflection, some loss in performance to some extent, the actual use of the amount needed to weigh.

Dynamic proxy mode, as a design pattern is relatively difficult to understand one of the major process in that after the layers of the package, and finally only through Proxy classes directly exposed to the external InvocationHandler use interface directly on the screen of the consumer specific details. But our understanding is that the model itself, to understand and master the process where appropriate, or somewhat beneficial.

Reproduced in: https: //juejin.im/post/5d047101f265da1bb2772e09

Guess you like

Origin blog.csdn.net/weixin_34255793/article/details/93176008