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

代理模式

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

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

静态代理模式

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

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

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

package com.tea.modules.design.proxy;

import java.math.BigDecimal;

/**
 * @author jaymin
* 租房子的主题.
* 对于租客来说,只需要付钱即可.
* 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.
* 房东,只负责收钱交房子即可.不关心谁进行支付.
* 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.
* 房租中介机构,负责帮租客找房子.
* 同时,找到房子后,中介需要向房东支付租金.
* 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.
* 租客,目前租客想找房子,手里有1000块钱.
* 中介找到了900块的房子,将订单接收了下来,收取100块的中介费.
* 房东此时有空置的房子,900块。
* 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.
* JDK动态代理实现中介赚取差价的逻辑.
* 此处封装切面逻辑,相对于AOP中的Aspect.
* 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.
* 动态代理:
* 1. JDK的动态代理,需要被代理类实现接口.
* 2. CGLIB动态代理.
* 2021/2/14 20:11 */ public class DynamicProxyDemo { public static void main(String[] args) { jdkDynamicProxy(); } /** * JDK的动态代理.
* 在这里,我们只需要提供一个切面逻辑的IntermediaryInvocationHandler即可完成代理逻辑的复用.
* 更难得的是,只要类实现了任意接口,并且方法参数中的第一个参数为金额,那么中介就可以无缝进行赚取差价了,而不是通过创建类的形式.
* 形象的来说,中介的逻辑在运行时被"织入"了.
* 通过debug可以看到,被代理的对象引用前缀为:$Proxy
*/ 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
    
    
        cglib
        cglib
        3.2.9
    
  • 创建一个没有实现接口的业务类
package com.tea.modules.design.proxy.dynamic.cglibproxy;

import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;

/**
 * @author jaymin.
* 对CGLIB测试,是否能增强没有实现接口的类.
* 此类为普通的房东,没有实现任何接口,纯收钱交房.
* 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.
* 基于CGLIB实现动态代理.
* 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.
* 动态代理:
* 1. JDK的动态代理,需要被代理类实现接口.
* 2. CGLIB动态代理.
* 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() {
                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动态代理和CGLIB代理)