代理模式:JDK动态代理和静态代理回顾

Posted ·梅花十三

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了代理模式:JDK动态代理和静态代理回顾相关的知识,希望对你有一定的参考价值。

背景:Spring主要有两大思想:IoC、AOP。对于IoC依赖注入不多说了,对于Spring的核心AOP来说,我们需要了解其底层的实现原理:java的动态代理机制。

 

本篇随笔就是对java的动态机制进行一个回顾。

代理模式的理解

类型:代理模式是GoF23种设计模式之一。属于结构型设计模式。

特点:对于客户端程序来说,使用代理对象时就像在使用目标对象一样。

意义:使用代理模式可以在不修改别代理对象代码的基础上,通过扩展代理类,进行一些功能的附加与增强

生活场景:

拍电影时,演员找替身演员,这就是一个代理模式。由替身演员——》去代理演员——》完成表演

思考:演员为什么要找替身呢?——为什么要使用代理模式呢?
    原因 ①:怕自己受伤,找个替身。(保护自己)
    原因 ②:自己完成不来这种高难度的动作,替身演员可以完成。(功能增强)

在java程序中的代理模式的作用:

  • 当一个对象需要 受到保护 时,可以考虑使用代理对象去完成某个行为
  • 需要给某个对象的功能进行 功能增强 的时候,可以考虑找一个代理进行增强。
  • A对象和B对象 无法直接交互 时,也可以使用代理模式来解决。(我是律师,我的当事人不方便和你对话。)

业务场景:

系统中有A,B,C三个模块,使用这些模块的前提是需要用户登陆,这样会导致三个模块都要写一套一样的登陆代码,可以为A,B,C三个模块提供一个代理,在代理当中写一次登陆判断即可。

代理的逻辑是:请求来了之后,判断用户是否登陆了,如果已经登陆了,则执行对应的目标,如果没有登陆则跳转到登陆页面【在程序中,目标不但受到保护,并且代码也得到了复用】

代理模式中的角色:

  • 代理类(代理主题)
  • 目标类(真实主题)
  • 代理类和目标类的公共接口(抽象主题):客户端在使用代理类时就像在使用目标类,不被客户端所察觉,所以代理类和目标类要用共同的行为,也就是实现共同的接口。

简单理解:目标对象是演员,代理对象是替身演员,所谓的公共接口就是演员和替身演员应该具有相同的行为动作,  不让观众察觉到替身演员。这里的观众其实就是“客户端程序”。


 

代理模式的类图:

代理模式类型

代理模式在代码实现上,包括两种形式

  • 静态代理:在程序运行前就存在代理类的字节码文件,代理类和委托类的关系在运行前确定
  • 动态代理 :在程序运行时,通过反射获取被代理类的字节码内容用来创建代理类

代理模式场景

假设存在如下订单业务接口

1. OrderService 订单接口代码如下

// 订单接口
public interface OrderService 
    // 创建订单
    void create();

    // 修改订单
    void update();

    // 查询订单
    void detail();

2. 订单接口的实现类: OrderServiceImpl 

// 订单接口的实现类
public class OrderServiceImpl implements OrderService

    // 创建订单
    @Override
    public void create() 
        // 模拟业务调用时长消耗
        try 
            Thread.sleep(1125);
         catch (InterruptedException e) 
            throw new RuntimeException(e);
        
        System.out.println("创建订单成功。");
    

    // 修改订单
    @Override
    public void update() 
        // 模拟业务调用时长消耗
        try 
            Thread.sleep(984);
         catch (InterruptedException e) 
            throw new RuntimeException(e);
        
        System.out.println("修改订单成功。");
    

    // 查询订单
    @Override
    public void detail() 
        // 模拟业务调用时长消耗
        try 
            Thread.sleep(111);
         catch (InterruptedException e) 
            throw new RuntimeException(e);
        
        System.out.println("查询订单成功。");
    

3. 测试类如下

public class Test 
    public static void main(String[] args) 
        OrderService order = new OrderServiceImpl();
        order.create();
        order.update();
        order.detail();
    

问题思考:这个功能已经运行了一年多了,突然客户提出一个新需求:要统计所有业务接口中每一个业务方法的耗时。你会怎么处理?

解决方案一:硬编码

在每一个接口方法里都加入计算时长的代码

 1.如下图所示,在OrderServiceImpl  里面的每个业务方法都加入耗时统计

public class OrderServiceImpl implements OrderService

    @Override
    public void create() 

        // ------ 加入统计计算
        long begin = System.currentTimeMillis();

        // 模拟业务调用时长消耗
        try 
            Thread.sleep(1125);
         catch (InterruptedException e) 
            throw new RuntimeException(e);
        
        System.out.println("创建订单成功。");

        // ------ 加入统计计算
        long end = System.currentTimeMillis();
        System.out.println("耗时:"+(end-begin));
    

    ....

2. 运行结果如下:

➳ 说明:这种方案虽然能够解决业务需求,但是存在两个缺点, 第一 违背OCP开闭原则  第二代码没有得到复用(相同的代码写了很多遍)

解决方案二:扩展业务类的子类

编写OrderServiceImpl 的子类,让子类继承业务类,对每个业务方法进行重写

1. 如下图所示,OrderServiceImpl  保持与原来不变,新增其子类 OrderServiceImplSub  

public class OrderServiceImplSub extends OrderServiceImpl

    @Override
    public void create() 
        long begin = System.currentTimeMillis();
        
        // 调用父类OrderServiceImpl的方法
        super.create();
        
        long end = System.currentTimeMillis();
        System.out.println("耗时:"+(end-begin));
    

    @Override
    public void update() 
        long begin = System.currentTimeMillis();
        
        super.update();
        
        long end = System.currentTimeMillis();
        System.out.println("耗时:"+(end-begin));
    

    @Override
    public void detail() 
        long begin = System.currentTimeMillis();
        
        super.detail();
        
        long end = System.currentTimeMillis();
        System.out.println("耗时:"+(end-begin));
    

 2. 测试类如下:

public class Test 
    public static void main(String[] args) 
        OrderService order = new OrderServiceImplSub();
        order.create();
        order.update();
        order.detail();
    

 ➳ 说明:1.虽然解决了OCP开闭原则,但这种方式会导致耦合度很高,因为采用了继承关系,继承关系时一种耦合度非常高的关系,不建议使用  2.代码同样没有得到复用

解决方案三:静态代理模式

编写OrderServiceImpl 的代理类,增强其业务方法,最后调用代理类的代理目标方法

静态代理模式的基本操作逻辑如下:

  • ① 明确目标对象即被代理的对象;
  • ② 定义代理对象,通过构造器持有目标对象;
  • ③ 代理对象中定义前后置增强方法;

1. 新建 OrderServiceProxy  代理类,注意代理类和目标类的行为特征是一样的,所以一定也是实现了OrderService接口

/**
 * @Author wpf
 * @Date 2023/3/21 15:20
 * @Desc 代理类
 * ① 明确目标对象即被代理的对象:代理对象和目标对象要具有相同的行为,要实现同一个或同一些接口
 */
public class OrderServiceProxy implements OrderService 


    /**
      将目标对象作为代理对象的一个属性。这种关系叫做关联关系,比继承关系的耦合度低
      代理对象中含有目标对象的引用,关联关系。has a
    */
    // ② 定义代理对象,通过构造器持有目标对象;
    private OrderService target; 

    // 创建代理对象的时候,传一个目标对象给代理对象
    public OrderServiceProxy(OrderService target) 
        this.target = target;
    

    @Override
    public void create()  // 代理方法
        // ③ 定义前后置增强方法
        long begin = System.currentTimeMillis();

        // 调用目标对象的目标方法
        target.create();

        // ③ 定义前后置增强方法
        long end = System.currentTimeMillis();
        System.out.println("耗时:"+(end-begin));
    

    @Override
    public void update()  // 代理方法
        // ③ 定义前后置增强方法
        long begin = System.currentTimeMillis();

        // 调用目标对象的目标方法
        target.update();

        // ③ 定义前后置增强方法
        long end = System.currentTimeMillis();
        System.out.println("耗时:"+(end-begin));
    

    @Override
    public void detail()  // 代理方法
        // ③ 定义前后置增强方法
        long begin = System.currentTimeMillis();

        // 调用目标对象的目标方法
        target.detail();

        // ③ 定义前后置增强方法
        long end = System.currentTimeMillis();
        System.out.println("耗时:"+(end-begin));
    

2. 测试类及执行结果如下

public class Test 
    public static void main(String[] args) 

        // 创建目标对象
        OrderService target = new OrderServiceImpl();
        // 创建代理对象
        OrderServiceProxy proxy = new OrderServiceProxy(target);
        // 调用代理对象的代理方法
        proxy.create();
        proxy.update();
        proxy.detail();
    

➳ 说明:1.解决了OCP开闭原则问题  2.采用代理模式的has a,可以降低耦合度。但静态代理这种方式也有一个很明显的缺点:  类爆炸! 假设系统中100个接口,那岂不是要写100个代理类?

可能有些人就问了,静态代理模式跟扩展子类也差不多啊,还是有这么多重复的代码,只不过一个是继承(方案二:扩展Impl的子类),一个是实现公共接口(方案三:静态代理)

错错错!方案二是泛化关系 is a ,例: cat is animal,方案三是关联关系 has a,例: I has a apple ,相比来说泛化关系的耦合度高于关联关系,所以优先使用关联关系!

注意:类之间包括6种关系。关系的强弱顺序为:泛化=实现 > 组合> 聚合> 关联> 依赖

静态代理解析

静态代理明确定义了代理对象,即有一个代理对象的.java文件加载到JVM的过程,很显然的一个问题,在实际的开发过程中,不可能为每个目标对象都定义一个代理类,同样也不能让一个代理对象去代理多个目标对象,这两种方式的维护成本都极高。

☁ 思考:怎么解决类爆炸问题?
 

解决方案四:动态代理模式(JDK)

在内存中动态的生成字节码代理类的技术,叫做:动态代理

在程序运行阶段,在内存中动态生成代理类,被称为动态代理,目的是为了减少代理类的数量,解决代码复用的问题。

这里需要了解一个5毛钱的小知识点

硬盘中的class文件和内存这种的class文件区别不大,唯一区别是硬盘上的class文件,关机之后文件还在,内存中的class是关机就没有了的。
相同点:硬盘上的class文件要想执行,得先通过类加载器加载到内存中才可以执行,同样内存里生成的class也是一样的,必须先通过类加载器加载到内存中

场景解释

基于一个场景来描述动态代理和静态代理的区别,即最近几年很火的概念,海外代购:

在代购刚兴起的初期,是一些常去海外出差的人,会接代购需求,即代理人固定;后来就兴起海外代购平台,海淘等一系列产品,即用户代购需求(目标对象)由代购平台去实现,但是具体谁来操作这个就看即时分配,这个场景与动态代理的原理类似。

代码实操

这里先贴具体代码,详细参数解析滑至下方JDK动态代理技术

1. 由于是动态生成class代理类,因此直接写一个测试类即可,也就是模拟客户端

public class JdkProxyTest 
    public static void main(String[] args) 
        // 创建目标对象
        OrderService target = new OrderServiceImpl();

        /**
          newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
          1.ClassLoader loader:
              (目标类)类加载器
          2.Class<?>[] interfaces:
              (目标类)实现接口,从面向对象来考虑,接口与实现分离,代理类通过实现xx接口,模拟目标类的需求
          3.InvocationHandler h:
              代理类提供的功能封装,可以在目标方法调用前后做增强处理
         注意点:
            类加载器必须和目标类的加载器是同一个,所以这里直接传入target.getClass().getClassLoader()
            实现接口也是一样,代理对象和目标对象要具有相同的行为,就要实现同一个或同一些接口
         */
        // 创建代理类,注意类型转换:代理对象和目标对象实现的接口一样,所以可向下转型
        OrderService proxy = (OrderService)Proxy.newProxyInstance(target.getClass().getClassLoader()
                , target.getClass().getInterfaces()
                , new TimerInvocationHandler(target));

        // 调用代理类的目标方法,在这一步TimerInvocationHandler的invoke方法被调用
        proxy.create();
        //proxy.detail();
        //proxy.update();
    

2. 实现 InvocationHandler 接口,写具体增强代码逻辑   

/**
 * @Author wpf
 * @Date 2023/3/22 11:23
 * @Desc 专门负责计时的一个调用处理器对象
 * 在当前调用处理器中编写计时相关的增强代码,该调用处理器只需写一次
   ————这个接口的目的地就是为了让你有地方写增强代码的
 */
public class TimerInvocationHandler implements InvocationHandler 

    // 目标对象
    private Object target;
    
    // 构造函数接收一个目标对象,主要用于invoke方法中的使用
    public TimerInvocationHandler(Object target)
        // 赋值成员变量
        this.target = target;
    


    /**
      public Object invoke(Object proxy, Method method, Object[] args)
       1.Object proxy:代理对象的引用,很少使用(代理后的实例对象com.sun.proxy.$Proxy0)
       2.Method method:目标对象的目标方法
       3.Object[] args:目标方法上的实参
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 
        System.out.println("(这里写增强代码)执行invoke...");
        // 注意:调用代理对象到的代理方法时,如果要做增强对的话,目标对象的目标方法需要执行
        return method.invoke(target,args);
    

3. 执行结果如下

如上图所示,在调用目标方法create()时,invoke方法就会被调用,在invoke里我们可以编写具体增强的代码逻辑,比如统计该方法的耗时等等

 

»» 头脑风暴

1. 为什么强行要求你必须实现InvocationHandler接口?
     a. 因为一个类实现接口就必须实现接口中的方法。
     b. JDK在底层调用invoke()方法的程序已经提前写好了

注意:invoke()方法不是我们程序员负责调用的,是JDK负责调用的!

2.invoke方法什么时候被调用呢?
  当代理对象调用代理方法时,注册在InvocationHandler当中的invoke()方法被调用


 

动态代理技术类型 

在内存当中动态生成类的技术常见的包括:

  1. JDK动态代理技术:只代理接口
  2. CGLIB动态代理技术:CGLIB(Code Generation Library) 是一个开源项目。是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。它既可以代理接口,又可以代理类,底层是通过继承的方式实现的。性能比JDK动态代理好(底层有一个小而快的字节码处理框架ASM)
  3. Javassist动态代理技术:Javassist是一个开源的分析,编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的千叶滋所创建的。它已加入了开放源代码JBoss应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态“AOP”框架

JDK动态代理技术

在JDK动态代理中,要求代理类的类加载器必须得和目标类的加载器一致,也就是必须和目标类的加载器是同一个!

首先看两个核心类,这里简述下概念,看完基本过程再细聊:

  • ① Proxy-创建代理对象,核心参数:

    • ClassLoader:(目标类的)加载器
    • Interfaces:(目标类的)接口数组
    • InvocationHandler:代理调用机制
  • ② InvocationHandler-代理类调用机制:

    • Object proxy:代理对象的引用(代理后的实例对象com.sun.proxy.$Proxy0)
    • Method method:目标对象的目标方法
    • Object[] :目标方法上的实参

Proxy语法:

Proxy.newProxyInstance(类加载器,代理类要实现的接口,调用处理器)

作用: 新建代理对象,通过调用该方法可以创建代理对象 

本质上Proxy.newProxyInstance方法的执行,主要做了两件事:

  1. 在内存中动态的生成了一个代理类的字节码class
  2. new对象了,通过内存中生成的代理类这个代码,实例化了代理对象

参数说明:

  •  ClassLoader loader:类加载器,加载类需要类加载器,所以要指定类加载器,并且JDK要求,目标类的类加载器必须和代理类的类加载器使用同一个
  •  Class<?>[] interfaces:代理类和目标类要实现同一个接口或同一些接口,在内存中生成代理类的时候,这个代理类需要你告诉它实现哪些接口
  •  InvocationHandler h:调用处理器,是一个接口,在该接口中编写增强代码,因为具体要增强什么代码,JDK动态代理技术它是猜不到的!(既然是接口,就要写接口的实现类)
     

 ▎InvocationHandler语法:

public Object invoke(目标对象的引用, 目标方法, 目标方法的实参)

作用: 通过动态代理对象调用一个方法时,该方法的调用会被转发到实现InvocationHandler接口类的invoke方法来调用 

参数说明:

  •  Object proxy:代理对象的引用,很少使用(代理后的实例对象com.sun.proxy.$Proxy0)
  •  Method method:目标对象的目标方法
  •  Object[] args:目标方法上的实参

invoke方法是JDK负责调用的,因此JDK调用这个方法时会自动传递这三个参数过来,我们可以直接拿来使用!

扩展知识点

方法调用四要素::哪个对象、哪个方法、传什么参数、返回什么值!

从上述动态代理代码实操处,InvocationHandler的实现类中invoke方法实际代码(如下),我们是通过传入了一个目标对象去调用方法的 method.invoke( target, args);   我开始写的时候没注意,直接把方法形参proxy给传进去了method.invoke( proxy , args); 导致运行的时候死循环调用报错!

原因:这个很好理解,proxy是代理类的对象,当该对象方法被调用时会触发InvocationHandler,而我们的InvocationHandler里又调用了proxy里的对象??递归循环调用自己!另外proxy对应的方法是没有实现的。最终导致结果就是循环的不停报错!

调用method.invoke( proxy , args); 死循环报错截图如下:

 

思考:JDK动态代理到底是怎么实现的呢?

JDK原生提供的动态代理就是通过反射实现的,但动态代理的实现方式还可以是ASM(一个短小精悍的字节码操作框架)、cglib(基于ASM)等,并不局限于反射。

我们来看看源码其中关键的地方,在newProxyInstance()发放中有这样几段:

思考:JDK动态代理为什么不能代理类,只能代理接口?

源码面前,了无秘密

 我们可以先从源码上分析,在测试代码中设置代理类的字节码文件可见,如下图所示

// 将JDK动态代理生成的类保存为.class文件(jdk1.8之前的写法)
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

// jdk1.8之后的写法
System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");

打开字节码文件查看,代理类本质上是继承了Proxy类,Proxy的作用简单来说就是保存自定义的InvocationHandler,即用户自定义代理逻辑的增强代码,便于在方法代理时执行自定义InvocationHandler的逻辑($Proxy0具体源码请滑至下方)

解答说明

 Because:从源码头部可以看出,$Proxy0继承了类Proxy,在Java中由于是单继承,多实现机制,因此不能再extends继承类了,故只能implements实现接口。

 But:个人认为代理接口不是从源码上理解的因为单继承的问题而做出的让步,而恰恰是代理接口比代理类更好更灵活些,在 mybatis 和 jdbc等spi机制下,全部都是调接口,因为这样可以灵活的代理多个类。我相信,代理类绝对可以做到不去继承 Proxy,但是人家并没有这么做。

代理类源码


public final class $Proxy0 extends Proxy implements OrderService 
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m5;
    private static Method m0;
    private static Method m4;

    // 通过反射获取到目标类的几个方法,基本的equals、toString、hashCode
    // 以及我们目标类中自己的方法,create、detail、update
    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("jdkProxy.OrderService").getMethod("detail");
            m5 = Class.forName("jdkProxy.OrderService").getMethod("create");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m4 = Class.forName("jdkProxy.OrderService").getMethod("update");
         catch (NoSuchMethodException var2) 
            throw new NoSuchMethodError(var2.getMessage());
         catch (ClassNotFoundException var3) 
            throw new NoClassDefFoundError(var3.getMessage());
        
    

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

    // 这是我们目标类中的自己设置的方法
    public final void detail() throws  
        try 
            super.h.invoke(this, m3, (Object[])null);
         catch (RuntimeException | Error var2) 
            throw var2;
         catch (Throwable var3) 
            throw new UndeclaredThrowableException(var3);
        
    

    // 这是我们目标类中的自己设置的方法
    public final void create() throws  
        try 
            super.h.invoke(this, m5, (Object[])null);
         catch (RuntimeException | Error var2) 
            throw var2;
         catch (Throwable var3) 
            throw new UndeclaredThrowableException(var3);
        
    

    // 这是我们目标类中的自己设置的方法
    public final void update() throws  
        try 
            super.h.invoke(this, m4, (Object[])null);
         catch (RuntimeException | Error var2) 
            throw var2;
         catch (Throwable var3) 
            throw new UndeclaredThrowableException(var3);
        
    

CGLIB动态代理

既可以代理接口,又可以代理类,底层采用继承的方式实现,所以被代理的目标类不能使用final修饰

1. 创建UserService目标类

/**
 * @Desc 目标类
 * 正常来说是一个接口,再写子类或者实现类去写具体的逻辑,cglib会自动生成子类,不用自己写
 */
public class UserService 
    public boolean login(String name,String password)
        System.out.println("正在校验身份....");
        if("admin".equals(name) && "123".equals(password))
            return true;
        
        return false;
    

    public void loginOut()
        System.out.println("正在退出系统...");
    

2. 创建cglib的回调类,即具体增强代码的类

/**
 * @Author wpf
 * @Date 2023/4/4 17:15
 * @Desc 跟JDK一样,只不过JDK使用的是InvocationHandler,CGLIB用的是MethodInterceptor
 */
public class TimerMethodInterceptor implements MethodInterceptor 

    /**
     * target:cglib生成的代理对象,表示增强的对象
     * method:被代理对象方法,即要拦截的方法
     * args:被拦截方法的参数
     * methodProxy: 代理方法
     */   
    @Override
    public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable 
        // 前置增强
        long begin = System.currentTimeMillis();

        // 调用目标方法
        Object obj = methodProxy.invokeSuper(target, args);

        // 后置增强
        long end = System.currentTimeMillis();
        System.out.println("耗时:"+(end-begin)+"毫秒");

        return obj;
    

3. 创建测试类

public class cglibTest 
    public static void main(String[] args) 

        // 1.创建字节码增强器对象
        // 该对象是CGLIB库中的核心对象,就是依靠它来生成代理类的
        Enhancer enhancer = new Enhancer();

        // 2.告诉cglib父类是谁?即目标类是哪个(cglib底层采用继承,生成目标类的子类方式)
        enhancer.setSuperclass(UserService.class);

        // 3.设置回调(等同于JDK动态代理中的调用处理器InvocationHandler)
        // 在CGLIB中不是InvocationHandler,而是方法拦截器接口:MethodInterceptor
        enhancer.setCallback(new TimerMethodInterceptor());

        // 4.创建代理对象。
        // 这一步做两件事:
        //    1.在内存中生成UserService类的子类,即代理类的字节码
        //    2.创建代理对象
        // 父类是UserService,子类这个代理类一定是UserService
        UserService userService = (UserService) enhancer.create();
        System.out.println("使用cglib生成的代理类:"+userService);

        // 5.调用目标方法
        boolean success = userService.login("admin", "123");
        System.out.println(success?"登陆成功!":"登陆失败!");

        userService.loginOut();
    

4. 执行结果如下:

 

 


 

CGLIB和JDK动态代理区别

主要区别:cglib可以代理接口和类,jdk只能代理接口

JDK 动态代理:

  • 只能对实现了接口的类生成代理,而不能针对类。
  • 利用拦截器(实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。

因为采用的是继承,所以该类或方法最好不要声明成final,对于final类或方法是无法继承的


CGLIB 动态代理:

  • 对指定的类生成一个子类,覆盖其中的方法,并覆盖其中方法实现增强,底层采用继承方式
  • 利用 ASM 开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

注意点:Spring AOP不支持代理类内部方法调用的拦截,比如类中a方法调用b方法,切面拦截b方法会失败的


何时使用 JDK 还是 CGLIB 动态代理呢?

目标对象实现了接口,默认情况下会采用 JDK 的动态代理实现 AOP,也可以强制使用 CGLIB ;
如目标对象未实现接口,必须采用 CGLIB,Spring 会自动在 JDK 动态代理和 CGLIB 之间转换。

如何强制使用 CGLIB 实现 AOP?

  xml配置

<aop:aspectj-autoproxy proxy-target-class="true"/>

  注解配置

@EnableAspectJAutoProxy(proxyTargetClass = true)

JDK 和 CGLIB 哪个速度更快?

每一次 JDK 版本升级,JDK 代理效率都得到提升,而 CGLIB 代理消息确有点跟不上步伐。

jdk6 之前比使用 Java 反射效率要高。 JDK 6 之前比使用 Java 反射效率要高。

jdk6后逐步对 JDK 动态代理优化之后,在调用次数较少的情况下:JDK > CGLIB ,进行大量调用的时候,jdk6 和 jdk7 比 CGLIB 代理效率低一点,

jdk8 时,JDK 代理效率高于 CGLIB 代理。

结论

JDK 动态代理不需要第三方库支持,只需要 JDK 环境就可以进行代理,使用条件:

  • 实现InvocationHandler
  • 使用Proxy.newProxyInstance产生代理对象
  • 被代理的对象必须要实现接口

CGLIB 必须依赖于 CGLIB 的类库,其为需要被代理的类生成一个子类,覆盖其中的方法,实际上是一种继承。

总结:动态代理的使用场景

好处就是比较灵活,可以在运行的时候才切入改变类的方法,而不需要预先定义它。

动态代理一般我们比较少去手写,但我们用得其实非常多。在Spring项目中用的注解,例如依赖注入的@Bean、@Autowired,事务注解@Transactional等都有用到,换言之就是Srping的AOP(切面编程)。

这种场景的使用是动态代理最佳的落地点,可以非常灵活地在某个类,某个方法,某个代码点上切入我们想要的内容,就是动态代理其中的内容。

深入理解设计模式-代理模式(静态代理动态代理jdk和cglib)

文章目录


一、定义

所谓代理模式是指客户端并不直接调用实际的对象,而是通过调用代理对象,来间接的调用实际的对象。通俗的来讲代理模式就是我们生活中常见的中介。


二、使用场景

  • 热门框架源码:Spring AOP、Mybatis Mapper

  • 权限管理、异常处理、操作日志、事务控制


三、代码样例

1.类图

2.静态代理

/**
 * 公共接口
 */
public interface Singer 
    void sing();


/**
 * 具体角色,男歌手
 */
public class MaleSinger implements Singer
    private String name;

    public MaleSinger() 
    

    public MaleSinger(String name) 
        this.name = name;
    
    @Override
    public void sing() 
        System.out.println(this.name+"男歌手在唱歌");
    


/**
 * 代理角色
 */
public class Agent implements Singer 
    //代理角色要有一个被代理角色
    private MaleSinger singer;
    public Agent(MaleSinger singer) 
        this.singer = singer;
    
    @Override
    public void sing() 
        System.out.println("协商出场费,做会场工作");
        //一定是被代理角色歌手去唱歌
        singer.sing();
        System.out.println("会场收拾,结算费用");
    


/**
 * 静态代理客户端
 */
public class StaticProxyClient 
    public static void main(String[] args) 
        MaleSinger singer=new MaleSinger("周杰伦");
        Agent agent=new Agent(singer);
        //通过代理来运行唱歌
        agent.sing();
    

3.JDK动态代理

/**
 * jdk动态代理类工厂
 */
public class JdkProxyFactory 
    /**
     * 动态生成代理对象
     * @param target
     * @return
     */
    public static <T> T getProxyBean(T target)
        Class clazz = target.getClass();
        //在JDK中动态生成代理对象的方法
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), new InvocationHandler() 
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 
                System.out.println("协商出场费,做会场工作");
                Object obj =  method.invoke(target,args);
                System.out.println("会场收拾,结算费用");
                return obj;
            

        );
    

/**
 * jdk动态代理客户端
 */
public class JdkProxyClient 
    public static void main(String[] args) 
        //设置用于输出jdk动态代理产生的类
        System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

        Singer singer = new MaleSinger("周杰伦");
        JdkProxyFactory.getProxyBean(singer).sing();
        System.out.println("aa");

    

4.Cglib动态代理

/**
 * cglib动态代理类工厂
 */
public class CglibProxyFactory 
    /**
     * 动态生成代理对象
     * @param target
     * @return
     */
    public static <T> T getProxyBean(T target)
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback(new MethodInterceptor() 
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable 
                System.out.println("协商出场费,做会场工作");
                Object obj = method.invoke(target,objects);
                System.out.println("会场收拾,结算费用");
                return obj;
            
        );
        return (T) enhancer.create();
    

/**
 * cglib动态代理客户端
 */
public class CglibProxyClient 
    public static void main(String[] args) 
        //用于输出生成的代理class文件,"D:/test/designpattern"表示存储在class文件夹中
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"D:/test/designpattern");
        Singer singer = new MaleSinger("周杰伦");
        CglibProxyFactory.getProxyBean(singer).sing();

    



四、优缺点

优点:

  • 1、代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用
  • 2、代理对象可以扩展目标对象的功能
  • 3、代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性

缺点:

  • 1、代理模式会造成系统设计中类的数量增加
  • 2、在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢
  • 3、增加了系统的复杂度

五、拓展延伸

1.jdk和cglib动态代理类反编译

JDK代理类反编译:


Cglib代理类反编译:


2.jdk和cglib动态代理区别

Jdk动态代理:
利用拦截器加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
特点:
JDK动态代理只能对实现了接口的类生成代理,而不能针对类

Cglib动态代理:
利用ASM框架,对代理对象类生成的class文件加载进来,通过修改其字节码生成子类来处理。
特点:
Cglib是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,并覆盖其中方法的增强,但是因为采用的是继承,所以该类或方法最好不要生成final,对于final类或方法,是无法继承的


结尾

  • 感谢大家的耐心阅读,如有建议请私信或评论留言。
  • 如有收获,劳烦支持,关注、点赞、评论、收藏均可,博主会经常更新,与大家共同进步

以上是关于代理模式:JDK动态代理和静态代理回顾的主要内容,如果未能解决你的问题,请参考以下文章

设计模式 结构型模式 -- 代理模式(动态代理(CGLIB动态代理)三种代理的对比(静态代理动态代理(JDK代理和CGLIB代理)优缺点使用场景))

深入理解设计模式-代理模式(静态代理动态代理jdk和cglib)

代理模式(静态代理,JDK动态代理,JDK动态代理原理分析)

代理模式(静态代理jdk动态代理CGLib动态代理)

设计模式之代理模式详解和应用

设计模式----代理模式