美文网首页Spring
[Spring]Spring AOP代理原理-JDK动态代理和C

[Spring]Spring AOP代理原理-JDK动态代理和C

作者: AbstractCulture | 来源:发表于2021-02-15 20:48 被阅读0次

    代理模式

    代理模式是属于结构型的设计模式,指客户端的请求到达真正的对象之前,做一些额外的操作。
    举个例子,

    1. 你需要找房子,那么通过向中介支付金额就可以找到心宜的房子,而中介需要跟房东商量好差价,衔接租户与房东,此时的中介就是代理.
    2. 过年需要回家,你不会操作12306的app,但是美团和支付宝出台了"帮你抢票"的功能,你无需操作12306,只需要向美团和支付宝支付金额,让平台帮你去抢票即可,这其实也是一种代理的体现.
    代理模式

    静态代理模式

    下面我们通过代码来实现静态代理.
    需求:

    1. 租客手里有1000块,需要租房.
    2. 中介可以帮租客租房,但是需要收取100块的中介费.
    3. 房东手里有房子,但是找不到真正的租客.
    • RentSubject

    建立一个租房的接口,用户通过操作该接口,即可进行支付获取房子的钥匙.

    package com.tea.modules.design.proxy;
    
    import java.math.BigDecimal;
    
    /**
     * @author jaymin<br>
     * 租房子的主题.<br>
     * 对于租客来说,只需要付钱即可.<br>
     * 2021/2/14 18:40
     */
    public interface RentSubject {
    
        /**
         * 支付租金寻找房子.
         * @param rent 租金
         * @return
         */
        String findHouse(BigDecimal rent);
    }
    
    • LandlordProxied

    房东对象,房东只管收钱和交接钥匙.

    package com.tea.modules.design.proxy;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.math.BigDecimal;
    
    /**
     * @author jaymin.<br>
     * 房东,只负责收钱交房子即可.不关心谁进行支付.<br>
     * 2021/2/14 18:48
     */
    @Slf4j
    public class LandlordProxied implements RentSubject {
    
        @Override
        public String findHouse(BigDecimal rent) {
            log.info("房东收到了:{}租金,交出钥匙.", rent);
            return "Lock";
        }
    }
    
    • RentAgencyProxy

    中介,中介负责从租客手里收钱,收取中介费后,向房东支付租金和获取钥匙,然后交给租客.

    package com.tea.modules.design.proxy.statics;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.math.BigDecimal;
    
    /**
     * @author jaymin.<br>
     * 房租中介机构,负责帮租客找房子.<br>
     * 同时,找到房子后,中介需要向房东支付租金.<br>
     * 2021/2/14 18:45
     */
    @Slf4j
    public class RentAgencyProxy implements RentSubject {
    
        private RentSubject rentSubject;
    
        public RentAgencyProxy(RentSubject rentSubject) {
            this.rentSubject = rentSubject;
        }
    
        @Override
        public String findHouse(BigDecimal rent) {
            BigDecimal actualRent = beforeRealSubject(rent);
            return this.rentSubject.findHouse(actualRent);
        }
    
        private BigDecimal beforeRealSubject(BigDecimal rent) {
            log.info("中介收取当前租客租金:{}", rent);
            // 中介赚取中间差价后,支付给房东
            BigDecimal actualRent = rent.subtract(BigDecimal.valueOf(100));
            return actualRent;
        }
    }
    
    
    • TenantClient

    租客,租客支付支付租金,获取钥匙.

    package com.tea.modules.design.proxy;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.math.BigDecimal;
    
    /**
     * @author jaymin.<br>
     * 租客,目前租客想找房子,手里有1000块钱.<br>
     * 中介找到了900块的房子,将订单接收了下来,收取100块的中介费.<br>
     * 房东此时有空置的房子,900块。<br>
     * 2021/2/14 18:53
     */
    @Slf4j
    public class TenantClient {
    
        public static void main(String[] args) {
            RentSubject rentSubject = new RentAgencyProxy(new LandlordProxied());
            String house = rentSubject.findHouse(BigDecimal.valueOf(1000));
            log.info("租客拿到了房门钥匙:" + house);
        }
    }
    
    • Result
    19:30:41.553 [main] INFO com.tea.modules.design.proxy.RentAgencyProxy - 中介收取当前租客租金:1000
    19:30:41.558 [main] INFO com.tea.modules.design.proxy.LandlordProxied - 房东收到了:900租金,交出钥匙.
    19:30:41.558 [main] INFO com.tea.modules.design.proxy.TenantClient - 租客拿到了房门钥匙:Lock
    
    静态代理存在的缺陷

    此时对于中介来说,它的目的已经很明确了,即赚取差价.中介其实并不关心真正需要做的是什么业务,无论是租房、买房、买家具...只需要从客户手里拿到钱,然后找到真正的服务商进行交付即可。

    那么此时对于Proxy类来说,无论最终的RealSubject中的逻辑是什么,它只负责代理(即经过代理类的金额会自动扣除100).想象一下此时如果有一个新的业务市场,也是同样的赚取差价,那么通过静态代理的方式仍然需要重新封装一套逻辑。如果这样的类越来越多,而代理逻辑都是一致的,那么最终项目的类会膨胀地非常快,同时加剧了维护成本.
    此时对于代理来说,代理逻辑是确定的,被代理的类(targetObject)可以是未知的,如何做到将代理逻辑与原始类逻辑分离?
    这个时候,我们就需要动态代理.

    动态代理

    动态代理技术在Spring AOP中分为两种:

    • 基于JDK原生的动态代理.

    提供一种在运行时创建一个实现了一组接口的新类.由于Java是不支持实例化接口的,因此JDK会在运行期间生成一个代理类对给定的接口进行实现,在调用该代理类接口的时候,将实现逻辑转发到调用处理器中(Invocation handler).
    使用JDK进行动态代理的类必须实现接口(所有的代理类都是java.lang.reflect.Proxy的子类,类名以$Proxy开始).

    • 基于CGLIB的动态代理.

    CGLIB(Code Generation Library)是基于ASM(对Java字节码进行操作的框架)的类库.在Spring AOP中,如果被代理类(targetObject)没有实现接口,即无法通过JDK的动态代理生成代理类,那么就会选择CGLIB来进行代理.
    CGLIB动态代理的原理:创建一个targetObject的子类,覆盖掉需要父类的方法,在覆盖的方法中对功能进行增强。
    注意,由于是采用继承覆盖的方式,所以由final方法修饰的类无法使用CGLIB进行代理.

    1. 使用JDK动态代理实现代理模式
    • IntermediaryInvocationHandler
    package com.tea.modules.design.proxy.dynamic.jdkproxy;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.math.BigDecimal;
    
    /**
     * @author jaymin.<br>
     * JDK动态代理实现中介赚取差价的逻辑.<br>
     * 此处封装切面逻辑,相对于AOP中的Aspect.<br>
     * 2021/2/14 19:56
     */
    @Slf4j
    public class IntermediaryInvocationHandler implements InvocationHandler {
    
        private Object targetObject;
    
        public IntermediaryInvocationHandler(Object targetObject) {
            this.targetObject = targetObject;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            BigDecimal actualPrice = beforeRealSubject(((BigDecimal) args[0]));
            args[0] = actualPrice;
            Object result = method.invoke(targetObject, args);
            return result;
        }
    
        private BigDecimal beforeRealSubject(BigDecimal money) {
            log.info("中介收取费用:{}", money);
            // 中介赚取中间差价后,支付给服务商
            BigDecimal actualPrice = money.subtract(BigDecimal.valueOf(100));
            return actualPrice;
        }
    }
    

    使用JDK的动态代理需要实现InvocationHandler接口,然后使用java.lang.reflect.Proxy#newProxyInstance来生成代理类.

    • DynamicProxyDemo
    package com.tea.modules.design.proxy.dynamic;
    
    import com.tea.modules.design.proxy.dynamic.jdkproxy.IntermediaryInvocationHandler;
    import com.tea.modules.design.proxy.statics.LandlordProxied;
    import com.tea.modules.design.proxy.statics.RentSubject;
    import net.sf.cglib.proxy.MethodInterceptor;
    
    import java.lang.reflect.InvocationHandler;
    import java.math.BigDecimal;
    
    /**
     * @author jaymin.<br>
     * 动态代理:        <br>
     * 1. JDK的动态代理,需要被代理类实现接口.<br>
     * 2. CGLIB动态代理.                 <br>
     * 2021/2/14 20:11
     */
    public class DynamicProxyDemo {
    
        public static void main(String[] args) {
            jdkDynamicProxy();
        }
    
        /**
         * JDK的动态代理.<br>
         * 在这里,我们只需要提供一个切面逻辑的IntermediaryInvocationHandler即可完成代理逻辑的复用.<br>
         * 更难得的是,只要类实现了任意接口,并且方法参数中的第一个参数为金额,那么中介就可以无缝进行赚取差价了,而不是通过创建类的形式.<br>
         * 形象的来说,中介的逻辑在运行时被"织入"了.<br>
         * 通过debug可以看到,被代理的对象引用前缀为:$Proxy <br>
         */
        private static void jdkDynamicProxy() {
            RentSubject targetObject = new LandlordProxied();
            InvocationHandler handler = new IntermediaryInvocationHandler(targetObject);
            // 获取当前被代理类的类加载器
            ClassLoader classLoader = targetObject.getClass().getClassLoader();
            Class<?>[] interfaces = targetObject.getClass().getInterfaces();
            RentSubject rentSubject = (RentSubject) Proxy.newProxyInstance(classLoader, interfaces, handler);
            System.out.println("当前对象是否为代理类:" + Proxy.isProxyClass(rentSubject.getClass()));
            rentSubject.findHouse(BigDecimal.valueOf(1000));
        }
    }
    
    • Result
    当前对象是否为代理类:true
    16:42:23.870 [main] INFO com.tea.modules.design.proxy.dynamic.jdkproxy.IntermediaryInvocationHandler - 中介收取费用:1000
    16:42:23.870 [main] INFO com.tea.modules.design.proxy.statics.LandlordProxied - 房东收到了:900租金,交出钥匙.
    

    可以看到,将实现了接口的LandlordProxied作为targetObject,通过Proxy.newProxyInstance创建出代理对象,就会在其执行findHouse时回调IntermediaryInvocationHandler中的invoke方法.

    2. 使用CGLIB实现代理模式

    CGLIB并非JDK原生的包,所以我们需要导入CGLIB的依赖.

    • pom.xml
        <!-- https://mvnrepository.com/artifact/cglib/cglib -->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.2.9</version>
        </dependency>
    
    • 创建一个没有实现接口的业务类
    package com.tea.modules.design.proxy.dynamic.cglibproxy;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.math.BigDecimal;
    
    /**
     * @author jaymin.<br>
     * 对CGLIB测试,是否能增强没有实现接口的类.<br>
     * 此类为普通的房东,没有实现任何接口,纯收钱交房.<br>
     * 2021/2/14 21:00
     */
    @Slf4j
    public class NormalLandlord {
    
        public String findHouse(BigDecimal rent) {
            log.info("房东收到了:{}租金,交出钥匙.", rent);
            return "Lock";
        }
    
    }
    
    • IntermediaryMethInterceptor
    package com.tea.modules.design.proxy.dynamic.cglibproxy;
    
    import lombok.extern.slf4j.Slf4j;
    import net.sf.cglib.proxy.MethodInterceptor;
    import net.sf.cglib.proxy.MethodProxy;
    
    import java.lang.reflect.Method;
    import java.math.BigDecimal;
    
    /**
     * @author jaymin.<br>
     * 基于CGLIB实现动态代理.<br>
     * 2021/2/14 20:46
     */
    @Slf4j
    public class IntermediaryMethInterceptor implements MethodInterceptor {
    
        @Override
        public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            BigDecimal actualPrice = beforeRealSubject(((BigDecimal) args[0]));
            args[0] = actualPrice;
            Object result = methodProxy.invokeSuper(object, args);
            return result;
        }
    
        private BigDecimal beforeRealSubject(BigDecimal money) {
            log.info("中介收取费用:{}", money);
            // 中介赚取中间差价后,支付给服务商
            BigDecimal actualPrice = money.subtract(BigDecimal.valueOf(100));
            return actualPrice;
        }
    }
    

    CGLIB中创建代理类需要先写好一个切面类,该类需要实现MethodInterceptor.在intercept方法中对业务进行增强,调用目标类的方法为methodProxy.invokeSuper.

    • DynamicProxyDemo
    package com.tea.modules.design.proxy.dynamic;
    
    import com.tea.modules.design.proxy.dynamic.cglibproxy.IntermediaryMethInterceptor;
    import com.tea.modules.design.proxy.dynamic.cglibproxy.NormalLandlord;
    import com.tea.modules.design.proxy.statics.LandlordProxied;
    import com.tea.modules.design.proxy.statics.RentSubject;
    import net.sf.cglib.proxy.Enhancer;
    import net.sf.cglib.proxy.MethodInterceptor;
    
    import java.lang.reflect.InvocationHandler;
    import java.math.BigDecimal;
    
    /**
     * @author jaymin.<br>
     * 动态代理:        <br>
     * 1. JDK的动态代理,需要被代理类实现接口.<br>
     * 2. CGLIB动态代理.                 <br>
     * 2021/2/14 20:11
     */
    public class DynamicProxyDemo {
    
        public static void main(String[] args) {
            cglibDynamicProxy();
        }
    
    
        /**
         * CGLIB.创建一个目标类的子类,重写其中的方法.最终逻辑委托到MethodInterceptor中 
         */
        private static void cglibDynamicProxy(){
            NormalLandlord targetObject = new NormalLandlord();
            MethodInterceptor methInterceptor = new IntermediaryMethInterceptor();
            NormalLandlord proxy = (NormalLandlord) Enhancer.create(targetObject.getClass(), methInterceptor);
            proxy.findHouse(BigDecimal.valueOf(1000));
        }
    }
    

    关键的代码其实就一行:Enhancer.create(targetObject.getClass(), methInterceptor),其中methInterceptor则是我们的切面类.

    • Result
    16:54:46.102 [main] INFO com.tea.modules.design.proxy.dynamic.cglibproxy.IntermediaryMethInterceptor - 中介收取费用:1000
    16:54:46.118 [main] INFO com.tea.modules.design.proxy.dynamic.cglibproxy.NormalLandlord - 房东收到了:900租金,交出钥匙.
    
    小结
    • JDK动态代理要求被代理类实现接口.切面类需要实现InvocationHandler.
    • CGLIB采用继承+方法覆盖的形式实现切面,在重写方法中将逻辑委托给MethodInterceptor#intercept.
    • CGLIB对代理类基本没有限制,但是需要注意被代理的类不可以被final修饰符修饰.因为Java无法重写final类.

    深入浅出动态代理

    1.JDK动态代理到底是怎么实现的?

    很多朋友都会有疑惑,这些动态代理的类看不见摸不着,虽然可以看到效果,但是底层到底是怎么做的,为什么要求实现接口呢?
    OK,下面我们从JDK的动态代理入手,来看看代理类到底长啥样.

    • 从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.
             * 查找或生成指定的代理类
             */
            Class<?> cl = getProxyClass0(loader, intfs);
            // 省略若干代码
        }
    

    第一步,尝试获取代理类,该代理类可能会被缓存,如果没有缓存,那么进行生成逻辑.

    • java.lang.reflect.Proxy#getProxyClass0
        private static Class<?> getProxyClass0(ClassLoader loader,
                                               Class<?>... interfaces) {
            if (interfaces.length > 65535) {
                throw new IllegalArgumentException("interface limit exceeded");
            }
    
            // 如果代理类已经通过类加载器对给定的接口进行实现了,那么从缓存中返回其副本
            // 否则,它将通过ProxyClassFactory创建代理类
            return proxyClassCache.get(loader, interfaces);
        }
    
    • java.lang.reflect.Proxy.ProxyClassFactory#apply
            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) {
                    /*
                     * A ClassFormatError here means that (barring bugs in the
                     * proxy class generation code) there was some other
                     * invalid aspect of the arguments supplied to the proxy
                     * class creation (such as virtual machine limitations
                     * exceeded).
                     */
                    throw new IllegalArgumentException(e.toString());
                }
            }
        }
    

    ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);,这段代码即为生成动态代理类的关键,执行完后会返回该描述该代理类的字节码数组.随后程序读取该字节码数组,将其转化为运行时的数据结构-Class对象,作为一个常规类使用.

    • sun.misc.ProxyGenerator#generateProxyClass(java.lang.String, java.lang.Class<?>[], int)
        public static byte[] generateProxyClass(final String var0, Class<?>[] var1, int var2) {
            ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);
            final byte[] var4 = var3.generateClassFile();
            // 如果声明了需要持久化代理类,则进行磁盘写入.
            if (saveGeneratedFiles) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        try {
                            int var1 = var0.lastIndexOf(46);
                            Path var2;
                            if (var1 > 0) {
                                Path var3 = Paths.get(var0.substring(0, var1).replace('.', File.separatorChar));
                                Files.createDirectories(var3);
                                var2 = var3.resolve(var0.substring(var1 + 1, var0.length()) + ".class");
                            } else {
                                var2 = Paths.get(var0 + ".class");
                            }
    
                            Files.write(var2, var4, new OpenOption[0]);
                            return null;
                        } catch (IOException var4x) {
                            throw new InternalError("I/O exception saving generated file: " + var4x);
                        }
                    }
                });
            }
    
            return var4;
        }
    

    这里我们找到了一个关键的判断条件-saveGeneratedFiles,即是否需要将代理类进行持久化.

    private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));
    

    这里会判断sun.misc.ProxyGenerator.saveGeneratedFiles变量是否为true.默认为false.

    • 在main方法启动时将saveGeneratedFiles设置为true.
    public class DynamicProxyDemo {
    
        public static void main(String[] args) {
            System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
            jdkDynamicProxy();
        }
    }
    

    为了定位生成的类,我们在Files.write(var2, var4, new OpenOption[0]);中断点一下查看路径.


    path
    • 生成的代理类
      $proxy
    package com.sun.proxy;
    
    import com.tea.modules.design.proxy.statics.RentSubject;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.lang.reflect.UndeclaredThrowableException;
    import java.math.BigDecimal;
    
    public final class $Proxy0 extends Proxy implements RentSubject {
        // 省略若干代码
        public final String findHouse(BigDecimal var1) throws  {
            try {
                return (String)super.h.invoke(this, m3, new Object[]{var1});
            } catch (RuntimeException | Error var3) {
                throw var3;
            } catch (Throwable var4) {
                throw new UndeclaredThrowableException(var4);
            }
        }
        // 省略若干代码
    }
    

    我们将目光聚焦在findHouse上,可以看到,调用代理的findHouse会去执行super.h.invoke,其中h即为Proxy类中的protected InvocationHandler h;,那么此时也印证了我们的想法是对的。
    同时,你也应该注意到,代理类继承自Proxy并且实现了给定的RentSubject接口.
    有理有据,此时你应该对JDK动态代理有了更深的理解了.

    这里贴一下从知乎上看到的关于动态代理更形象的解释:

    Java 动态代理作用是什么?

    2. 为什么有时候会产生代理失效?

    下面给出一个例子来演示失效的场景.
    假设此时有一个日志记录的注解:@Log,在另一个类注入了SimpleServiceImpl,并且调用了其中的simpleServiceImpl.foo(),那么此时的bar()方法是不会执行切面逻辑的。

    public class SimpleServiceImpl implements SimpleService {
    
        public void foo() {
            // 通过foo()调用了方法内的bar()
            this.bar();
        }
        
        @Log
        public void bar() {
            // some logic...
        }
    }
    

    原因这里简单说一下:Spring对SimpleServiceImpl进行了代理,但是@Log注解仅标注在bar()上,那么需要通过SimpleServiceImpl.bar()这样的形式才可以进入代理类的逻辑中,因此此时持有的是代理类的引用.
    换个角度思考一下,SimpleServiceImpl#foo将逻辑委托到了target类进行执行,此时在target类中调用了this.bar(),this指向的引用是target类本身,而不是代理类的引用,因此是无法被代理类进行环绕的.

    如果还不理解,可以访问以下文章加深理解:

    Spring官网对AOP代理的解释

    一个Spring AOP的坑!很多人都犯过!

    3. 既然CGLIB更加自由(不用实现接口),为什么Spring AOP还要内置JDK动态代理?

    JDK的动态代理是Java官方推出的动态代理模式,官方对此进行维护和优化,无需引入第三方依赖.
    CGLIB属于第三方框架,随着JDK版本的升级,项目也许需要更换CGLIB来兼容最新的JDK.
    性能上,随着JDK版本的更新,已经跟CGLIB差别不大.

    4. 动态代理会对程序有性能影响么?

    如果使用动态代理生成了大量的类,可能会引发方法区的内存溢出.
    JDK8开始,JVM去除了永久代,取而代之的是元空间.在默认设置下,由框架生成的动态代理类难以使JVM产生方法区内存溢出的异常.
    但是,JDK8之前的版本,会产生动态代理类大量填充方法区引起内存溢出的问题.
    相关的例子可以查看周志明的《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》
    其中,关于CGLIB,可以查看相关文章:
    CGLib: The Missing Manual

    总结

    OK,看到这里,相信你对动态代理技术已经有了一定的理解了,其实我们平时编程用到动态代理的场景比较少,大部分都是充斥着业务代码。但是学习框架底层的原理,会让你更好地理解Spring AOP,来规避掉一些平时常见的错误。

    相关文章

      网友评论

        本文标题:[Spring]Spring AOP代理原理-JDK动态代理和C

        本文链接:https://www.haomeiwen.com/subject/qxlmxltx.html