Java Dynamic Proxy — application scenarios and basic principles in framework

Time:2021-1-10

preface

Five articles have been used to explain the principle of Java dynamic proxy. This article will add the last jigsaw puzzle to this series to show the usage and application scenarios of Java dynamic proxy

It is mainly divided into the following four parts

1. Why use Java dynamic proxy

2. How to use Java Dynamic Proxy

3. The application of Java Dynamic Proxy in the framework

4. The basic principle of Java dynamic proxy

1. Why use dynamic proxy

There is a very common pattern in design patterns: proxy pattern. Academically speaking, it is to provide a proxy object for a certain behavior of some objects, and the proxy object completely controls the actual execution of the behavior.

Generally speaking, I want to order takeout, but my mobile phone is out of power, so I asked my classmates to use his mobile phone to order takeout for me. In this process, it’s actually meStudents (proxy objects))HelpI (represented object)AgentTo order (the act of being represented)In this process, students can completely control the takeout shop, the app they use, and even eat the takeout directly(complete control of behavior)

Therefore, the four elements of agency are summarized

Proxy object

The act of being represented

Proxy object

Complete control of behavior

From the perspective of actual coding, we assume that we meet such a requirement that we need to record the execution time of some methods, so the simplest way is, of course, to record a timestamp at the beginning of the method and a timestamp before return. However, if the process of the method is complex, for example:

public class Executor {
    public void execute(int x, int y) {
        log.info("start:{}", System.nanoTime());
        if (x == 3) {
            log.info("end:{}", System.nanoTime());
            return;
        }
        for (int i = 0; i < 100; i++) {
            if (y == 5) {
                log.info("end:{}", System.nanoTime());
                return;
            }
        }
        log.info("end:{}", System.nanoTime());
        return;
    }
}

We need to add a line of code to record the timestamp before each return, which is very troublesome. So we think that the time can be recorded by the caller of the method

public class Invoker {
    private Executor executor = new Executor();

    public void invoke() {
        log.info("start:{}", System.nanoTime());
        executor.execute(1, 2);
        log.info("end:{}", System.nanoTime());
    }
}

We have another problem. If the method is called in many places, or there are many methods that need to be recorded, we will still face the problem of repeated manual writing of log code.

Therefore, we can consider creating a proxy object to help us unify the time stamps. For example:

public class Proxy {
    Executor executor = new Executor();

    public void execute(int x, int y) {
        log.info("start:{}", System.nanoTime());
        executor.execute(x, y);
        log.info("start:{}", System.nanoTime());
    }
}

In the invoker, the method in the executor is called instead of the method in the proxy. Of course, the name and signature of the method are exactly the same. When you need to call the execute method in other places, you only need to call the execute method in the proxy, and the timestamp will be automatically recorded, which is not perceptible to the user. Here is an example:

public class Invoker {
    private Proxy executor;

    public void invoke() {
        executor.execute(1, 2);
    }
}

The proxy shown above is a typical static proxy. The “static” is embodied in that the proxy method is directly encoded in the class.

Then we come across the next problem. If the executor adds a new method to record the time, we have to modify the proxy code. And if other classes have the same requirements, it needs to create a new proxy class to better implement the function, which is also very troublesome.

Then we need to upgrade the static agent to a dynamic agent, and “dynamic” is just to optimize the problems encountered by the two static agents mentioned above.

2. How to use Java Dynamic Proxy

To create a Java Dynamic Proxy, you need to use the following classes

java.lang.reflect.Proxy

Call its newproxyinstance method. For example, we need to create a proxy for map

Map mapProxy = (Map) Proxy.newProxyInstance(
        HashMap.class.getClassLoader(),
        new Class[]{Map.class},
        new InvocationHandler(){...}
);

Let’s go on to analyze this method. Check its signature first:

public static Object newProxyInstance(ClassLoader loader,
                                      Class>[] interfaces,
                                      InvocationHandler h)

Loader of classloader type: the loader of the proxied class, which can be considered to correspond to theProxy object

Interfaces of class array: the interface of the agent, which corresponds to the interface of the four elementsThe act of being representedYou can notice that you need to pass in an interface instead of a specific class, so it represents a behavior.

H of invocationhandler interfaceAgent’s specific behavior is corresponding to one of the four elementsComplete control of behaviorOf course, it is also the core of Java dynamic proxy.

The last returned object object corresponds to one of the four elementsProxy object

Next, let’s use Java dynamic proxy to complete the requirement of recording method execution time stamp

First, defineThe act of being representedInterface:

public interface ExecutorInterface {
    void execute(int x, int y);
}

Next, defineProxy objectThat is, the class that implements the interface:

public class Executor implements ExecutorInterface {
    public void execute(int x, int y) {
        if (x == 3) {
            return;
        }
        for (int i = 0; i < 100; i++) {
            if (y == 5) {
                return;
            }
        }
        return;
    }
}

Then there is the core of agency, namelyBehavior control, you need a class that implements the invocationhandler interface:

public class TimeLogHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return null;
    }
}

The method in this interface is not complicated. Let’s analyze its signature first

Proxy of type object: final generatedProxy object

Method of type method: the method being represented. This is actually a combination of two elements, namelyProxy objectHow is it implementedThe act of being representedYes. Although we say that we want to control the behavior completely, most of the time, we only add some additional functions to the behavior, so we still need to use the original execution process of the proxy object.

Args of object array: parameters for method execution

Because our purpose is to record the execution time stamp of the method, and the original method itself still needs to be executed, in the constructor of the timeloghandler, an original object is passed in, and the method can be used when calling the invoke method.

The behavior of defining an agent is as follows:

public class TimeLogHandler implements InvocationHandler {
    private Object target;

    public TimeLogHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log.info("start:{}", System.nanoTime());
        Object result = method.invoke(target, args);
        log.info("end:{}", System.nanoTime());
        return result;
    }
}

Next, let’s see how invoker uses proxy. Here, for the convenience of demonstration, we instantiate the proxy object in the constructor. In actual use, we can instantiate it by means of dependency injection or singleton

public class Invoker {
    private ExecutorInterface executor;

    public Invoker() {
        executor = (ExecutorInterface) Proxy.newProxyInstance(
                Executor.class.getClassLoader(),
                new Class[]{ExecutorInterface.class},
                new TimeLogHandler(new Executor())
        );
    }
  
    public void invoke() {        
        executor.execute(1, 2);
    }
}

At this time, if any new method is added to the executor, the invoker and the timeloghandler can support the timestamp recording of the new method without any changes. If you are interested, you can try it yourself.

In addition, if there are other classes that also need to use timestamp records, you just need to use the Proxy.newProxyInstance Method can be created without any other changes.

3. The application of Java Dynamic Proxy in the framework

Next, let’s take a look at the practical application of Java Dynamic Proxy in some common frameworks

Spring AOP

Spring AOP is a very common function in our spring project.

For example, when we get some data, we need to query whether there is a cache in redis. If there is no cache, we can read the database. We can define the following aspects and behaviors, and then add corresponding annotations to the methods that need this function, instead of requiring each method to write its own logic. Here is an example:

@Aspect
@Component
public class TestAspect {
    /**
     *It means that all have cn.tera.aop Method of. Redispoint annotation
     *Will read redis first
     */
    @Pointcut("@annotation(cn.tera.aop.RedisPoint)")
    public void pointCut() {
    }

    /**
     *The process of getting the actual number
     */
    @Around("pointCut()")
    public Object advise(ProceedingJoinPoint joinPoint) {
        try {
            /**
             *Check redis first
             */
            Object data = RedisUtility.get(some_key);
            if (data == null) {
                /**
                 * joinPoint.proceed () means to execute the original method
                 *If there is no cache in redis, execute the original method to get the data
                 *Then you can get the cache directly next time by inserting it into redis
                 */
                data = joinPoint.proceed();
                RedisUtility.put(some_key, data);
            }
            return data;
        } catch (Throwable r) {
            return null;
        }
    }
}

The principle behind it is to use Java dynamic proxy. Of course, it is required that the class of the annotated method must implement the interface (in retrospect) Proxy.newProxyInstance Otherwise, you need to use another gclib library, but this is another story, which will not be expanded here.

In most cases, spring AOP adds something to the original execution logic.

RPC framework

In some RPC frameworks, clients only need to pay attention to interface calls, while specific remote requests are implemented by the framework itself. For example, we simulate a simple RPC request. The interface is as follows:

public interface OrderInterface {
    /**
     *Generate a new order
     */
    void addOrder();
}

RPC framework can generate proxy objects of interfaces, such as:

public class SimpleRpcFrame {
    /**
     *Create a remote request broker object
     */
    public static  T getRemoteProxy(Class service) {
        return (T) Proxy.newProxyInstance(service.getClassLoader(),
                new Class>[]{service},
                new RpcHandler(service));
    }

    /**
     *A class that handles specific remote calls
     */
    static class RpcHandler implements InvocationHandler {
        private Class service;

        public RpcHandler(Class service) {
            this.service = service;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            /**
             *According to the interface name and method name, a specific network request is initiated to obtain data
             */
            Object result = RemoteCallUtility.request(service.getName(), method.getName(), args);
            return result;
        }
    }
}

When the client calls, it does not need the specific implementation of the interface. It only needs to obtain the proxy of the interface through the RPC framework. At this time, whether to use the HTTP protocol or directly request data through socket is the responsibility of the framework, for example:

public class RpcInvoker {
    public void invoke() {
        OrderInterface order = SimpleRpcFrame.getRemoteProxy(OrderInterface.class);
        order.addOrder();
    }
}

The proxy in RPC does not need the original execution logic at all, but completely controls the execution process of the behavior

This is the example of the framework using java dynamic proxy.

4. The basic principle of Java dynamic proxy

Previously, I have introduced the implementation principle of Java dynamic proxy through five articles, but because it is a bit obscure, I will abandon the analysis of detailed code here, so that we can understand its basic principle from an intuitive point of view as much as possible.

Suppose we still implement the function of adding time stamp at the beginning. At this time, we need the following code to obtain its proxy:

ExecutorInterface executor = (ExecutorInterface) Proxy.newProxyInstance(
                Executor.class.getClassLoader(),
                new Class[]{ExecutorInterface.class},
                new TimeLogHandler()
        );
        executor.execute(1, 2);

At this point, we print the actual class name of the executor, the implemented interface, and the name of the parent class. The results are as follows:

Class name: com.sun.proxy .$Proxy11
Parent class: java.lang.reflect .Proxy
Implementation interface: executorinterface

Therefore, the generated proxy class has the following three characteristics:

1. Inherited the proxy class
2. Implement our interface
3. Name with $proxy + random number

Next, we still need to look at the source code of the newproxyinstance method. We only need to care about the following lines of core code, as follows:

public static Object newProxyInstance(ClassLoader loader,
                                      Class>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
    ...
    /**
     *Create a class based on the interface we pass in
     */
    Class> cl = getProxyClass0(loader, intfs);
    ...
    /**
     *Find the constructor of the class, which takes a parameter of type invocationhandler
     */
    final Constructor> cons = cl.getConstructor(constructorParams);
    ...
    /**
     *The instance of proxy class is generated by construction method
     */
    return cons.newInstance(new Object[]{h});
}

So let’s summarize the process of creating dynamic proxy objects

1. Create a class dynamically according to our incoming interface

2. Get the constructor of the class

3. Pass the invocationhandler as a parameter into the constructor, instantiate the instance of the proxy object, and return it

Of course, the core method here is naturally the creation of classes. In short, it is to construct a byte array byte by byte at runtime, and this byte array is exactly a. Class bytecode, and then convert it into the class class at runtime through a native method.

More generally speaking:The classes we usually use are precompiled. Class files, while the dynamic proxy creates a. Class file by assembling a byte array at runtimeThis should be easier to understand.

If you are interested in how to build this byte array, you are welcome to take a look at the five articles I wrote before, which not only introduces the source code of dynamic proxy, but also has an in-depth understanding of the more detailed structure of class bytecode
1.https://www.cnblogs.com/tera/p/13267630.html
2.https://www.cnblogs.com/tera/p/13280547.html
3.https://www.cnblogs.com/tera/p/13336627.html
4.https://www.cnblogs.com/tera/p/13419025.html
5.https://www.cnblogs.com/tera/p/13442018.html

Finally, let’s take a look at what the generated proxy class looks like, which is in line with the three characteristics of the proxy object we summarized earlier (we also showed how to see this content in the previous article). In particular, because all classes inherit from object, besides the methods defined in our own interface, there are also methods of object class

public final class $Proxy11 extends Proxy implements ExecutorInterface {
    private static Method m1;
    private static Method m2;
    private static Method m0;
    private static Method m3;

    public $Proxy11(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 void execute(int var1, int var2) throws  {
        try {
            super.h.invoke(this, m3, new Object[]{var1, var2});
        } catch (RuntimeException | Error var4) {
            throw var4;
        } catch (Throwable var5) {
            throw new UndeclaredThrowableException(var5);
        }
    }

    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("cn.tera.aopproxy.proxyuse.ExecutorInterface").getMethod("execute", Integer.TYPE, Integer.TYPE);
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

So far, the basic introduction of Java dynamic proxy is over

Finally, we summarize the idea and principle of Java dynamic proxy

1. Four elements of agent: agent object, agent’s behavior, agent’s object and complete control of behavior

2. Application of proxy: it is convenient to add some common logic (spring AOP) for some behaviors or to completely control the execution of behaviors by proxy (RPC)

3. The principle of Java Dynamic Proxy: build a class bytecode array at runtime, convert it into a class object at runtime, and then construct its instance