今天看啥  ›  专栏  ›  JayminAuthor

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

JayminAuthor  · 掘金  ·  · 2021-02-15 20:51
阅读 50

[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<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,来规避掉一些平时常见的错误。




原文地址:访问原文地址
快照地址: 访问文章快照