Design Patterns-Dynamic Agent Principle and Writing a Dynamic Agent of Our Own by Imitating JDK Proxy

Keywords: Java JDK Spring github

This article contains a lot of code, which may be a bit rough. You can read it selectively.

The purpose of this article is to simply analyze the principle of dynamic agent, to imitate JDK Proxy to write a dynamic agent and to summarize several agents.

For the introduction and explanation of the agency model, there are many high-quality articles on the Internet, I will not introduce too much here, here recommend several high-quality articles for reference:

  1. Explain to your girlfriend what the agency model is
  2. Easy learning, proxy mode and dynamic proxy in Java

In addition, my github repository has relevant basic sample code in the corresponding directory: https://github.com/eamonzzz/java-advanced...

JDK Proxy Dynamic Agent

The concept of dynamic agent will not be explained here; compared with static agent, dynamic agent has more powerful functions and stronger adaptability with the expansion of business.

Before we talk about the principle of dynamic proxy, let's look at the general use of dynamic proxy.

Use

This article uses the code of the simplest proxy pattern as an example. I believe you have seen or touched the code when you learn or understand the proxy pattern.

  1. First create a Subject principal Abstract interface:
/**
 * @author eamon.zhang
 * @date 2019-10-09 4:06 p.m.
 */
public interface Subject {
    void request();
}
  1. Create a real subject RealSubject to handle our real logic:
/**
 * @author eamon.zhang
 * @date 2019-10-09 4:06 p.m.
 */
public class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("Real processing logic!");
    }
}
  1. Without modifying the RealSubject class, how do we implement a piece of logic before or after executing the request() method in the RealSubject class? This requires creating a proxy class to enhance the original code. So now create a JDK dynamic proxy class RealSubjectJDK DynamicProxy:
/**
 * @author eamon.zhang
 * @date 2019-10-09 4:08 p.m.
 */
public class RealSubjectJDKDynamicProxy implements InvocationHandler {
    // Reference to the Proxy Object
    private Object target;
    // Import object references through constructors
    public RealSubjectJDKDynamicProxy(Object target) {
        this.target = target;
    }
    // Obtain the proxy object created by JDK dynamic proxy
    public Object getInstance() {
        Class<?> clazz = target.getClass();
        return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
    }

    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        before();
        // Approaches to Proxy Execution of Proxy Objects
        Object invoke = method.invoke(target, objects);
        after();
        return invoke;
    }

    private void before() {
        System.out.println("Pre-enhancement!");
    }

    private void after() {
        System.out.println("Post Enhancement!");
    }
}
  1. Test code:
@Test
public void test(){
    Subject realSubject = new RealSubject();
    RealSubjectJDKDynamicProxy proxy = new RealSubjectJDKDynamicProxy(realSubject);
    Subject instance = (Subject) proxy.getInstance();
    instance.request();
    System.out.println(realSubject.getClass());
    System.out.println(instance.getClass());
}
  1. test result
Pre-enhancement!
Real processing logic!
Post Enhancement!
class com.eamon.javadesignpatterns.proxy.dynamic.jdk.RealSubject
class com.sun.proxy.$Proxy8

As a result, the above code has achieved our enhancement purpose.

Principle analysis

I wonder if you have noticed that in the last two lines of the test code above, I printed out the class objects before and after the proxy, and found that the two objects are not the same. Most importantly, the Subject of the object after the proxy is com.sun.proxy.$Proxy8 instead of com.eamon.javadepatterns.proxy.dynamic.jdk.RealSubject or com.eamon.javadesignpatterns.proxy.dynamic.jdk.Subject, so where exactly does this instance come from? With this in mind, let's analyze the JDK Proxy source code.

We follow up Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this) in the RealSubjectJDK DynamicProxy class.

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 {
        ...
        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);
    }
   ...
}

It is found that the getProxyClass0(loader, intfs) method is invoked in the newProxyInstance method. Let's take a look at this method.

/**
 * 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 code logic is simple, and it does two things:

  1. Check if the number of interfaces in the class exceeds 65535. The number of interfaces is stored in 2 byte s, with a maximum support of 65535.
  2. From the proxyClassCache cache, you can see from the annotations that if the cache is not available, you will call ProxyClassFactory to create it.

Now let's briefly analyze the logic in proxyClassCache.get(loader, interfaces):

public V get(K key, P parameter) {
    Objects.requireNonNull(parameter);

    expungeStaleEntries();

    Object cacheKey = CacheKey.valueOf(key, refQueue);

    // lazily install the 2nd level valuesMap for the particular cacheKey
    ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
    if (valuesMap == null) {
        ConcurrentMap<Object, Supplier<V>> oldValuesMap
            = map.putIfAbsent(cacheKey,
                              valuesMap = new ConcurrentHashMap<>());
        if (oldValuesMap != null) {
            valuesMap = oldValuesMap;
        }
    }

    // create subKey and retrieve the possible Supplier<V> stored by that
    // subKey from valuesMap
    Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
    Supplier<V> supplier = valuesMap.get(subKey);
    Factory factory = null;
    // Here is a while (true)
    while (true) {
        // If the factory (ProxyClassFactory in this case) is created successfully, the factory.get() method is called.
        if (supplier != null) {
            // supplier might be a Factory or a CacheValue<V> instance
            //
            V value = supplier.get();
            if (value != null) {
                return value;
            }
        }
        // else no supplier in cache
        // or a supplier that returned null (could be a cleared CacheValue
        // or a Factory that wasn't successful in installing the CacheValue)

        // lazily construct a Factory
        if (factory == null) {
            factory = new Factory(key, parameter, subKey, valuesMap);
        }

        if (supplier == null) {
            supplier = valuesMap.putIfAbsent(subKey, factory);
            if (supplier == null) {
                // successfully installed Factory
                supplier = factory;
            }
            // else retry with winning supplier
        } else {
            if (valuesMap.replace(subKey, supplier, factory)) {
                // successfully replaced
                // cleared CacheEntry / unsuccessful Factory
                // with our Factory
                supplier = factory;
            } else {
                // retry with current supplier
                supplier = valuesMap.get(subKey);
            }
        }
    }
}

The code may be a bit long, but the logic is to call ProxyClassFactory.apply() to generate proxy classes. Let's look at dividing the code into two parts at while(true):

  1. In the first half, the ProxyClassFactory is fetched from the cache, and if it is created successfully, it can be fetched (the key in the cache is not analyzed here).
  2. Then look at the logic in the while(true) code block, if (supplier!= null), the judgment that if a ProxyClassFactory is created in the cache, it will execute supplier.get() and terminate the loop; if not, it will execute a new Factory(key, parameter, subKey, valuesMap); create a factory, then put it in the cache supplier, and then continue the loop, at which time it will execute if. (supplier!= null) The logic in the code block, let's analyze the code in this code block again:
if (supplier != null) {
    // supplier might be a Factory or a CacheValue<V> instance
    V value = supplier.get();
    if (value != null) {
        return value;
    }
}

Follow up the supplier.get() method to see, we can see from the above analysis that the supplier here is actually a Factory, so we look at the implementation of Factory, focusing on get() method:

private final class Factory implements Supplier<V> {
       ...
        @Override
        public synchronized V get() { // serialize access
            ...
            // create new value
            V value = null;
            try {
                value = Objects.requireNonNull(valueFactory.apply(key, parameter));
            } finally {
                if (value == null) { // remove us on failure
                    valuesMap.remove(subKey, this);
                }
            }
            // the only path to reach here is with non-null value
            assert value != null;

            // wrap value with CacheValue (WeakReference)
            CacheValue<V> cacheValue = new CacheValue<>(value);

            // put into reverseMap
            reverseMap.put(cacheValue, Boolean.TRUE);

            // try replacing us with CacheValue (this should always succeed)
            if (!valuesMap.replace(subKey, this, cacheValue)) {
                throw new AssertionError("Should not reach here");
            }

            // successfully replaced us with new CacheValue -> return the value
            // wrapped by it
            return value;
        }
    }

We noticed that the focus of the code is on Objects. requireNonNull (valueFactory. apply (key, parameter);, what is the valueFactory in this code? Let's look at the definition of proxyClassCache in Proxy

 private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
        proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

WeakCacheThe second parameter isnew ProxyClassFactory() ,Let's look at the corresponding constructor:

public WeakCache(BiFunction<K, P, ?> subKeyFactory,
                 BiFunction<K, P, V> valueFactory) {
    this.subKeyFactory = Objects.requireNonNull(subKeyFactory);
    this.valueFactory = Objects.requireNonNull(valueFactory);
}

Do you understand at this time? In fact, valueFactory is ProxyClassFactory()

With that in mind, let's analyze what valueFactory.apply(key, parameter) actually does. Let's look directly at the code for ProxyClassFactory

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) {

        ...

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

Overall, it is not difficult to analyze that the code is actually creating the intermediate proxy Class of $Proxy, in which byte[] proxyClassFile is the bytecode file data of the Class assembled in the code block, which is generated by ProxyGenerator.generateProxyClass(); and then the classloader dynamically loads the bytecode, generates the Class instance of the dynamic proxy Class, and returns it.

Let's follow up the ProxyGenerator.generateProxyClass() method to see the processing logic in the generation of proxy classes and the key code:.

 public static byte[] generateProxyClass(final String var0, Class<?>[] var1, int var2) {
    ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);
    final byte[] var4 = var3.generateClassFile();
    ...

    return var4;
}

You can see that its code calls var3.generateClassFile() to generate the Class file, so we follow the generateClassFile() method to see the main content:

private byte[] generateClassFile() {
    this.addProxyMethod(hashCodeMethod, Object.class);
    this.addProxyMethod(equalsMethod, Object.class);
    this.addProxyMethod(toStringMethod, Object.class);
    Class[] var1 = this.interfaces;
    int var2 = var1.length;

    int var3;
    Class var4;
    for(var3 = 0; var3 < var2; ++var3) {
        var4 = var1[var3];
        Method[] var5 = var4.getMethods();
        int var6 = var5.length;

        for(int var7 = 0; var7 < var6; ++var7) {
            Method var8 = var5[var7];
            this.addProxyMethod(var8, var4);
        }
    }
    ...
}

The code is a bit long, but it's not all expanded here. Interested friends can follow up and have a detailed look. From the code, we can see roughly that in the process of generating proxy classes, three methods, hashCode, equals and toString, are added. Then the logic is to iterate all the interfaces in the proxy object, to regenerate all the methods of proxy classes, and to generate bytecodes.

Finally, the proxy class is loaded into the JVM.

Take a look at the proxy class $Proxy generated by JDK Proxy

We output the $Proxy file to the file through the following code:

@Test
public void test1(){
    System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
    RealSubject realSubject = new RealSubject();

    RealSubjectJDKDynamicProxy proxy = new RealSubjectJDKDynamicProxy(realSubject);
    Subject instance = (Subject) proxy.getInstance();
    try {
        byte[] proxychar=  ProxyGenerator.generateProxyClass("$Proxy0", new Class[]{Subject.class});
        OutputStream outputStream = new FileOutputStream("/Users/eamon.zhang/IdeaProjects/own/java-advanced/01.DesignPatterns/design-patterns/"+instance.getClass().getSimpleName()+".class");
        outputStream.write(proxychar);
        outputStream.flush();
        outputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    instance.request();
    System.out.println(instance.getClass());

}

Check out $Proxy0 with the IDEA tool to confirm our previous analysis:

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

    public $Proxy0(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 void request() throws  {
        try {
            super.h.invoke(this, m3, (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);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("com.eamon.javadesignpatterns.proxy.dynamic.jdk.Subject").getMethod("request");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

summary

Summarize the implementation steps of JDK Proxy:

  1. Get a reference to the proxy object and all its interfaces (through reflection)
  2. The JDK Proxy class regenerates a new class. At the same time, the new class implements all the interfaces implemented by the proxy class. There are three methods: hashCode, equals and toString.
  3. Generate Java code dynamically, and call new business logic methods from certain logic code (reflected in the code)
  4. Compile the. class file of the newly generated Java code
  5. Reload to run in JVM

Simulated Handwritten JDK Proxy

After understanding the above principles, we can actually try to implement a JDK Proxy manually:

Referring to the principle of JDK Proxy implementation, we analyze what needs to be written by hand.

  • First we need to have a proxy class, MimeProxy
  • Then starting from the proxy class, we need a new Proxy Instance (clazz. getClassLoader (), clazz. getInterfaces (), this) method, the method parameters are: (ClassLoader loader, Class <> [] interfaces, Invocation Handler h), so we need to create a ClassLoader, Invocation Handler;

Step by step, create:

  1. First create the MimeClassLoader class, inherit from ClassLoader, and override the findClass() method:
/**
 * @author eamon.zhang
 * @date 2019-10-10 2:47 p.m.
 */
public class MimeClassLoader extends ClassLoader {
    private Object target;

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

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String classname = target.getClass().getPackage().getName() + "." + name;
        String filePath = MimeClassLoader.class.getResource("").getPath() + name + ".class";
        try {
            URI uri = new URI("file:///" + filePath);
            Path path = Paths.get(uri);
            File file = path.toFile();
            if (file.exists()) {
                byte[] fileBytes = Files.readAllBytes(path);
                return defineClass(classname, fileBytes, 0, fileBytes.length);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}
  1. Create the MimeInvocationHandler class:
/**
 * @author eamon.zhang
 * @date 2019-10-10 2:46 p.m.
 */
public interface MimeInvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable;
}
  1. Create the MimeProxy class, which is used to assemble the proxy class, load it into the JVM, and then return the proxy object:
/**
 * @author eamon.zhang
 * @date 2019-10-10 3:08 p.m.
 */
public class MimeProxy {
    private static final String ln = "\r\n";
    private static final String semi = ";";

    private static Map<Class, Class> mappings = new HashMap<Class, Class>();

    static {
        mappings.put(int.class, Integer.class);
    }

    public static Object newProxyInstance(MimeClassLoader loader, Class<?>[] interfaces, MimeInvocationHandler h)
            throws IllegalArgumentException {
        try {
            // 1. Dynamic Generation of. java Files
            String src = generateSrc(interfaces);
//            System.out.println(src);
            // 2. java file output to disk
            String filePath = MimeProxy.class.getResource("").getPath();
//            System.out.println(filePath);
            File f = new File(filePath + "$Proxy8.java");
//            f.deleteOnExit();
            FileWriter fw = new FileWriter(f);
            fw.write(src);
            fw.flush();
            fw.close();
            // 3. Compile java files into. class files
            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
            StandardJavaFileManager sjfm = compiler.getStandardFileManager(null, null, null);
            Iterable<? extends JavaFileObject> iterable = sjfm.getJavaFileObjects(f);
            JavaCompiler.CompilationTask task = compiler.getTask(null, sjfm, null, null, null, iterable);
            task.call();
            sjfm.close();
            // 4. Load the. class file into jvm
            Class<?> proxyClass = loader.findClass("$Proxy8");
            Constructor<?> c = proxyClass.getConstructor(MimeInvocationHandler.class);
            f.delete();

            // 5. Return a new proxy object after bytecode reorganization
            return c.newInstance(h);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;

    }

    /**
     * Generating proxy classes
     *
     * @param interfaces
     * @return
     */
    private static String generateSrc(Class<?>[] interfaces) {
        // Thread security using StringBuffer here
        StringBuffer sb = new StringBuffer();
        sb.append("package ").append(interfaces[0].getPackage().getName()).append(semi).append(ln);
        sb.append("import ").append(interfaces[0].getName()).append(semi).append(ln);
        sb.append("import java.lang.reflect.*;").append(ln);
        sb.append("import ").append(interfaces[0].getPackage().getName()).append(".mimeproxy.MimeInvocationHandler;").append(ln);
        sb.append("public class $Proxy8 implements ").append(interfaces[0].getSimpleName()).append(" {").append(ln);
        sb.append("MimeInvocationHandler h;" + ln);
        sb.append("public $Proxy8(MimeInvocationHandler h) {").append(ln);
        sb.append("this.h = h;").append(ln);
        sb.append("}").append(ln);

        for (Method method : interfaces[0].getMethods()) {
            Class<?>[] params = method.getParameterTypes();

            StringBuffer paramNames = new StringBuffer();
            StringBuffer paramValues = new StringBuffer();
            StringBuffer paramClasses = new StringBuffer();

            for (Class<?> clazz : params) {
                String type = clazz.getName();
                String paramName = toLowerFirstCase(clazz.getSimpleName());

                paramNames.append(type).append(" ").append(paramName);

                paramValues.append(paramName);
                paramClasses.append(clazz.getName()).append(".class");

                for (int i = 0; i < params.length; i++) {
                    paramNames.append(",");
                    paramValues.append(",");
                    paramClasses.append(",");
                }
            }

            sb.append("public ").append(method.getReturnType().getName()).append(" ").append(method.getName())
                    .append("(").append(paramNames.toString()).append(") {").append(ln);
            sb.append("try {").append(ln);
            // Method m = interfaces[0].getName().class.getMethod(method.getName()),new Class[]{paramClasses.toString()});
            sb.append("Method m = ").append(interfaces[0].getName()).append(".class.getMethod(\"")
                    .append(method.getName()).append("\", new Class[]{").append(paramClasses.toString()).append("});")
                    .append(ln);
            // return this.h.invoke(this, m, new Object[]{paramValues}, method.getReturnType());
            sb.append(hasReturnValue(method.getReturnType()) ? "return " : "")
                    .append(getCaseCode("this.h.invoke(this,m,new Object[]{" + paramValues + "})", method.getReturnType()))
                    .append(";")
                    .append(ln);
            sb.append("} catch (Error _ex) {}").append(ln);
            sb.append("catch (Throwable e) {").append(ln);
            sb.append("throw new UndeclaredThrowableException(e);").append(ln);
            sb.append("}");
            sb.append(getReturnEmptyCode(method.getReturnType())).append(ln);
            sb.append("}");

        }
        sb.append("}").append(ln);

        return sb.toString();
    }

    /**
     * Get the return value type
     *
     * @param returnClass
     * @return
     */
    private static String getReturnEmptyCode(Class<?> returnClass) {
        if (mappings.containsKey(returnClass)) {
            return "return 0;";
        } else if (returnClass == void.class) {
            return "";
        } else {
            return "return null;";
        }
    }

    /**
     * Splicing invocationHandler execution code
     *
     * @param code
     * @param returnClass
     * @return
     */
    private static String getCaseCode(String code, Class<?> returnClass) {
        if (mappings.containsKey(returnClass)) {
            return "((" + mappings.get(returnClass).getName() + ")" + code + ")." + returnClass.getSimpleName() + "Value()";
        }
        return code;
    }

    /**
     * Determine whether there is a return value
     *
     * @param clazz
     * @return
     */
    private static boolean hasReturnValue(Class<?> clazz) {
        return clazz != void.class;
    }

    /**
     * Converting initial letters to lowercase
     *
     * @param src
     * @return
     */
    private static String toLowerFirstCase(String src) {
        char[] chars = src.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }
}

In this way, we have written a dynamic proxy of our own. Of course, the proxy method is not perfect, but we have written this example. Interested friends can try to change it into more general code.

CGlib Dynamic Agent

Let's look at the use of CGlib's dynamic proxy

Use

First create the RealSubject class. Note that this class does not implement any interfaces:

/**
 * @author eamon.zhang
 * @date 2019-10-09 4:22 p.m.
 */
public class RealSubject {
    public void request(){
        System.out.println("Real processing logic!");
    }
}

Then create the RealSubjectCglibDynamicProxy proxy class, which must implement the MethodInterceptor interface:

/**
 * @author eamon.zhang
 * @date 2019-10-09 4:23 p.m.
 */
public class RealSubjectCglibDynamicProxy implements MethodInterceptor {

    public Object getInstance(Class<?> clazz) {
        // The process of obtaining proxy objects through CGLIB dynamic proxy
        Enhancer enhancer = new Enhancer();
        // Which is to be set as the new class parent to be generated
        enhancer.setSuperclass(clazz);
        // Setting callback objects
        enhancer.setCallback(this);
        // Creating proxy objects
        return enhancer.create();
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        before();
        Object invokeSuper = proxy.invokeSuper(obj, args);
        after();
        return invokeSuper;
    }

    private void before() {
        System.out.println("Pre-enhancement!");
    }

    private void after() {
        System.out.println("Post Enhancement!");
    }
}

In this way, a simple CGlib dynamic proxy implementation is completed. Now let's create the test code:

@Test
public void test(){
    RealSubjectCglibDynamicProxy proxy = new RealSubjectCglibDynamicProxy();
    RealSubject instance = (RealSubject) proxy.getInstance(RealSubject.class);
    instance.request();
}

Test results:

Pre-enhancement!
Real processing logic!
Post Enhancement!

Principle analysis

Whether it's JDK Proxy or CGlib, their core content is to create proxy classes, so we just need to understand the process of creating proxy classes.

As can be seen from the simple examples above, in order to use CGlib dynamic proxy, the proxy class must implement MethodInterceptor (method interceptor). The source code of the MethodInterceptor interface is as follows:

/**
 * General-purpose {@link Enhancer} callback which provides for "around advice".
 * @author Juozas Baliuka <a href="mailto:baliuka@mwm.lt">baliuka@mwm.lt</a>
 * @version $Id: MethodInterceptor.java,v 1.8 2004/06/24 21:15:20 herbyderby Exp $
 */
public interface MethodInterceptor
extends Callback
{
    /**
     * All generated proxied methods call this method instead of the original method.
     * The original method may either be invoked by normal reflection using the Method object,
     * or by using the MethodProxy (faster).
     * @param obj "this", the enhanced object
     * @param method intercepted Method
     * @param args argument array; primitive types are wrapped
     * @param proxy used to invoke super (non-intercepted method); may be called
     * as many times as needed
     * @throws Throwable any exception may be thrown; if so, super method will not be invoked
     * @return any value compatible with the signature of the proxied method. Method returning void will ignore this value.
     * @see MethodProxy
     */
    public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
                               MethodProxy proxy) throws Throwable;

}

There is only one intercept method in the interface, where the incoming parameters are:

  1. obj represents an enhanced object, that is, an object that implements this interface class.
  2. Method represents the method to be intercepted;
  3. args represents method parameters;
  4. proxy means the method object to trigger the parent class.

In the logical getInstance (Class <?> clazz) for creating proxy objects, the enhancer.create() method is called. Let's follow the source code and see:

/**
 * Generate a new class if necessary and uses the specified
 * callbacks (if any) to create a new object instance.
 * Uses the no-arg constructor of the superclass.
 * @return a new instance
 */
public Object create() {
    classOnly = false;
    argumentTypes = null;
    return createHelper();
}

Source comment content translation: If necessary, generate a new class and use the specified callback (if any) to create a new object instance. Instantiate the parent class by using the parameter constructor of the parent class.

Its core content is createHelper(); method:

private Object createHelper() {
    preValidate();
    Object key = KEY_FACTORY.newInstance((superclass != null) ? superclass.getName() : null,
            ReflectUtils.getNames(interfaces),
            filter == ALL_ZERO ? null : new WeakCacheKey<CallbackFilter>(filter),
            callbackTypes,
            useFactory,
            interceptDuringConstruction,
            serialVersionUID);
    this.currentKey = key;
    Object result = super.create(key);
    return result;
}

The function of the preValidate() method is to pre-check whether callbackTypes, filter s are empty, and to process space-time.

Then we create the EnhancerKey object through the KEY_FACTORY.newInstance() method and pass it in as a parameter of the super.create(key) method. Let's look at the create() method and find that it is a method of the parent AbstractClassGenerator of the Enhancer class:

protected Object create(Object key) {
    try {
        ClassLoader loader = getClassLoader();
        Map<ClassLoader, ClassLoaderData> cache = CACHE;
        ClassLoaderData data = cache.get(loader);
        if (data == null) {
            synchronized (AbstractClassGenerator.class) {
                cache = CACHE;
                data = cache.get(loader);
                if (data == null) {
                    Map<ClassLoader, ClassLoaderData> newCache = new WeakHashMap<ClassLoader, ClassLoaderData>(cache);
                    data = new ClassLoaderData(loader);
                    newCache.put(loader, data);
                    CACHE = newCache;
                }
            }
        }
        this.key = key;
        Object obj = data.get(this, getUseCache());
        if (obj instanceof Class) {
            return firstInstance((Class) obj);
        }
        return nextInstance(obj);
    } catch (RuntimeException e) {
        throw e;
    } catch (Error e) {
        throw e;
    } catch (Exception e) {
        throw new CodeGenerationException(e);
    }
}

This method finally calls the nextInstance(obj) method, which is implemented in the Enhancer class:

protected Object nextInstance(Object instance) {
    EnhancerFactoryData data = (EnhancerFactoryData) instance;

    if (classOnly) {
        return data.generatedClass;
    }

    Class[] argumentTypes = this.argumentTypes;
    Object[] arguments = this.arguments;
    if (argumentTypes == null) {
        argumentTypes = Constants.EMPTY_CLASS_ARRAY;
        arguments = null;
    }
    return data.newInstance(argumentTypes, arguments, callbacks);
}

The data.newInstance(argumentTypes, arguments, callbacks) method is called here. The first parameter is the constructor type of the proxy object, the second is the method parameter of the proxy object, and the third is the corresponding callback object. The source code is as follows:

public Object newInstance(Class[] argumentTypes, Object[] arguments, Callback[] callbacks) {
    setThreadCallbacks(callbacks);
    try {
        // Explicit reference equality is added here just in case Arrays.equals does not have one
        if (primaryConstructorArgTypes == argumentTypes ||
                Arrays.equals(primaryConstructorArgTypes, argumentTypes)) {
            // If we have relevant Constructor instance at hand, just call it
            // This skips "get constructors" machinery
            return ReflectUtils.newInstance(primaryConstructor, arguments);
        }
        // Take a slow path if observing unexpected argument types
        return ReflectUtils.newInstance(generatedClass, argumentTypes, arguments);
    } finally {
        // clear thread callbacks to allow them to be gc'd
        setThreadCallbacks(null);
    }

}

We find that the logic here means that we can use the proxy class of cglib to write the class file in memory to the local disk by generating the object through reflection according to the incoming parameters:

@Test
public void test1(){
    //Using the proxy class of cglib, the class file in memory can be written to the local disk
    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/eamon.zhang/Documents/cglib");
    RealSubjectCglibDynamicProxy proxy = new RealSubjectCglibDynamicProxy();
    RealSubject instance = (RealSubject) proxy.getInstance(RealSubject.class);
    instance.request();
}

After execution, you can see in the corresponding directory that the three. class files in the following figure are generated:

Through debug tracking, we find that RealSubject$$EnhancerByCGLIB$89cdca is the proxy class generated by CGLib and inherits the RealSubject class. View the source code through IDEA:

public class RealSubject$$EnhancerByCGLIB$$5389cdca extends RealSubject implements Factory {
    ...
    static void CGLIB$STATICHOOK1() {
        CGLIB$THREAD_CALLBACKS = new ThreadLocal();
        CGLIB$emptyArgs = new Object[0];
        Class var0 = Class.forName("com.eamon.javadesignpatterns.proxy.dynamic.cglib.RealSubject$$EnhancerByCGLIB$$5389cdca");
        Class var1;
        CGLIB$request$0$Method = ReflectUtils.findMethods(new String[]{"request", "()V"}, (var1 = Class.forName("com.eamon.javadesignpatterns.proxy.dynamic.cglib.RealSubject")).getDeclaredMethods())[0];
        CGLIB$request$0$Proxy = MethodProxy.create(var1, var0, "()V", "request", "CGLIB$request$0");
        Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
        CGLIB$equals$1$Method = var10000[0];
        CGLIB$equals$1$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$1");
        CGLIB$toString$2$Method = var10000[1];
        CGLIB$toString$2$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$2");
        CGLIB$hashCode$3$Method = var10000[2];
        CGLIB$hashCode$3$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$3");
        CGLIB$clone$4$Method = var10000[3];
        CGLIB$clone$4$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$4");
    }

    final void CGLIB$request$0() {
        super.request();
    }

    public final void request() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
            var10000.intercept(this, CGLIB$request$0$Method, CGLIB$emptyArgs, CGLIB$request$0$Proxy);
        } else {
            super.request();
        }
    }
    ...
}

We can see from the source code of the proxy class that the proxy class will get all the methods inherited from the parent class, and there will be MethodProxy corresponding to it, such as Method CGLIB$request Method CGLIB $request $0 $Method,$Method, MethodProxy CGLIB$request MethodProxy CGLIB $request $0 $Proxy,$Proxy, which are called in the reuqest() of the proxy class.

Call procedure: The proxy object calls this.request() method - > call interceptor - > methodProxy. invokeSuper - > CGLIB $request $0 () - > the proxy object request() method. At this point, we find that the invokeSuper method of MethodProxy calls the proxy method in the interceptor MethodInterceptor.

MethodProxy is crucial. Let's analyze what it does specifically.

public class MethodProxy {
    private Signature sig1;
    private Signature sig2;
    private CreateInfo createInfo;

    private final Object initLock = new Object();
    private volatile FastClassInfo fastClassInfo;

    /**
     * For internal use by {@link Enhancer} only; see the {@link net.sf.cglib.reflect.FastMethod} class
     * for similar functionality.
     */
    public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {
        MethodProxy proxy = new MethodProxy();
        proxy.sig1 = new Signature(name1, desc);
        proxy.sig2 = new Signature(name2, desc);
        proxy.createInfo = new CreateInfo(c1, c2);
        return proxy;
    }
    ...

    private static class CreateInfo
    {
        Class c1;
        Class c2;
        NamingPolicy namingPolicy;
        GeneratorStrategy strategy;
        boolean attemptLoad;

        public CreateInfo(Class c1, Class c2)
        {
            this.c1 = c1;
            this.c2 = c2;
            AbstractClassGenerator fromEnhancer = AbstractClassGenerator.getCurrent();
            if (fromEnhancer != null) {
                namingPolicy = fromEnhancer.getNamingPolicy();
                strategy = fromEnhancer.getStrategy();
                attemptLoad = fromEnhancer.getAttemptLoad();
            }
        }
    }
    ...

Continue with the invokeSuper() method:

public Object invokeSuper(Object obj, Object[] args) throws Throwable {
    try {
        init();
        FastClassInfo fci = fastClassInfo;
        return fci.f2.invoke(fci.i2, obj, args);
    } catch (InvocationTargetException e) {
        throw e.getTargetException();
    }
}

private static class FastClassInfo
{
    FastClass f1;
    FastClass f2;
    int i1;
    int i2;
}

The above code call procedure is to get the FastClass corresponding to the proxy class and execute the proxy method. Remember generating three class files before? RealSubject$$EnhancerByCGLIB$89cdca$$FastClassByCGLIB$b94d72.class is the FastClass of the proxy class. RealSubject $$FastClassByCGLIB $$ed23432. This is the FastClass of the proxy class.

CGLib dynamic agent execution agent method is more efficient than JDK because Cglib adopts FastClass mechanism. Its principle is simply as follows:

  • Generate a Class for each of the proxy class and the proxy class, which assigns an index(int type) to the method of the proxy class or the proxy class. As an input parameter, FastClass can directly locate the method to be invoked and make the invocation directly, thus eliminating reflection invocation, so the invocation efficiency is higher than that of JDK dynamic proxy through reflection invocation.

So far, the principle of Cglib dynamic proxy is basically clear, if you are interested in code details of the small partners can further study.

Comparison between JDK Proxy and CGlib

  1. JDK dynamic proxy implements the interface of the proxy object, and CGLib inherits the proxy object.
  2. Both JDK and CGLib generate bytecode at run time. JDK directly writes Class bytecode. CGLib uses ASM framework to write Class bytecode. Cglib proxy implements more complex and generates proxy classes with lower efficiency than JDK.
  3. JDK calls proxy method through reflection mechanism, CGLib calls method directly through FastClass mechanism, and CGLib performs more efficiently.

Agent Model and Spring

Principle of Agent Selection in Spring

  1. Spring uses JDK's dynamic proxy when the Bean has an implementation interface
  2. Spring chooses CGLib when the Bean does not implement the interface.
  3. Spring can enforce CGLib through configuration by adding the following code to Spring's configuration file:
<aop:aspectj-autoproxy proxy-target-class="true"/>

Reference material: https://docs.spring.io/spring...

summary

Essential difference between static proxy and dynamic proxy

  1. Static proxy can only complete proxy operation manually. If new methods are added to the proxy class, the proxy class needs to be added synchronously, which violates the open-close principle.
  2. Dynamic proxy generates code dynamically at runtime, eliminating the restriction on the extension of the proxy class and following the open-close principle.
  3. If the dynamic proxy wants to extend the enhanced logic of the target class and combine with the policy pattern, it only needs to add the new policy class, without modifying the code of the proxy class.

Advantages and disadvantages of agent model

Advantage

  1. The proxy pattern can separate the proxy object from the real object being called.
  2. To a certain extent, it reduces the coupling degree of the system and has good expansibility.
  3. It can protect the target object.
  4. Enhanced functionality for target objects

shortcoming

  1. Proxy mode will increase the number of classes in system design.
  2. Adding a proxy object to the client and the target object will slow down request processing.
  3. The complexity of the system is increased.

Source directory for this article: https://github.com/eamonzzz/j...

Test class source directory: https://github.com/eamonzzz/j...

Welcome to star Source Code and make progress together. I will record articles and source code while learning according to the outline on git.~

Shortly after the blogger started to write his blog, if there are any mistakes or suggestions in the article, please point out in the message and learn from you.~

This article by the blog article multiple platform OpenWrite Release!

Posted by grga on Mon, 14 Oct 2019 18:13:34 -0700