GoF之代理模式(静态代理+动态代理(JDK动态代理+CGLIB动态代理带有一步一步详细步骤))

1. GoF之代理模式(静态代理+动态代理(JDK动态代理+CGLIB动态代理带有一步一步详细步骤))

@


每博一文案

你还坐在小时候的篝火旁
看天上的月亮还是那个模样
北斗星在指着你的前方
在夜里你也不会失去你的方向
不会再度感到迷茫
湖面倒映着天空是你心中的梦想
丢弃内心不安的彷徨
不会在任何寂静夜里孤单幻想流浪
总在见过一场暴雨才懂 信念未被击碎
可经历过沉寂的梦想 dreaming再不疲惫
曾经在沉浸 在努力 也赞叹过神秘
曾经为山下的风景 可曾付出过全力
还没做 再付之一炬 擦掉痕迹
曾经许下 多少豪言壮语 太无趣
抓不住机会 let it go 就别再理会
被击溃的理想太脆弱 没太多的滋味
走心的最能代表内心 钢琴再次弹奏
就算把听说故事记录 雷声当作伴奏
Shady的馈赠 也许作为Stan的听众才懂
致敬或拙劣模仿 听到
Not afraid the rhythm
Now u know the feel
新节奏 钻研最新的对策
拒绝去邂逅 嫌弃眼神 由谁制定规则
Call me the king of loser
泥水作champion的配色
胜败输赢无所谓
自甘堕落才是罪恶
					——————《篝火旁(再启程)》

2. 代理模式的理解

场景:拍电影的时候,演员找替身演员。这就是一个代理模式
替身演员: 去代理,演员完成表演。

思考:演员为什么要找替身呢?(为什么要使用代理模式呢?)
第一个原因: 拍自己受伤,找个替身(保护自己)
第二个原因: 自己完成不来,这种高度的动作,替身演员可以完成。

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

  1. 第一个作用: 当一个对象需要受到保护的时候,可以考虑使用代理对象去完成某个行为。
  2. 第二个作用:需要给某个对象的功能进行功能增强的时候,可以考虑找一个代理进行增强。
  3. 第三个作用:A对象无法和B对象直接交互时,也可以使用代理模式来解决。

代理模式中有三大角色:

  1. 第一个角色:目标对象(演员)
  2. 第二个角色:代理对象(替身演员)
  3. 第三个角色:目标对象和代理对象的公共接口,(只有演员和替身演员具有相同的行为动作,才能不被客户端察觉你是)

为什么需要演员和替身演员要有相同的行为动作?

是因为不想让观众知道替身演员,这里的观众其实就是“客户端程序”
如果你使用代理模式的话,对于客户端程序来说,客户端是无法察觉到的,客户端
在使用代理对象的时候就像在使用目标对象。

举例子:

  1. 生活场景1:牛村的牛二看上了隔壁村小花,牛二不好意思直接找小花,于是牛二找来了媒婆王妈妈。这里面就有一个非常典型的代理模式。牛二不能和小花直接对接,只能找一个中间人。其中王妈妈是代理类,牛二是目标类。王妈妈代替牛二和小花先见个面。(现实生活中的婚介所)【在程序中,对象A和对象B无法直接交互时。】
  2. 生活场景2:你刚到北京,要租房子,可以自己找,也可以找链家帮你找。其中链家是代理类,你是目标类。你们两个都有共同的行为:找房子。不过链家除了满足你找房子,另外会收取一些费用的。(现实生活中的房产中介)【在程序中,功能需要增强时。】
  3. 西游记场景:八戒和高小姐的故事。八戒要强抢民女高翠兰。悟空得知此事之后怎么做的?悟空幻化成高小姐的模样。代替高小姐与八戒会面。其中八戒是客户端程序。悟空是代理类。高小姐是目标类。那天夜里,在八戒眼里,眼前的就是高小姐,对于八戒来说,他是不知道眼前的高小姐是悟空幻化的,在他内心里这就是高小姐。所以悟空代替高小姐和八戒亲了嘴儿。这是非常典型的代理模式实现的保护机制。代理模式中有一个非常重要的特点:对于客户端程序来说,使用代理对象时就像在使用目标对象一样。【在程序中,目标需要被保护时】
  4. 业务场景:系统中有A、B、C三个模块,使用这些模块的前提是需要用户登录,也就是说在A模块中要编写判断登录的代码,B模块中也要编写,C模块中还要编写,这些判断登录的代码反复出现,显然代码没有得到复用,可以为A、B、C三个模块提供一个代理,在代理当中写一次登录判断即可。代理的逻辑是:请求来了之后,判断用户是否登录了,如果已经登录了,则执行对应的目标,如果没有登录则跳转到登录页面。【在程序中,目标不但受到保护,并且代码也得到了复用。】

代理模式是GoF23种设计模式之一。属于结构型设计模式。
代理模式的作用是:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用。代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不应该看到的内容和服务或者添加客户需要的额外服务。 通过引入一个新的对象来实现对真实对象的操作或者将新的对象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一个对象,这就是代理模式的模式动机。
代理模式中的角色:

  • 代理类(代理主题)

  • 目标类(真实主题)

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

在这里插入图片描述

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

  • 静态代理

  • 动态代理

3. 静态代理

场景如下:

现在有这样一个接口和实现类:

一个名为:OrderService 的接口。

package com.rainbowsea.proxy.service;


/**
 * 订单业务接口
 */
public interface OrderService {  // 目标接口

    /**
     * 生产订单
     */
    void generate();


    /**
     * 修改订单信息
     */
    void modify();

    /**
     * 查看订单详情
     */
    void detail();
}

还有一个名为 OrderServiceImp 的对应该 OrderService 接口的实现类

package com.rainbowsea.proxy.service;


/**
 * 项目经理提出一个新的需求,要统计所以业务接口中有一个业务方法的耗时
 * 
 */
public class OrderServiceImp implements OrderService{  // 目标对象

    @Override
    public void generate() {

        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("订单已生成");
    }

    @Override
    public void modify() {
        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("订单已修改");
    }

    @Override
    public void detail() {
        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("请看订单详情");
    }
}

项目经理提出一个新的需求,要统计所以业务接口中有一个业务方法的耗时

其中 Thread.sleep() 方法的调用是为了模拟操作耗时。
项目已上线,并且运行正常,只是客户反馈系统有一些地方运行较慢,要求项目组对系统进行优化。于是项目负责人就下达了这个需求。首先需要搞清楚是哪些业务方法耗时较长,于是让我们统计每个业务方法所耗费的时长。如果是你,你该怎么做呢?

第一种方案:直接修改Java源代码,:硬编码,在每一个业务接口中的每一个业务方法中直接添加统计耗时的程序。如下:

在这里插入图片描述

package com.rainbowsea.proxy.service;


/**
 * 项目经理提出一个新的需求,要统计所以业务接口中有一个业务方法的耗时
 * 解决方案一:硬编码,在每一个业务接口中的每一个业务方法中直接添加统计耗时的程序。
 * 这种方案的缺点:
 *      缺点一:违背了OCP开闭原则(修改关闭,这里修改(改了源代码),扩展打开)
 *      缺点二:代码没有得到复用(相同的代码写了很多遍)
 * 解决方案二:编写业务类的子类,让子类集成业务类,对每个业务方法进行编写
 *      缺点一:虽然解决了OCP开闭原则,但是这种方式会导致耦合度很高,因为采用了继承关系
 *      缺点二:代码没有得到复用(相同的代码写了很多遍)
 */
public class OrderServiceImp implements OrderService{  // 目标对象

    @Override
    public void generate() {

        long begin = System.currentTimeMillis();
        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("订单已生成");
        long end = System.currentTimeMillis();
        System.out.println("耗时" + (end - begin) + "毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("订单已修改");
        long end = System.currentTimeMillis();
        System.out.println("耗时" + (end - begin) + "毫秒");
    }

    @Override
    public void detail() {

        long begin = System.currentTimeMillis();

        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("请看订单详情");

        long end = System.currentTimeMillis();
        System.out.println("耗时" + (end - begin) + "毫秒");
    }
}

运行测试:

在这里插入图片描述

方案解析:

解决方案一:硬编码,在每一个业务接口中的每一个业务方法中直接添加统计耗时的程序。

  • 这种方案的缺点:

    缺点一:功能需求上虽然实现了,但是违背了OCP开闭原则(修改关闭,这里修改(改了源代码),扩展打开)

  •  缺点二:代码没有得到复用(相同的代码写了很多遍)
    

方案二: 编写业务类的子类,让子类集成业务类,对每个业务方法进行编写。

编写一个子类 OrderServiceImpSub 继承OrderServiceImpl,在子类中重写每个方法。

在这里插入图片描述

package com.rainbowsea.proxy.service;



public class OrderServiceImp implements OrderService{  // 目标对象

    @Override
    public void generate() {

        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("订单已生成");

    }

    @Override
    public void modify() {
        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("订单已修改");
    }

    @Override
    public void detail() {


        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("请看订单详情");

    }
}


在这里插入图片描述

package com.rainbowsea.proxy.service;

public class OrderServiceImpSub extends OrderServiceImp{

    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        super.generate();
        long end = System.currentTimeMillis();
        System.out.println("耗时" + (end - begin) + "毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        super.modify();
        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) + "毫秒");
    }
}

在这里插入图片描述
方案解析:

解决方案二:编写业务类的子类,让子类集成业务类,对每个业务方法进行编写

  •  缺点一:虽然解决了OCP开闭原则,但是这种方式会导致耦合度很高,因为采用了继承关系
    
  •  缺点二:代码没有得到复用(相同的代码写了很多遍)
    
  •  缺点三:假设系统中有100个这样的业务类,需要提供100个子类,并且之前写好的创建Service对象的代码,都要修改为创建子类对象。
    

第三种方案:使用代理模式(这里采用静态代理)

这里我们将可以为 OrderService接口,作为公共接口,并提供一个代理类(OrderServiceProxy)。

注意: 代理对象(代理对象和目标对象要具有相同的行为,就要实现同一个或同一些接口),客户端在使用代理对象的时候就像在使用目标对象一样。

在这里插入图片描述

package com.rainbowsea.proxy.service;


/**
 * 代理对象(代理对象和目标对象要具有相同的行为,就要实现同一个或同一些接口)
 * 客户端在使用代理对象的时候就像在使用目标对象一样。
 */
public class OrderServiceProxy implements OrderService{

    // 将目标对象作为代理对象的一个属性,这种关系叫做关联关系,比继承关系的耦合度低
    // 代理对象中含有目标对象的引用。关联关系:has a
    private OrderService target;  //  这就是目标对象,

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

    @Override
    public void generate() { // 代理方法
        // 增强
        long begin = System.currentTimeMillis();
        target.generate();
        long end = System.currentTimeMillis();
        System.out.println("耗时: " + (end - begin) + "毫秒");

    }

    @Override
    public void modify() {  // 代理方法
        // 增强
        long begin = System.currentTimeMillis();
        target.modify();
        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) + "毫秒");
    }
}

编写客户端程序:测试:

在这里插入图片描述

 public static void main(String[] age) {
        // 创建目标对象
        OrderService target = new OrderServiceImp();
        // 创建代理对象
        OrderService proxy = new OrderServiceProxy(target);
        // 调用代理对象的代理方法

        proxy.generate();
        proxy.modify();
        proxy.detail();

    }

以上就是代理模式中的静态代理,其中OrderService接口是代理类和目标类的共同接口。OrderServiceImpl是目标类。OrderServiceProxy是代理类。

方案分析:

解决方案三:代理模式

目前我们使用的是静态代理,这个静态代理的缺点是什么?

类爆炸,假设系统中N给你有1000个接口,那么每个接口都需要对于代理类,这样类会急剧膨胀,不好维护。再比如:如果系统中业务接口很多,一个接口对应一个代理类,显然也是不合理的,会导致类爆炸。

怎么解决类爆炸问题?

可以使用动态代理来解决这个问题。

  • 动态代理是代理模式,只不过添加了字节码生成技术,可以在内存中为我们动态的生成一个class字节码,
  • 这个字节码就是代理类。
  • 在内存动态的生成字节码代理类的技术。叫做:动态代理
  • 因为在动态代理中可以在内存中动态的为我们生成代理类的字节码。代理类不需要我们写了。类爆炸解决了,而且代码只需要写一次,代码也会得到复用。

4. 动态代理

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

  • JDK动态代理技术:只能代理接口

  • CGLIB动态代理技术:CGLIB(Code Generation Library)是一个开源项目。是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。它既可以代理接口,又可以代理类,底层是通过继承的方式实现的。性能比JDK动态代理要好。(底层有一个小而快的字节码处理框架ASM。)

  • Javassist 动态代理技术:Javassist 是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。

4.1 JDK动态代理

我们接着上面静态代理中的例子:一个接口和一个实现类。

目标接口: OrderService

在这里插入图片描述

package com.rainbowsea.proxy.service;

public interface OrderService { //目标接口

    /**
     * 生产订单
     */
    void generate();


    /**
     * 修改订单信息
     */
    void modify();

    /**
     * 查看订单详情
     */
    void detail();

    String getName();
}

公共接口实现的目标类:

在这里插入图片描述

package com.rainbowsea.proxy.service;



public class OrderServiceImp implements OrderService{  // 目标对象



    @Override
    public void generate() {

        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("订单已生成");
    }

    @Override
    public void modify() {
        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("订单已修改");
    }

    @Override
    public void detail() {
        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("请看订单详情");
    }

    @Override
    public String getName() {
        System.out.println("getName()方法执行了");
        return "张三";
    }
}

注意:我们在静态代理的时候,除了以上一个接口和一个实现类之外,是不是要写一个代理类UserServiceProxy,但是,在动态代理中 UserServiceProxy 代理类是可以动态生成的,所以这个类不需要写。我们直接写客户端程序即可:

重点:动态生成class字节码,代理类

我们使用: Proxy.newProxyInstance(类加载器,代理类要实现的接口,调用处理器); 来创建生成代理类

在这里插入图片描述
newProxyInstance 翻译为:新建代理对象,也就是说:通过调用这个方法可以创建代理对象

本质上,这个Proxy.newProxyInstance()方法的执行,做了两件事

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

Proxy类全名:java.lang.reflect.Proxy。这是JDK提供的一个类(所以称为JDK动态代理)。主要是通过这个类在内存中生成代理类的字节码。其中newProxyInstance()方法有三个参数:

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

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h) {
    1. 第一个参数:ClassLoader Loader 类加载器。在内存中生成了字节码也就是class 文件,要想执行这个字节码,就需要先把这个字节码加载到内存当中的,而加载到内存当中,就需要类加载器,(加载类,就需要类加载器)所以要指定使用哪个类加载器加载。并且用JDK要求,目标类的类加载器器必须和代理类的类加载器使用同一个(基本上都是一致的,因为这里我们就只有目标类的类加载器,代理类在动态生成)。可以通过 对象.getClass().getClassLoader() 获取到对应的类加载器(就是目标对象的类加载器)。

    2. 第二个参数:Class<?>[] interfaces 接口类型。注意:代理类和目标类要实现同一个接口或同一些接口,在内存当中生成代理类的时候,这个代理类需要知道你告诉它实现哪些接口,从而重写哪些方法()。可以通过对象.getClass().getInterfaces() ** 获取到对应实现类实现的公共接口(就是公共接口**)。

    3. 第三个参数:Invocation Handler h 调用处理器。这是一个JDK动态代理规定的接口,接口全名:java.lang.reflect.InvocationHandler。是一个接口,在调用处理器接口中编写的就是:增强代码(你要添加扩展的内容) ,因为具体生成的代理类(动态代理类即使),它是猜不到的,你要(具体增强什么代码,扩展什么功能的),没有那么神,所以需要你自己编写,既然是接口,就要写接口的实现类(这里类里面,就是编写你要增强的代码,扩展的内容)。注意:虽然这里的可以用Lamda表达式写(从而不需要单独写个实现类),但是这么做的复用性,并没有单独写好为一个类,复用性高

    4. 可能会疑问?自己还要动手写调用处理器接口的实现类,这不会类爆炸吗?不会, 因为这种调用处理器是写一次就好了。

      在这里插入图片描述
      最后一个参数:编写实现了InvocationHandler 处理器接口的实现类(用于编写,我们要增强的代码,扩展的功能)。这里我们编写一个名为:TimerInvocationHandler 的类,实现该 InvocationHandler 处理器接口

      在这里插入图片描述
      该InvocationHandler 处理器接口,要重写其中的一个 public Object invoke(Object proxy, Method method, Object[] args) 方法

      在这里插入图片描述
      注意: invoke方法不是我们程序员负责调用,是JDK负责调用的

      invoke 方法什么时候被调用呢?

      当代理对象调用代理方法(就是目标对象(公共接口)中的方法,)的时候,注册在 InvocationHandler 调用处理器当中的的invoke()方法被调用。这里是:proxy.generate();调用代理对象的代理方法

      invoke 方法的三个参数:

      public Object invoke(Object proxy, Method method, Object[] args)
      invoke 方法是JDK负责调用的,所以JDK调用这个方法的时候会自动给我们传过来这个三个参数
          我们可以在invoke 方法的大括号中直接使用这三参数,这三个参数并不是 NULL值,而是有值的。
          第一个参数:Object proxy 代理对象的引用,这个参数使用较少
          第二个参数:Method method 目标对象上的目标方法,(要执行的目标方法就是它,目标对象的方法就存储在这个参数里)
          第三个参数:Object[] args 目标方法上的参数    
          返回值:Object 是目标方法中的返回值,如果要接收目标方法中的返回值,就需要用这个:Object[] args 参数 return 回去。
      

上面的知识铺垫完成了,下面就开始,来实现JDK动态代理

首先我们通过:Proxy.newProxyInstance(类加载器,代理类要实现的接口,调用处理器) 方法,来创建一个动态代理类。

在这里插入图片描述
下面编写实现了 InvocationHandler 处理器接口的实现类(用于编写,我们要增强的代码,扩展的功能)。这里我们编写一个名为:TimerInvocationHandler 的类,实现该 InvocationHandler 处理器接口。注意:虽然这里的可以用Lamda表达式写(从而不需要单独写个实现类),但是这么做的复用性,并没有单独写好为一个类,复用性高

在这里插入图片描述

我们将来肯定是要调用“目标对象当中的目标方法”的,但要调用目标对象当中的目标方法的话,需要“目标对象”的存在,“目标对象”从哪儿来呢?我们可以给 TimerInvocationHandler 提供一个构造方法,可以通过这个构造方法传过来“目标对象”,代码如下:

在这里插入图片描述
在这里插入图片描述

package com.rainbowsea.proxy.service;

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


/**
 * 专门赋值计时的一个调用处理器对象
 */
public class TimerInvocationHandler implements InvocationHandler {

    // 目标对象
    private Object target;

    public TimerInvocationHandler(Object target) {
        // 赋值给成员变量
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("invoke 执行...");
        return null;
    }




}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
运行测试:

在这里插入图片描述

package com.rainbowsea.proxy.client;

import com.rainbowsea.proxy.service.OrderService;
import com.rainbowsea.proxy.service.OrderServiceImp;
import com.rainbowsea.proxy.service.TimerInvocationHandler;
import com.rainbowsea.proxy.util.ProxyUtil;

import java.lang.reflect.Proxy;

public class Client {
    // 客户端程序
    public static void main(String[] args) {
        // 创建目标对象
        OrderService target = new OrderServiceImp();

        // 创建代理对象
        //Proxy.newProxyInstance(类加载器,代理类要实现的接口,调用处理器);
        
        // 注意:代理对象和目标对象都实现了共同的接口,接口是一样的看,所以可以向下转型
        OrderService proxy = (OrderService)Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(), new TimerInvocationHandler(target));

        // 调用代理对象的代理方法
        // 注意:调用代理对象的代理方法的时候,如果你要做增强的话,弥补对象的目标方法得保证执行。
        proxy.generate();
        proxy.modify();
        proxy.detail();



    }
}

在这里插入图片描述

为什么明明调用了,目标对象当中的,generate( )

modify( ),detail( ) 方法 ,却没有执行呢,反而执行的是:调用处理器实现 InvocationHandler

接口中的实现类(当中增强代码,扩展代码/功能)的 invoke( ) 方法呢。

那是因为,我们来需要在,

在这里插入图片描述

在invoke() 方法大括号下,调用其中的参数,method.invoke() 方法,执行目标对象当中的方法。

注意这个[method.invoke() ] 调用目标对象上的目标方法;只要代理对象当中注册的InvocationHandler的方法被调用了,这里就会被调用

// 方法四要素: 哪个对象,哪个方法,传什么参数,返回什么值
// target 目标对象,method方法,args 参数,reValue 返回值(返回值,只能返回一个值)
Object reValue = method.invoke(target, args);

在这里插入图片描述

在这里插入图片描述
测试:

在这里插入图片描述

package com.rainbowsea.proxy.service;

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


/**
 * 专门赋值计时的一个调用处理器对象
 */
public class TimerInvocationHandler implements InvocationHandler {
   
    // 目标对象
    private Object target;
    public TimerInvocationHandler(Object target) {
        // 赋值给成员变量
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
         // 这个接口的目的就是为了让你有地方写增强代码
        
        System.out.println("invoke 执行...");
 // 调用目标对象上的目标方法;只要代理对象当中注册的InvocationHandler的方法被调用了,这里就会被调用
        // 方法四要素: 哪个对象,哪个方法,传什么参数,返回什么值
        Object reValue = method.invoke(target, args);

        return null;
    }

}

package com.rainbowsea.proxy.client;

import com.rainbowsea.proxy.service.OrderService;
import com.rainbowsea.proxy.service.OrderServiceImp;
import com.rainbowsea.proxy.service.TimerInvocationHandler;
import com.rainbowsea.proxy.util.ProxyUtil;

import java.lang.reflect.Proxy;

public class Client {
    // 客户端程序
    public static void main(String[] args) {
        // 创建目标对象
        OrderService target = new OrderServiceImp();

        // 创建代理对象
        // 注意:代理对象和目标对象都实现了共同的接口,接口是一样的看,所以可以向下转型
        OrderService proxy = (OrderService)Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(), new TimerInvocationHandler(target));

        //OrderService proxy = (OrderService) ProxyUtil.newProxyInstance(target);
        // 虽然这里的    new TimerInvocationHandler() 可以用Lamda表达式写,但是这么做的复用性,并没有
        //单独写好为一个类,复用性高

        // 调用代理对象的代理方法
        // 注意:调用代理对象的代理方法的时候,如果你要做增强的话,弥补对象的目标方法得保证执
        proxy.generate();
        proxy.modify();
        proxy.detail();
        String name = proxy.getName();

        System.out.println(name);


    }
}

接下来,我们就要完善一下,调用处理器当中,我们要增强的代码了,实现对业务上的运行耗时计算:

在这里插入图片描述

package com.rainbowsea.proxy.service;

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


/**
 * 专门赋值计时的一个调用处理器对象
 */
public class TimerInvocationHandler implements InvocationHandler {
    
    // 目标对象
    private Object target;
    public TimerInvocationHandler(Object target) {
        // 赋值给成员变量
        this.target = target;
    }


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        long begin = System.currentTimeMillis();
        // 这个接口的目的就是为了让你有地方写增强代码
        // 调用目标对象上的目标方法;只要代理对象当中注册的InvocationHandler的方法被调用了,这里就会被调用
        // 方法四要素: 哪个对象,哪个方法,传什么参数,返回什么值
        Object reValue = method.invoke(target, args);

        long end = System.currentTimeMillis();

        System.out.println("耗时" + (end - begin) + "毫秒");

        // 注意:这个invoke方法的返回值,如果代理对象调用代理方法之后,需要返回结果的话,
        // invoke 方法必须将目标对象的目标方法执行结果继续返回
        return null;
    }
}

到此为止,调用处理器就完成了。接下来,应该继续完善Client程序:

在这里插入图片描述
运行:功能实现没问题。在这里插入图片描述

package com.rainbowsea.proxy.service;

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


/**
 * 专门赋值计时的一个调用处理器对象
 */
public class TimerInvocationHandler implements InvocationHandler {
    /*
    1.为什么强行要求你必须实现InvocationHandler 接口?
        因为一个类实现接口就必须实现接口中的方法
        以下这个方法必须是 invoke(),因为JDK在底层调用invoke()方法的程序已经提前写好了
        注意: invoke方法不是我们程序员负责调用,是JDK负责调用的
    2.invoke 方法什么时候被调用呢?
        当代理对象调用代理方法的时候,注册在InvocationHandler调用处理器当中的invoke()方法被调用。
        proxy.generate();调用代理对象的代理方法
    3.invoke 方法的三个参数
        invoke 方法是JDK负责调用的,所以JDK调用这个方法的时候会自动给我们传过来这个三个参数
        我们可以往invoke 方法的大括号中直接使用。
        第一个参数:Object proxy 代理对象的引用,这个参数使用较少
        第二个参数:Method method 目标对象上的目标方法,(要执行的目标方法就是它)
        第三个参数:Object[] args 目标方法上的参数

     4. 我们可以看到这里并没有调用到目标方法的对象中的方法呀
     这个需要我们在增强代码中通过传的参数,自己调用目标对象/代理当中的目标方法。
     */

    // 目标对象
    private Object target;

    public TimerInvocationHandler(Object target) {
        // 赋值给成员变量
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        long begin = System.currentTimeMillis();
        // 这个接口的目的就是为了让你有地方写增强代码
        // 调用目标对象上的目标方法;只要代理对象当中注册的InvocationHandler的方法被调用了,这里就会被调用
        // 方法四要素: 哪个对象,哪个方法,传什么参数,返回什么值
        Object reValue = method.invoke(target, args);

        long end = System.currentTimeMillis();

        System.out.println("耗时" + (end - begin) + "毫秒");

        // 注意:这个invoke方法的返回值,如果代理对象调用代理方法之后,需要返回结果的话,
        // invoke 方法必须将目标对象的目标方法执行结果继续返回
        return reValue;
    }
}

4.1.1 JDK动态代理中(获取到目标对象中目标方法的返回值)

功能是实现了,但是如果我们突然想要通过代理类,获取到目标对象中目标方法的返回值呢

在这里插入图片描述

我们就需要在,我们的调用处理器实现 InvocationHandler

接口中的实现类的(这里是:TimerInvocationHandler类)当中重写的 invoke()方法的大括号中,通过其中的method.invoke()反射机制调用目标对象的方法的返回的值,通过 return 返回回去就可以了。

在这里插入图片描述

注意:这个 method.invoke( ) 方法会,获取到getName()的返回值,虽然可以获取到返回值,但也仅仅只能获取到一个返回值,这一点需要格外的注意。

在这里插入图片描述

在这里插入图片描述

package com.rainbowsea.proxy.service;

public interface OrderService { //目标接口

    /**
     * 生产订单
     */
    void generate();


    /**
     * 修改订单信息
     */
    void modify();

    /**
     * 查看订单详情
     */
    void detail();

    String getName();
}

package com.rainbowsea.proxy.service;


public class OrderServiceImp implements OrderService{  // 目标对象

    @Override
    public void generate() {

        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("订单已生成");
    }

    @Override
    public void modify() {  // 目标对象的,目标方法
        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("订单已修改");
    }

    @Override
    public void detail() {
        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("请看订单详情");
    }

    @Override
    public String getName() {
        System.out.println("getName()方法执行了");
        return "张三";
    }
}

package com.rainbowsea.proxy.service;

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


/**
 * 专门赋值计时的一个调用处理器对象
 */
public class TimerInvocationHandler implements InvocationHandler {
    /*
    1.为什么强行要求你必须实现InvocationHandler 接口?
        因为一个类实现接口就必须实现接口中的方法
        以下这个方法必须是 invoke(),因为JDK在底层调用invoke()方法的程序已经提前写好了
        注意: invoke方法不是我们程序员负责调用,是JDK负责调用的
    2.invoke 方法什么时候被调用呢?
        当代理对象调用代理方法的时候,注册在InvocationHandler调用处理器当中的invoke()方法被调用。
        proxy.generate();调用代理对象的代理方法
    3.invoke 方法的三个参数
        invoke 方法是JDK负责调用的,所以JDK调用这个方法的时候会自动给我们传过来这个三个参数
        我们可以往invoke 方法的大括号中直接使用。
        第一个参数:Object proxy 代理对象的引用,这个参数使用较少
        第二个参数:Method method 目标对象上的目标方法,(要执行的目标方法就是它)
        第三个参数:Object[] args 目标方法上的参数

     4. 我们可以看到这里并没有调用到目标方法的对象中的方法呀
     这个需要我们在增强代码中通过传的参数,自己调用目标对象/代理当中的目标方法。
     */
    // 目标对象
    private Object target;
    public TimerInvocationHandler(Object target) {
        // 赋值给成员变量
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        long begin = System.currentTimeMillis();
        // 这个接口的目的就是为了让你有地方写增强代码
        // 调用目标对象上的目标方法;只要代理对象当中注册的InvocationHandler的方法被调用了,这里就会被调用
        // 方法四要素: 哪个对象,哪个方法,传什么参数,返回什么值
        Object reValue = method.invoke(target, args);

        long end = System.currentTimeMillis();

        System.out.println("耗时" + (end - begin) + "毫秒");

        // 注意:这个invoke方法的返回值,如果代理对象调用代理方法之后,需要返回结果的话,
        // invoke 方法必须将目标对象的目标方法执行结果继续返回
        return reValue;
    }
}

package com.rainbowsea.proxy.client;

import com.rainbowsea.proxy.service.OrderService;
import com.rainbowsea.proxy.service.OrderServiceImp;
import com.rainbowsea.proxy.service.TimerInvocationHandler;
import com.rainbowsea.proxy.util.ProxyUtil;

import java.lang.reflect.Proxy;

public class Client {
    // 客户端程序
    public static void main(String[] args) {
        // 创建目标对象
        OrderService target = new OrderServiceImp();

        // 创建代理对象
        //Proxy.newProxyInstance(类加载器,代理类要实现的接口,调用处理器);

        /*
        newProxyInstance 翻译为:新建代理对象
        也就是说:通过调用这个方法可以创建代理对象
        本质上,这个Proxy.newProxyInstance()方法的执行,做了两件事
                第一件事:在内存中动态的生成一个代理类的字节码class
                第二件事:new 对象了,通过内存中生成的代理类这个代码,实例化了代理对象
          2.关于 newProxyInstance()方法的三个重要的参数, 每一个什么含义,有什么用?
                第一个参数:ClassLoader Loader
                类加载器,这个类加载器有什么用呢?
                    在内存当中生成的字节码也就是class文件,要执行也得先加载到内存当中,加载类就需要
                    类加载器,所以这里需要指定类加载器。并且用JDK要求,目标类的类加载器必须和代理类的类加载器使用同一个
                第二个参数:Class<?>[] interfaces
                    代理类和目标类要实现同一个接口或同一些接口
                    在内存中生成代理类的时候,这个代理类是需要你告诉它实现哪些接口的
                第三个参数:InvocationHandler h
                    InvocationHandler 被翻译为:调用处理器,是一个接口
                    在调用处理器接口中编写的就是:增强代码
                    因为具体要增强什么代码,JDK动态代理技术它是猜不到的,没有那么神
                    既然是接口,就要写接口的实现类。

                    可能会疑问?
                        自己还要动手写调用处理器接口的实现类,这不会类爆炸吗?不会
                        因为这种调用处理器是写一次就好了。

         */
        // 注意:代理对象和目标对象都实现了共同的接口,接口是一样的看,所以可以向下转型
        OrderService proxy = (OrderService)Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(), new TimerInvocationHandler(target));

        //OrderService proxy = (OrderService) ProxyUtil.newProxyInstance(target);
        // 虽然这里的    new TimerInvocationHandler() 可以用Lamda表达式写,但是这么做的复用性,并没有
        //单独写好为一个类,复用性高

        // 调用代理对象的代理方法
        // 注意:调用代理对象的代理方法的时候,如果你要做增强的话,弥补对象的目标方法得保证执行。
        proxy.generate();
        proxy.modify();
        proxy.detail();
        String name = proxy.getName();

        System.out.println(name);


    }
}

到这里,JDK动态代理的原理就结束了。
不过我们看以下这个代码确实有点繁琐,对于客户端来说,用起来不方便:

在这里插入图片描述

我们可以提供一个工具类:ProxyUtil,将这个方法封装起来,让客户端使用:

在这里插入图片描述
在这里插入图片描述

4.2 CGLIB动态代理

CGLIB动态代理技术:CGLIB(Code Generation Library)是一个开源项目。是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。它既可以代理接口,又可以代理类,底层是通过继承的方式实现的。性能比JDK动态代理要好。(底层有一个小而快的字节码处理框架ASM。)

CGLIB既可以代理接口,又可以代理类。因为底层采用继承的方式实现。所以被代理的目标类不能使用final修饰。
使用CGLIB,需要引入它的依赖

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>

在这里插入图片描述
我们准备一个没有实现接口的类,这里我们用继承的方式实现代理类。 如下:

在这里插入图片描述

package com.rainbowsea.proxy.service;

public class UserService {
    // 目标方法
    public boolean login(String username,String password) {
        System.out.println("系统正在验证身份...");

        if("admin".equals(username) && "123".equals(password)) {
            return true;
        }

        return false;
    }


    // 目标方法
    public void logout() {
        System.out.println("系统正在退出...");
    }
}

使用CGLIB在内存中为UserService类生成代理类,并创建对象:

第一步:创建字节码增强器对,使用 new Enhancer()

在这里插入图片描述

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

第二步:告诉CGLIB父类是谁,告诉CGLIB目标类是谁。继承没有其他的就是,父类,就是目标类本身了。

// 告诉CGLIB父类是谁,告诉CGLIB目标类是谁。
enhancer.setSuperclass(UserService.class);

第三步:设置回调(等同于JDK动态代理当中的调用处理器

在CGLIB当中不是invocationHandler 接口,是方法拦截器接口,MethodInterceptor 在这里插入图片描述

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

同样和JDK动态代理一样,我们通过实现该接口(增加具体什么代码,增加功能扩展),new对象。如下: 和JDK动态代理原理差不多,在CGLIB中需要提供的不是InvocationHandler,而是:net.sf.cglib.proxy.MethodInterceptor
编写MethodInterceptor接口实现类:

MethodInterceptor接口中有一个方法intercept(),该方法有4个参数:
第一个参数:目标对象
第二个参数:目标方法
第三个参数:目标方法调用时的实参
第四个参数:代理方法
在MethodInterceptor的intercept()方法中调用目标以及添加增强:

在这里插入图片描述

第四步:创建代理对象,这一步会做两件事

  1. 第一件事: 在内存中生成UserService类的子类,其实就是代理类的字节码
  2. 第二件事: 创建代理对象。

这里父类(目标对象)是UserService ,子类这个代理类一定是UserService。因为代理类和目标类是继承关系,所以可以强制类型转换(向下转型)

// 创建代理对象
// 这一步会做两件事
/*
 第一件事: 在内存中生成UserService类的子类,其实就是代理类的字节码
第二件事: 创建代理对象。
父类是UserService ,子类这个代理类一定是UserService
 */
UserService userServiceProxy = (UserService) enhancer.create();

 // 建议大家能够把CGLIB动态代理生成的代理对象的名字格式有点印象
        // 根据这个名字可以推测框架底层是否使用了CGLIB动态代理
        System.out.println(userServiceProxy);
        // 底层本质,继承,内存当中生成的继承代理类,同时实例化该代理对象
// com.rainbowsea.proxy.service.UserService$$EnhancerByCGLIB$$59206fe7@5d3411d extends UserService{}

第五步:调用代理对象的代理方法

在这里插入图片描述

// 调用代理对象的代理方法
boolean success = userServiceProxy.login("admin", "123");

完整的演示代码:

目标对象,继承的目标对象

package com.rainbowsea.proxy.service;

public class UserService {
    // 目标方法
    public boolean login(String username,String password) {
        System.out.println("系统正在验证身份...");

        if("admin".equals(username) && "123".equals(password)) {
            return true;
        }

        return false;
    }


    // 目标方法
    public void logout() {
        System.out.println("系统正在退出...");
    }
}

和JDK动态代理原理差不多,在CGLIB中需要提供的不是InvocationHandler,而是 net.sf.cglib.proxy.MethodInterceptor
编写MethodInterceptor接口实现类:

package com.rainbowsea.proxy.service;

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

import java.lang.reflect.Method;

public class TimerMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 前面增强
        long begin = System.currentTimeMillis();
        // 怎么调用目标对象的目标方法呢,reValue 可以作为目标对象的返回值
        Object retValue = methodProxy.invokeSuper(target, objects);

        // 后面增强
        long end = System.currentTimeMillis();

        System.out.println("耗时" + (end - begin) + "毫秒");

        return retValue;
    }
}


客户端:

package com.rainbowsea.proxy.clicent;

import com.rainbowsea.proxy.service.TimerMethodInterceptor;
import com.rainbowsea.proxy.service.UserService;
import net.sf.cglib.proxy.Enhancer;

public class Client {

    public static void main(String[] args) {
        // 创建字节码增强器对象
        // 这个对象是CGLIB库当中的核心对象,就是依靠它李生成代理类
        Enhancer enhancer = new Enhancer();

        // 告诉CGLIB父类是谁,告诉CGLIB目标类是谁
        enhancer.setSuperclass(UserService.class);

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


        // 创建代理对象
        // 这一步会做两件事
        /*
            第一件事: 在内存中生成UserService类的子类,其实就是代理类的字节码
            第二件事: 创建代理对象。
            父类是UserService ,子类这个代理类一定是UserService
         */
        UserService userServiceProxy = (UserService) enhancer.create();

    

        // 调用代理对象的代理方法
        boolean success = userServiceProxy.login("admin", "123");
        System.out.println(success ? "登录成功" : "登录失败");

        userServiceProxy.logout();

    }
}

注意,对于高版本的JDK(高于JDK8以上的),如果使用CGLIB,需要在启动项中添加两个启动参数

--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/sun.net.util=ALL-UNNAMED

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

VM options 填(--add-opens java.base/java.lang=ALL-UNNAMED)

Program arguments 填(--add-opens java.base/sun.net.util=ALL-UNNAMED)

如果没有显示:VM options,点击 Modify options 手动添加上。

在这里插入图片描述
在这里插入图片描述
最后点击Apply ,或者 OK,就可以运行了。
在这里插入图片描述
在这里插入图片描述


5. 总结:

  1. 在Java程序中的代理模式的作用:
    1. 第一个作用: 当一个对象需要受到保护的时候,可以考虑使用代理对象去完成某个行为。
    2. 第二个作用:需要给某个对象的功能进行功能增强的时候,可以考虑找一个代理进行增强。
    3. 第三个作用:A对象无法和B对象直接交互时,也可以使用代理模式来解决。
  2. 代理模式中有三大角色:
    1. 第一个角色:目标对象(演员)
    2. 第二个角色:代理对象(替身演员)
    3. 第三个角色:目标对象和代理对象的公共接口,(只有演员和替身演员具有相同的行为动作,才能不被客户端察觉你是)
  3. 代理模式中的公共接口,让代理类和目标类,具有相同的功能,也可以增强功能,增加了扩展的同时,又防止l 客户端知道我们的目标类的,保护目标类
  4. 代理模式在代码实现上,包括两种形式: 静态代理,动态代理
    1. 动态代理:
      1. JDK动态代理技术:只能代理接口

      2. CGLIB动态代理技术:CGLIB(Code Generation Library)是一个开源项目。是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。它既可以代理接口,又可以代理类,底层是通过继承的方式实现的。性能比JDK动态代理要好。(底层有一个小而快的字节码处理框架ASM。)

      3. Javassist 动态代理技术:Javassist 是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架

  5. JDK动态代理技术 Proxy.newProxyInstance(类加载器,代理类要实现的接口,调用处理器),三个参数的使用的作用,以及当代理对象调用代理方法(就是目标对象(公共接口)中的方法,)的时候,注册在 InvocationHandler 调用处理器当中的的invoke()方法被调用。这里是:proxy.generate();调用代理对象的代理方法 invoke 方法中的的三个参数的具体作用。
  6. JDK动态代理中(获取到目标对象中目标方法的返回值)的方式,这个 method.invoke( ) 方法会,获取到getName()的返回值,虽然可以获取到返回值,但也仅仅只能获取到一个返回值,这一点需要格外的注意。
  7. CGLIB动态代理:在CGLIB当中不是invocationHandler 接口,是方法拦截器接口,MethodInterceptor

6. 最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”

在这里插入图片描述

posted @ 2024-05-19 21:18  Rainbow-Sea  阅读(47)  评论(0编辑  收藏  举报