Detailed explanation of Java Dynamic Proxy

Dynamic proxy is widely used in Java, such as Spring AOP, Hibernate data query, back-end mock of test framework, RPC remote call, Java annotation object acquisition, log, user authentication, global exception handling, performance monitoring, and even transaction processing.

This paper mainly introduces two common dynamic proxy methods in Java: JDK native dynamic proxy and CGLIB dynamic proxy.

Since Java dynamic proxy is closely related to java reflection mechanism, please make sure that readers have understood java reflection mechanism. Please refer to the previous article Detailed explanation of Java reflection mechanism.

proxy pattern

The Java Dynamic Proxy introduced in this article is related to the proxy pattern in the design pattern. What is the proxy pattern?

Proxy mode: provide a proxy for an object, and the proxy object controls the access to the real object. Agent pattern is a structural design pattern.

There are three types of agent mode roles:

Subject (abstract topic role): defines the public external method of proxy class and real topic, and it is also the method of proxy class representing real topic;

RealSubject (real subject role): a class that truly implements business logic;

Proxy (proxy topic role): used to proxy and encapsulate real topics;

The structure of proxy mode is relatively simple, and its core is proxy class. In order to enable the client to treat real objects and proxy objects consistently, an abstraction layer is introduced into the proxy mode

The agent mode is classified according to responsibilities (usage scenarios), and can be divided into the following categories at least: 1. Remote agent. 2. Virtual agent. 3. Copy on write agent. 4. Protect or Access agent. 5. Cache agent. 6. Firewall agent. 7. Synchronization agent. 8. Smart Reference proxy and so on.

If classified according to the creation time of bytecode, it can be divided into static agent and dynamic agent:

  • The so-called static means that the bytecode file of the agent class already exists before the program runs, and the relationship between the agent class and the real subject role is determined before the program runs.
  • The source code of dynamic agent is dynamically generated by JVM according to reflection and other mechanisms during program running, so there is no bytecode file of agent class before running.

Static proxy

We first learn about static agents through examples, then understand the shortcomings of static agents, and then learn the protagonist of this article: dynamic agents

Write an interface UserService and an implementation class UserServiceImpl of the interface

public interface UserService {
    public void select();   
    public void update();
}
 
public class UserServiceImpl implements UserService {  
    public void select() {  
        System.out.println("query selectById");
    }
    public void update() {
        System.out.println("to update update");
    }
}

We will enhance UserServiceImpl through static proxy and record some logs before calling select and update. Write a proxy class UserServiceProxy. The proxy class needs to implement UserService

public class UserServiceProxy implements UserService {
    private UserService target; // Proxied object
 
    public UserServiceProxy(UserService target) {
        this.target = target;
    }
    public void select() {
        before();
        target.select();    // This is where the method of the real subject role is actually called
        after();
    }
    public void update() {
        before();
        target.update();    // This is where the method of the real subject role is actually called
        after();
    }
 
    private void before() {     // Execute before executing method
        System.out.println(String.format("log start time [%s] ", new Date()));
    }
    private void after() {      // Execute after method execution
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

Client test

public class Client1 {
    public static void main(String[] args) {
        UserService userServiceImpl = new UserServiceImpl();
        UserService proxy = new UserServiceProxy(userServiceImpl);
 
        proxy.select();
        proxy.update();
    }
}

output

log start time [Thu Dec 20 14:13:25 CST 2018] 
query selectById
log end time [Thu Dec 20 14:13:25 CST 2018] 
log start time [Thu Dec 20 14:13:25 CST 2018] 
to update update
log end time [Thu Dec 20 14:13:25 CST 2018] 

Through the static agent, we achieve the purpose of function enhancement without invading the original code, which is an advantage of the static agent.

Disadvantages of static agents

Although the implementation of static agent is simple and does not invade the original code, the disadvantages of static agent will also be exposed when the scene is a little more complex.

  1. When you need to proxy multiple classes, because the proxy object needs to implement an interface consistent with the target object, there are two ways:
  • Only one proxy class is maintained, and multiple interfaces are implemented by this proxy class, but this leads to too large proxy classes.
  • Create multiple proxy classes, and each target object corresponds to a proxy class, but this will produce too many proxy classes.
  1. When the interface needs to add, delete and modify methods, the target object and proxy class must be modified at the same time, which is difficult to maintain.

How to improve?

Of course, let the proxy class be generated dynamically, that is, dynamic proxy.

Why can classes be generated dynamically? The Java virtual machine class loading process is mainly divided into five stages: loading, verification, preparation, parsing and initialization. In the loading phase, the following three things need to be completed:

  1. Get the binary byte stream defining a class through the fully qualified name of the class
  2. The static storage structure represented by this byte stream is transformed into the runtime data structure of the method area
  3. Generate a Java. Net file representing this class in memory Lang. class object is used as various data access entries of this class in the method area

Since the virtual machine specification does not specify these three requirements, the actual implementation is very flexible. As for point 1, there are many ways to obtain the binary byte stream (class bytecode) of the class:

  • Get from ZIP package, which is the basis of JAR, EAR, WAR and other formats
  • From the network, the typical application is Applet
  • The dynamic agent technology is most used in this scenario, which is generated by runtime calculation lang.reflect. In the Proxy class, proxygenerator is used Generateproxyclass to generate binary byte stream of Proxy class in the form of * $Proxy for a specific interface
  • Generated by other files. The typical application is JSP, that is, the corresponding Class class is generated by JSP files
  • Get from the database, etc

Therefore, the dynamic proxy is to find a way to calculate the bytecode of the proxy class according to the interface or target object, and then load it into the JVM for use. But how? How to generate? The situation may be much more complicated than expected. We need to rely on the existing scheme.

Common bytecode operation class libraries

Here are some introductions: https://java-source.net/open-source/bytecode-libraries

  • Apache BCEL (Byte Code Engineering Library): it is a widely used framework for Java classworking. It can go deep into the details of class operation in JVM assembly language.
  • ObjectWeb ASM: is a Java bytecode operation framework. It can be used to dynamically generate stub root classes or other proxy classes directly in binary form, or dynamically modify classes when loading.
  • CGLIB(Code Generation Library): it is a powerful, high-performance and high-quality code generation library for extending Java classes and implementing interfaces at run time.
  • Javassist: Java's load time reflection system. It is a class library for editing bytecode in Java; It enables Java programs to define new classes at run time and modify class files before the JVM loads.
  • ...

Thinking direction of realizing dynamic agent

In order to keep the generated proxy class consistent with the target object (real topic role), the following two most common methods will be introduced from now on:

  1. By implementing the interface - > JDK dynamic proxy
  2. By inheriting classes - > cglib dynamic proxy

Note: using ASM requires a lot of users, and using Javassist will be troublesome

JDK dynamic agent

JDK dynamic proxy mainly involves two classes: Java lang.reflect. Proxy and Java lang.reflect. Invocationhandler, we still learn through cases.

Write a calling logic processor LogHandler class, provide log enhancement, and implement InvocationHandler interface; Maintain a target object in LogHandler, which is the proxy object (real subject role); Write the logical processing of method calls in the invoke method

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Date;
 
public class LogHandler implements InvocationHandler {
    Object target;  // The object being represented, the actual method executor
 
    public LogHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);  // Call the method method of target
        after();
        return result;  // Returns the execution result of the method
    }
    // Execute before invoking the invoke method
    private void before() {
        System.out.println(String.format("log start time [%s] ", new Date()));
    }
    // Execute after invoking the invoke method
    private void after() {
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

To write a client and obtain the dynamically generated Proxy class object, you must use the newProxyInstance method of the Proxy class. See the code and comments for the specific steps

import proxy.UserService;
import proxy.UserServiceImpl;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
 
public class Client2 {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException {
        // Setting the variable can save the dynamic proxy class, and the default name is named in the format of $Proxy0
        // System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        // 1. Create the proxy object and the implementation class of UserService interface
        UserServiceImpl userServiceImpl = new UserServiceImpl();
        // 2. Get the corresponding ClassLoader
        ClassLoader classLoader = userServiceImpl.getClass().getClassLoader();
        // 3. Get the classes of all interfaces. The UserServiceImpl here implements only one interface UserService,
        Class[] interfaces = userServiceImpl.getClass().getInterfaces();
        // 4. Create a call request processor that will be passed to the proxy class to handle the method calls on all proxy objects
        //     A custom log processor is created here, and the actual execution object userServiceImpl must be passed in
        InvocationHandler logHandler = new LogHandler(userServiceImpl);
        /*
           5.According to the information provided above, in the process of creating a proxy object,
               a.JDK It will dynamically create and update data in memory according to the passed in parameter information class file equivalent bytecode
               b.Then it is converted into the corresponding class according to the corresponding bytecode,
               c.Then call newInstance() to create the proxy instance.
         */
        UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
        // Method of calling proxy
        proxy.select();
        proxy.update();
 
        // Save the proxy class generated by JDK dynamic proxy, and save the class name as UserServiceProxy
        // ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceProxy");
    }
}

Operation results

log start time [Thu Dec 20 16:55:19 CST 2018] 
query selectById
log end time [Thu Dec 20 16:55:19 CST 2018] 
log start time [Thu Dec 20 16:55:19 CST 2018] 
to update update
log end time [Thu Dec 20 16:55:19 CST 2018] 

The main methods of InvocationHandler and Proxy are described as follows:

java.lang.reflect.InvocationHandler:

Object invoke (object proxy, method, method, object [] args) defines the actions you want to perform when a proxy object invokes a method. It is used to centrally process method calls on dynamic proxy class objects

java.lang.reflect.Proxy:

static InvocationHandler getInvocationHandler(Object proxy) is used to obtain the calling processor associated with the specified proxy object

Static class getproxyclass (classloader, loader, class... Interfaces) returns the proxy class of the specified interface

Static object newproxyinstance (classloader, loader, class <? > [] interfaces, invocationhandler h) constructs a new instance of the proxy class that implements the specified interface. All methods will call the invoke method of the given processor object

Static Boolean isproxyclass (class <? > cl) Returns whether cl is a proxy class

Calling procedure of proxy class

What does the generated proxy class look like? With the help of the following tool classes, save the proxy class and find out (you can also save the proxy class by setting the environment variable sun.misc.ProxyGenerator.saveGeneratedFiles=true)

import sun.misc.ProxyGenerator;
import java.io.FileOutputStream;
import java.io.IOException;
 
public class ProxyUtils {
    /**
     * Save the binary bytecode dynamically generated according to the class information to the hard disk. The default is in the clazz directory
     * params: clazz Classes that need to generate dynamic proxy classes
     * proxyName: The name of the dynamically generated proxy class for
     */
    public static void generateClassFile(Class clazz, String proxyName) {
        // Generate bytecode according to the class information and the provided proxy class name
        byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, clazz.getInterfaces());
        String paths = clazz.getResource(".").getPath();
        System.out.println(paths);
        FileOutputStream out = null;
        try {
            //Keep to hard disk
            out = new FileOutputStream(paths + proxyName + ".class");
            out.write(classFile);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Then add a line of code to the last side of the main of the Client2 test class

// Save the proxy class generated by JDK dynamic proxy, and save the class name as UserServiceProxy
ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceProxy");

After IDEA runs again, you can find userserviceproxy under the classpath of target Class. After double clicking, the decompilation plug-in of IDEA will the binary class file

The code of UserServiceProxy is as follows:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import proxy.UserService;
 
public final class UserServiceProxy extends Proxy implements UserService {
    private static Method m1;
    private static Method m2;
    private static Method m4;
    private static Method m0;
    private static Method m3;
 
    public UserServiceProxy(InvocationHandler var1) throws  {
        super(var1);
    }
 
    public final boolean equals(Object var1) throws  {
        // Omit
    }
 
    public final String toString() throws  {
        // Omit
    }
 
    public final void select() throws  {
        try {
            super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
 
    public final int hashCode() throws  {
        // Omit
    }
 
    public final void update() throws  {
        try {
            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");
            m4 = Class.forName("proxy.UserService").getMethod("select");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m3 = Class.forName("proxy.UserService").getMethod("update");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

From the code of UserServiceProxy, we can find that:

  • UserServiceProxy inherits the Proxy class and implements all the interfaces being proxied, as well as the equals, hashCode, toString and other methods
  • Because UserServiceProxy inherits the Proxy class, each Proxy class is associated with an InvocationHandler method call handler
  • Class and all methods are modified by public final, so proxy classes can only be used and can no longer be inherited
  • Each Method has a Method object to describe. The Method object is created in static static code block and named in the format of m + number
  • When calling a method, you can use super h.invoke(this, m1, (Object[])null); Call, where super h. The invoke is actually passed to the proxy when the proxy is created The LogHandler object of newproxyinstance inherits the InvocationHandler class and is responsible for the actual call processing logic

After receiving the method, args and other parameters, the invoke method of LogHandler performs some processing, and then allows the proxy object target to execute the method through reflection

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);       // Call the method method of target
        after();
        return result;  // Returns the execution result of the method
    }

The process diagram of DK dynamic agent executing method call is as follows:

The calling process of Proxy class is believed to be clear to everyone. For the source code analysis of Proxy, please refer to other articles or directly look at the source code.

CGLIB dynamic proxy

maven introduces the CGLIB package and then writes a UserDao class. It has no interface and only two methods, select() and update()

public class UserDao {
    public void select() {
        System.out.println("UserDao query selectById");
    }
    public void update() {
        System.out.println("UserDao to update update");
    }
}

Write a LogInterceptor, which inherits the MethodInterceptor and is used to intercept callbacks of methods

import java.lang.reflect.Method;
import java.util.Date;
 
public class LogInterceptor implements MethodInterceptor {
    /**
     * @param object Represents the object to be enhanced
     * @param method Indicates the method of interception
     * @param objects Array represents the parameter list. The basic data type needs to be passed in its wrapper type, such as int -- > integer, long long, double -- > double
     * @param methodProxy Represents the proxy to the method, and the invokeSuper method represents the call to the proxy object method
     * @return results of enforcement
     * @throws Throwable
     */
    @Override
    public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object result = methodProxy.invokeSuper(object, objects);   // Note that this is to call invokeSuper instead of invoke, otherwise it will be an endless loop, methodproxy invokeSuper executes the method of the original class, method Invoke executes methods of subclasses
        after();
        return result;
    }
    private void before() {
        System.out.println(String.format("log start time [%s] ", new Date()));
    }
    private void after() {
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

test

import net.sf.cglib.proxy.Enhancer;
 
public class CglibTest {
    public static void main(String[] args) {
        DaoProxy daoProxy = new DaoProxy(); 
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Dao.class);  // Set the superclass. cglib is implemented through inheritance
        enhancer.setCallback(daoProxy);
 
        Dao dao = (Dao)enhancer.create();   // Create proxy class
        dao.update();
        dao.select();
    }
}

Operation results

log start time [Fri Dec 21 00:06:40 CST 2018] 
UserDao query selectById
log end time [Fri Dec 21 00:06:40 CST 2018] 
log start time [Fri Dec 21 00:06:40 CST 2018] 
UserDao to update update
log end time [Fri Dec 21 00:06:40 CST 2018] 

You can further filter multiple methodinterceptors

public class LogInterceptor2 implements MethodInterceptor {
    @Override
    public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object result = methodProxy.invokeSuper(object, objects);
        after();
        return result;
    }
    private void before() {
        System.out.println(String.format("log2 start time [%s] ", new Date()));
    }
    private void after() {
        System.out.println(String.format("log2 end time [%s] ", new Date()));
    }
}
 
// Callback filter: during CGLib callback, you can set different callback logic for different methods, or do not execute callback at all.
public class DaoFilter implements CallbackFilter {
    @Override
    public int accept(Method method) {
        if ("select".equals(method.getName())) {
            return 0;   // The first interceptor in the Callback list
        }
        return 1;   // The second interceptor in the Callback list, return 2 is the third, and so on
    }
}

Retest

public class CglibTest2 {
    public static void main(String[] args) {
        LogInterceptor logInterceptor = new LogInterceptor();
        LogInterceptor2 logInterceptor2 = new LogInterceptor2();
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(UserDao.class);   // Set the superclass. cglib is implemented through inheritance
        enhancer.setCallbacks(new Callback[]{logInterceptor, logInterceptor2, NoOp.INSTANCE});   // Set multiple interceptors, NOOP Instance is an empty interceptor and does not do any processing
        enhancer.setCallbackFilter(new DaoFilter());
 
        UserDao proxy = (UserDao) enhancer.create();   // Create proxy class
        proxy.select();
        proxy.update();
    }
}

Operation results

log start time [Fri Dec 21 00:22:39 CST 2018] 
UserDao query selectById
log end time [Fri Dec 21 00:22:39 CST 2018] 
log2 start time [Fri Dec 21 00:22:39 CST 2018] 
UserDao to update update
log2 end time [Fri Dec 21 00:22:39 CST 2018] 

CGLIB creates dynamic proxy classes in the following mode:

  1. Find the method definitions of all non final public types on the target class;
  2. Convert the definitions of these methods into bytecode;
  3. Convert the bytecode into the class object of the corresponding agent;
  4. Implement the MethodInterceptor interface to process requests for all methods on the proxy class;

Comparison between JDK dynamic agent and CGLIB dynamic agent

JDK dynamic proxy: it is implemented based on Java reflection mechanism. Only when the business class of the interface is implemented can the proxy object be generated in this way.

CGLIB dynamic proxy: it is implemented based on ASM mechanism by generating subclasses of business classes as proxy classes.

Advantages of JDK Proxy:

  • Minimizing dependencies and reducing dependencies means simplifying development and maintenance. The support of JDK itself may be more reliable than CGLIB.
  • The JDK version is upgraded smoothly, and the bytecode class library usually needs to be updated to ensure that it can be used on the new version of Java.
  • The code implementation is simple.

Based on the advantages of similar CGLIB framework:

  • There is no need to implement the interface to achieve no intrusion of the proxy class.
  • Only operate the classes we care about, without adding workload to other related classes.
  • High performance.

Interview questions

Describe several implementations of dynamic agents? Tell the corresponding advantages and disadvantages

Agents can be divided into "static agent" and "dynamic agent", and dynamic agent can be divided into "JDK dynamic agent" and "CGLIB dynamic agent".

Static proxy: the proxy object and the actual object inherit the same interface. The proxy object points to the instance of the actual object. In this way, the proxy object is exposed and the Real Object is called

  • Advantages: it can well protect the external exposure of the business logic of the actual object, so as to improve the security.
  • Disadvantages: different interfaces need different proxy class implementations, which will be redundant

JDK dynamic agent:

  • In order to solve the redundancy caused by generating a large number of agent classes in static agents;
  • JDK dynamic proxy only needs to implement the InvocationHandler interface and rewrite the invoke method to complete the proxy implementation,
  • JDK dynamic proxy uses reflection to generate proxy class proxyxx Class proxy class bytecode and generate objects
  • The reason why JDK dynamic Proxy can only Proxy interfaces is that the Proxy class itself has extended Proxy, and java does not allow multiple inheritance, but allows multiple interfaces to be implemented
  • Advantages: it solves the problem of redundant agent implementation classes in static agents.
  • Disadvantages: JDK dynamic agent is designed and implemented based on interface. If there is no interface, exceptions will be thrown.

CGLIB agent:

  • Because the JDK dynamic agent limits the design based on the interface, but the JDK method can not solve the situation without the interface;
  • CGLib adopts very low-level bytecode technology. Its principle is to create a subclass for a class through bytecode technology, intercept the calls of all parent methods in the subclass by using method interception technology, and weave cross cutting logic to complete the implementation of dynamic agent.
  • The implementation method implements the MethodInterceptor interface, rewrites the intercept method, and implements it through the callback method of Enhancer class.
  • However, CGLib takes much more time to create proxy objects than JDK. Therefore, for singleton objects, CGLib is appropriate because there is no need to create objects frequently. On the contrary, JDK is more appropriate.
  • At the same time, because CGLib uses the method of dynamically creating subclasses, it cannot proxy the final method.
  • Advantages: dynamic proxy can be realized without interface, and bytecode enhancement technology is adopted, with good performance.
  • Disadvantages: the technical implementation is relatively difficult to understand.

CGlib implements proxy for interface?

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import proxy.UserService;
import java.lang.reflect.Method;
 
/**
 * Create a factory for the proxy class that implements the MethodInterceptor interface.
 * Three tasks are completed in this category:
 * (1)Declare the member variables of the target class and create a constructor with the target class object as a parameter. Used to receive the target object
 * (2)Defines the generation method of the proxy, which is used to create proxy objects. The method name is arbitrary. The proxy object is a subclass of the target class
 * (3)Define callback interface methods. The enhancement of the target class is done here
 */
public class CGLibFactory implements MethodInterceptor {
    // Declare member variables of the target class
    private UserService target;
 
    public CGLibFactory(UserService target) {
        this.target = target;
    }
    // Defines the generation method of the proxy, which is used to create proxy objects
    public UserService myCGLibCreator() {
        Enhancer enhancer = new Enhancer();
        // Set the parent class for the proxy object, that is, specify the target class
        enhancer.setSuperclass(UserService.class);
        /**
         * Set the callback interface object. Note that only this can be written in the setCallback() method,
         * This is because the methodinterceptor interface inherits from Callback and is its sub interface
         */
        enhancer.setCallback(this);
        return (UserService) enhancer.create();// create is used to generate CGLib proxy objects
    }
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("start invoke " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("end invoke " + method.getName());
        return result;
    }
}

reference material

Java core technology Volume I

In depth understanding of Java virtual machine 7.3

java docs: https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Proxy.html

There are three Java proxy modes: static proxy, dynamic proxy and cglib proxy

Describe several implementation methods of dynamic agent, and state the corresponding advantages and disadvantages

JDK dynamic agent details

Detailed explanation of Java dynamic proxy mechanism (JDK and CGLIB, Javassist, ASM)

Understanding of static and dynamic agents

Added by adx on Wed, 19 Jan 2022 07:26:55 +0200