【spring】静态代理模式和JDK、CGLIB动态代理

前言

温习一下动态代理的知识,如果对java的动态代理无所了解的话,那么对AOP源码的分析就无从谈起。

代理模式可分为静态代理和动态代理两种。而动态代理又有JDK、CGLIB动态代理之分。下面我们逐步分析这几种代理。

1. 静态代理

接口和实现类,接口只模拟了两个接口方法,账户查询和账户更新,并在实现类里进行了打印。

package com.lyc.cn.v2.day04.proxy.proxy;

/**
 * 账户接口
 */
public interface Count {
    
    
    // 查询账户
    void queryCount();

    // 修改账户
    void updateCount();
}
package com.lyc.cn.v2.day04.proxy.proxy;

/**
 * @author: LiYanChao
 * @create: 2018-10-21 12:07
 */
public class CountImpl implements Count {
    
    
    @Override
    public void queryCount() {
    
    
        System.out.println("==查询账户");
    }

    @Override
    public void updateCount() {
    
    
        System.out.println("==更新账户");
    }
}

代理类:

package com.lyc.cn.v2.day04.proxy.proxy;

/**
 * 代理类
 * @author: LiYanChao
 * @create: 2018-10-21 12:08
 */
public class CountProxy implements Count {
    
    

    private CountImpl countImpl;

    /**
     * 覆盖默认构造器
     */
    public CountProxy(CountImpl countImpl) {
    
    
        this.countImpl = countImpl;
    }

    @Override
    public void queryCount() {
    
    
        System.out.println("==查询账户开始");
        // 调用真正的查询账户方法
        countImpl.queryCount();
        System.out.println("==查询账户结束");
    }

    @Override
    public void updateCount() {
    
    
        System.out.println("==更新账户开始");
        // 调用真正的修改账户操作
        countImpl.updateCount();
        System.out.println("==更新账户结束");
    }
}

测试类:

@Test
public void test1() {
    
    
    // 静态代理
    CountImpl countImpl = new CountImpl();
    CountProxy countProxy = new CountProxy(countImpl);
    countProxy.updateCount();
    System.out.println("\n*******\n");
    countProxy.queryCount();
}

执行结果:

==更新账户开始
==更新账户
==更新账户结束

*******

==查询账户开始
==查询账户
==查询账户结束

该中模式比较简单,不再做过多的分析。就是硬编码,采用代理模式,手动实现一个代理类。

硬编码的缺点是什么?就是一旦代码需要变更,需要重新编译后才能生效。

2. JDK动态代理

JDK动态代理所用到的代理类在程序调用到代理类对象时才由JVM真正创建,JVM根据传进来的业务实现类对象以及方法名 ,动态地创建了一个代理类的class文件并被字节码引擎执行,然后通过该代理类对象进行方法调用。

我们需要做的,只需指定代理类的预处理、调用后操作即可。JDK的动态代理需要实现InvocationHandler接口,并重写invoke()方法。并且被代理的类必须要实现一个接口,接口类型不限制,来看具体的例子:

即将被代理的接口和类:

package com.lyc.cn.v2.day04.proxy.jdk;

public interface JDKAnimal {
    
    
    void sayHello();
}
package com.lyc.cn.v2.day04.proxy.jdk;

public class JDKDog implements JDKAnimal {
    
    
    @Override
    public void sayHello() {
    
    
        System.out.println("我是一只猫");
    }
}

自定义InvocationHandler:

package com.lyc.cn.v2.day04.proxy.jdk;

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

public class MyInvocationHandler implements InvocationHandler {
    
    

    // 目标对象
    private Object target;

    /**
     * 构造方法
     * @param target 目标对象
     */
    public MyInvocationHandler(Object target) {
    
    
        super();
        this.target = target;
    }

    /**
     * @param proxy  JDK动态生成的最终代理对象
     * @param method 调用真实对象的某个方法的Method对象
     * @param args   调用真实对象某个方法时接受的参数
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
        System.out.println("==代理方法开始执行");
        Object invoke = method.invoke(target, args);
        System.out.println("==代理方法结束执行");
        return invoke;
    }

    /**
     * 获取目标对象的代理对象
     * @return 代理对象
     */
    public Object getProxy() {
    
    
        /**
         * 参数:
         * 1、获取当前线程的类加载
         * 2、获取接口
         * 3、当前对象
         */
        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                target.getClass().getInterfaces(),
                this);
    }

}

测试类:

public class MainClass {
    
    
    public static void main(String[] args) {
    
    
        // JDK动态代理
        MyInvocationHandler handler = new MyInvocationHandler(new JDKDog());
        JDKAnimal proxy = (JDKAnimal) handler.getProxy();
        proxy.sayHello();
    }
  }  

执行结果:

==代理方法开始执行
我是一只猫
==代理方法结束执行

2.1 JDK动态代理原理分析

利用Java的反射技术(Java Reflection),在运行时创建一个实现某些给定接口的新类(也称“动态代理类”)及其实例(对象),代理的是接口(Interfaces),不是类(Class),也不是抽象类。在运行时才知道具体的实现,spring aop就是此原理。

在MyInvocationHandler类中,通过实现InvocationHandler并重写invoke方法,实现了对JDKDog类的动态代理。但是这里大家一定会有一个疑问,在测试类中通过JDKAnimal proxy = (JDKAnimal) handler.getProxy();获取了代理类的实例,但是当调用proxy.sayHello();方法时,却会调用MyInvocationHandler的invoke方法,要解开这个谜题,就需要了解一下代理类是如何生成、生成的代理类是什么样子的、是如何被实例化的。

2.1.1 Proxy.newProxyInstance方法简析

打开MyInvocationHandler类的getProxy方法,查看Proxy.newProxyInstance方法源码:

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.
     */
     //[0] 查找或生成一个代理类
    Class<?> cl = getProxyClass0(loader, intfs);

    /*
     * Invoke its constructor with the designated invocation handler.
     */
    try {
    
    
        if (sm != null) {
    
    
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }
        //[2]构造器
        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;
                }
            });
        }
        // [3]反射创建生成代理类的实例
        return cons.newInstance(new Object[]{
    
    h});
    } catch (IllegalAccessException|InstantiationException e) {
    
    
        throw new InternalError(e.toString(), e);
    } catch (InvocationTargetException e) {
    
    
        Throwable t = e.getCause();
        if (t instanceof RuntimeException) {
    
    
            throw (RuntimeException) t;
        } else {
    
    
            throw new InternalError(t.toString(), t);
        }
    } catch (NoSuchMethodException e) {
    
    
        throw new InternalError(e.toString(), e);
    }
}

[1].调用getProxyClass0()方法获得代理类
[2].构造器
[3].利用反射,创建代理类的实例

2.2.1.1 getProxyClass0()

实现动态代理的核心方法,动态代理的思路便是生成一个新类,调用getProxyClass0()便成为了生成新类,getProxyClass0()方法直接取proxyClassCache缓存中的代理类,proxyClassCache是一个代理类的缓存变量,如果这个缓存里有这个代理类,就直接返回代理类,如果没有,就会通过ProxyClassFactory创建代理对象。

    private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
    
    

        // 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);
    }

2.2.1.1 ProxyClassFactory

ProxyClassFactory是Proxy里的一个内部类,用来生成代理类,apply()方法的最后会调用ProxyGenerator.generateProxyClass()方法来完成生成字节码的操作:

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

            /*
             * Generate the specified proxy class.
             */
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
           
        }
    }

ProxyGenerator.generateProxyClass()可以生成一个字节码文件,那么通过查看该字节码文件,我们就能明白原理了!

2.1.2 查看代理的字节码文件

默认情况下,jvm是不会保存这个字节码文件的,是个临时文件,生成后会删除掉,我们可以通过系统参数,让其保存。

JDK1.8下:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

不同版本JDK可能参数略有不同,比如:System.getProperties().put(“sun.misc.ProxyGenerator.saveGeneratedFiles”,“true”);

    public static void main(String[] args) {
    
    
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
        // JDK动态代理
        MyInvocationHandler handler = new MyInvocationHandler(new JDKDog());
        JDKAnimal proxy = (JDKAnimal) handler.getProxy();
        proxy.sayHello();
    }        

默认路径是工程目录/com\sun\proxy\$Proxy0.class,JD-GUI工具 可以实现对.class文件的反编译,该软件支持windows、linux、macOS。用JD-GUI打开生成的$Proxy.class文件

package com.lyc.cn.v2.day04.proxy.jdk;

/**
 * 反编译代理.class文件
 * @author: LiYanChao
 * @create: 2018-11-02 15:01
 */

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

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

    public $Proxy(InvocationHandler paramInvocationHandler) {
    
    
        super(paramInvocationHandler);
    }

    public final boolean equals(Object paramObject) {
    
    
        try {
    
    
            return ((Boolean) this.h.invoke(this, m1, new Object[]{
    
    paramObject})).booleanValue();
        } catch (Error | RuntimeException localError) {
    
    
            throw localError;
        } catch (Throwable localThrowable) {
    
    
            throw new UndeclaredThrowableException(localThrowable);
        }
    }

    /**
     * 代理类中实现了sayHello接口
     */
    public final void sayHello() {
    
    
        try {
    
    
            this.h.invoke(this, m3, null);
            return;
        } catch (Error | RuntimeException localError) {
    
    
            throw localError;
        } catch (Throwable localThrowable) {
    
    
            throw new UndeclaredThrowableException(localThrowable);
        }
    }

    public final String toString() {
    
    
        try {
    
    
            return (String) this.h.invoke(this, m2, null);
        } catch (Error | RuntimeException localError) {
    
    
            throw localError;
        } catch (Throwable localThrowable) {
    
    
            throw new UndeclaredThrowableException(localThrowable);
        }
    }

    public final int hashCode() {
    
    
        try {
    
    
            return ((Integer) this.h.invoke(this, m0, null)).intValue();
        } catch (Error | RuntimeException localError) {
    
    
            throw localError;
        } catch (Throwable localThrowable) {
    
    
            throw new UndeclaredThrowableException(localThrowable);
        }
    }

    static {
    
    
        try {
    
    
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{
    
    Class.forName("java.lang.Object")});
            m3 = Class.forName("com.lyc.cn.v2.day04.proxy.jdk.JDKAnimal").getMethod("sayHello", new Class[0]);
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            // return 会报错,注释掉即可。
            //return;
        } catch (NoSuchMethodException localNoSuchMethodException) {
    
    
            throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
        } catch (ClassNotFoundException localClassNotFoundException) {
    
    
            throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
        }
    }
}

代理类$Proxy被声明为final类,继承Proxy类并实现了JDKAnimal接口,重写了equals、toString、hashCode等方法,当然最重要的还是实现了JDKAnimal接口中的sayHello方法,并通过静态代码块拿到了sayHello方法的信息,看到sayHello方法体:
在这里插入图片描述
看到这里相信大家对前面提到的疑问已经恍然大悟了,这个代理对象调用了MyInvocationHandler的invoke()方法。

new $Proxy(handler)构造函数接受的就是我们自定义的MyInvocationHandler,所以当代码运行到$Proxy的sayHello方法时,this.h.invoke(this, m3, null); this.h就是MyInvocationHandler的实例,所以自然就会调用到invoke方法了,因为JDKAnimal proxy = (JDKAnimal) handler.getProxy();获取到的是代理类的实例,而不是JDKAnimal的实例


并且从字节码文件能看到,实现了JDKAnimal接口,入下图所示:
在这里插入图片描述
因此,动态代理的前提是目标类必须实现一个接口!

2.1.2.1 指定代理的字节码文件路径

原文这个例子是为了指定代理的字节码文件路径,替换DEFAULT_FILE_PATH 变量的路径为你期望的路径即可。

原理是手动写个测试方法,调用ProxyGenerator.generateProxyClass(),该方法是生成代理字节码文件的核心方法,该方法接收一个路径,即字节码的路径:

package com.lyc.cn.v2.day04.proxy.jdk;

import org.junit.Test;
import sun.misc.ProxyGenerator;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * @author: LiYanChao
 * @create: 2018-11-02 14:11
 */
public class GenJdkProxyClass {
    
    

    /**
     * 生成代理类的名称
     */
    private static String DEFAULT_CLASS_NAME = "$Proxy";

    /**
     * 默认生成的文件全路径
     */
    private static String DEFAULT_FILE_PATH = "/Users/liyanchao/Desktop/" + DEFAULT_CLASS_NAME + ".class";


    /**
     * 使用ProxyGenerator生成代理类.class文件
     * @param path 文件路径
     */
    public static void genProxyClass(String path) {
    
    

        byte[] classFile = ProxyGenerator.generateProxyClass(DEFAULT_CLASS_NAME, new Class[]{
    
    JDKAnimal.class});
        FileOutputStream fos = null;
        try {
    
    
            fos = new FileOutputStream(path);
            fos.write(classFile);
            fos.flush();
        } catch (FileNotFoundException e) {
    
    
            e.printStackTrace();
        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            if (fos != null) {
    
    
                try {
    
    
                    fos.close();
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void TestGenProxyClass() {
    
    
        GenJdkProxyClass.genProxyClass(DEFAULT_FILE_PATH);
    }
}

2.2 CGLIB动态代理

CGLIB是针对类来实现代理的,原理是对指定的业务类生成一个子类,并覆盖其中业务方法实现代理。因为采用的是继承,所以不能对final修饰的类进行代理。 在使用的时候需要引入cglib和asm的jar包,并实现MethodInterceptor接口。

Cglib动态代理所需要的jar,一共有两个(asm-3.3.1和cglib-2.2.2)

被代理类,没有实现任何接口:

package com.lyc.cn.v2.day04.proxy.cglib;

public class CglibCat {
    
    
    public void sayHello() {
    
    
        System.out.println("我是一只猫。。。");
    }
}

自定义MethodInterceptor:

package com.lyc.cn.v2.day04.proxy.cglib;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class MyCglibProxy implements MethodInterceptor {
    
    

    private Enhancer enhancer = new Enhancer();

    // 这里的目标类型为Object,则可以接受任意一种参数作为被代理类,实现了动态代理
    public Object getInstance(Class clazz) {
    
    
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        // 返回代理对象
        return enhancer.create();
    }

    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    
    
        System.out.println("==代理方法开始执行");
        Object result = methodProxy.invokeSuper(proxy, args);
        System.out.println("==代理方法结束执行");
        return result;
    }

}

测试类:

public class MainClass {
    
    

    public static void main(String[] args) {
    
    

        // CGLIB动态代理
        CglibCat cat = (CglibCat) new MyCglibProxy().getInstance(CglibCat.class);
        cat.sayHello();
    }
 }

CGLib采用了非常底层的字节码技术( ASM),其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。

《cglib原理解析》

3. 总结

总而言之,动态代理技术是AOP的基础也是核心,大家一定要先把JDK、CGLIB动态代理搞明白之后,再去看AOP的源码,才能达到事半功倍的效果。


参考:
《静态代理模式和JDK、CGLIB动态代理》

猜你喜欢

转载自blog.csdn.net/m0_45406092/article/details/115221380