从本讲开始,我们就进入到第三章内容的学习中了,而第三章内容讲的就是结构型模式,所以我们有必要知道什么是结构型模式。

什么是结构型模式呢?结构型模式描述如何将类或对象按某种布局组成更大的结构(可知,结构型模式强调的就是这个结构)。它分为类结构型模式和对象结构型模式,前者采用继承机制(或者实现机制)来组织接口和类,后者釆用组合或聚合来组合对象。

由于组合关系或聚合关系比继承关系耦合度低,满足"合成复用原则",所以对象结构型模式比类结构型模式具有更大的灵活性。

知道了什么是结构型模式之后,接下来我们来看一下结构型模式总共可分为哪几种,如下所示,结构型模式分为以下7种:

  1. 代理模式
  2. 适配器模式
  3. 装饰者模式
  4. 桥接模式
  5. 外观模式
  6. 组合模式
  7. 享元模式

关于以上这7种设计模式,在后续的学习中,我都会为大家一一地进行详细介绍。而在本讲中,我会先为大家介绍第一种设计模式,即代理模式。

概述

什么是代理模式呢?这是我们必须要知道的。

由于某些原因需要给某对象提供一个代理以控制对该对象的访问,这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

以上这段话读完之后,你有什么感想啊?感觉好像字都认识,但连在一起就不知道是什么意思了,是不是啊!没关系,我会举几个现实生活中的例子为大家解释一下。例如,你现在有钱了,想要去买房,这时一般而言你是不可能直接去找到真正的房屋房主的,而是应该去找房屋中介,由房屋中介在中间进行一个牵线,那么这就是所谓的代理模式。再来举一个例子,你想要去买电脑,你总不可能直接去找对应的电脑厂商吧!而是应该去找对应的代理商,就如下图所示的一样。

java是结构化编程语言吗 java结构模式有哪几种_动态代理

现在回过头来再来理解以上对代理模式的描述,应该不难理解吧!它说由于某些原因需要给某对象提供一个代理以控制对该对象的访问,很显然,这个某对象就是目标对象,对应上面卖电脑案例中的联想厂商;此外,它说还提供了一个代理对象,即对应上面卖电脑案例中的地方代理商,这就跟我们去买电脑,不直接去找联想厂商,而是去找地方代理商一样。

大家得好好理解一下代理模式的概念,即使它不是那么好懂。接下来,我们来看一下下面的描述。

Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLIB代理两种。

结构

代理(Proxy)模式分为三种角色:

  • 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法,也就是说,在抽象主题类中定义的是规范
  • 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。对应上面卖电脑案例中的联想厂商
  • 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。对应上面卖电脑案例中的地方代理商

如果大家对以上三种角色还不是特别好理解的话,那么下面我再举个例子来为大家解释一下。例如,现在地方代理商不仅要代理联想,还得代理戴尔,那么此时我们就得要定义一些规范了,不然的话,就没有章法了,而该规范,我们可以把它定义成接口或者抽象类,也即代理模式中的抽象主题类角色。很显然,此时,代理模式中的真实主题类角色就是联想厂商或者戴尔厂商。

java是结构化编程语言吗 java结构模式有哪几种_java是结构化编程语言吗_02

静态代理

在该章节,我们通过一个案例来感受一下静态代理,这个案例就是火车站卖票。

如果要买火车票的话,那么首选是去火车站买票,但是这样会经历坐车到火车站、排队、买票等一系列复杂的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的代理模式,火车站是目标对象,代售点是代理对象。

看完上面的一个描述之后,下面我们再来看一下以下类图,通过该类图我们再把静态代理模式里面的角色给区分一下。

java是结构化编程语言吗 java结构模式有哪几种_目标对象_03

从以上类图中可以看到,有一个接口,即SellTickets,并且它里面还有一个卖票的方法,很显然,它是属于抽象主题类角色的,因为它义的是一套规范。然后,火车站及其代售点都得实现SellTickets接口并重写它里面卖票的方法,这是因为不仅火车站有卖票的功能,而且代售点也有卖火车票的功能。此外,大家还得注意一点,就是在代售点类中得聚合火车站类的对象,因为本质上代售点调用的也是火车站卖票的方法。最后,就是咱们的客户端类了,它直接访问的是代售点,而不直接去访问火车站。

分析完以上类图之后,接下来我们就得编写具体代码来实现以上案例了。

首先,打开咱们的maven工程,并在com.meimeixia.pattern包下新建一个子包,即proxy.static_proxy,也即静态模式的具体代码我们是放在了该包下。

然后,新建SellTickets接口,即卖火车票的接口。对于火车站和代售点而言,它们都得去实现该接口,所以创建该接口也就是定义了一套规范,也即卖火车票的规范。

package com.meimeixia.pattern.proxy.static_proxy;

/**
 * 卖火车票的接口
 * @author liayun
 * @create 2021-06-21 20:54
 */
public interface SellTickets {

    void sell();

}

接着,新建火车站类,即TrainStation。记住,该类得去实现SellTickets接口并重写它里面的卖票方法。

package com.meimeixia.pattern.proxy.static_proxy;

/**
 * 火车站类
 * @author liayun
 * @create 2021-06-21 20:57
 */
public class TrainStation implements SellTickets {
    @Override
    public void sell() {
        System.out.println("火车站卖票");
    }
}

紧接着,新建代售点类,即ProxyPoint。同理,该类也得去实现SellTickets接口并重写它里面的卖票方法,还要一点大家需要注意,从以上类图中可看出,在代售点类中聚合了火车站类的对象,因为代售点卖票本质上还是调用火车站里面的卖票功能进行卖票,所以在代售点类的代码中,我们得在成员位置处声明火车站类的对象。

package com.meimeixia.pattern.proxy.static_proxy;

/**
 * 代售点类
 * @author liayun
 * @create 2021-06-21 21:05
 */
public class ProxyPoint implements SellTickets {
    // 声明火车站类对象
    private TrainStation trainStation = new TrainStation();
    
    @Override
    public void sell() {
        System.out.println("代售点收取一些服务费用");
        trainStation.sell();
    }
}

从上可以看到,代售点在调用火车站里面的卖票功能进行卖票时,还进行了一个增强,即收取了一些服务费用。

以上接口与类创建完毕之后,我们来区分一下它们分别代表的是什么角色?很显然,SellTickets接口是属于抽象主题类角色,TrainStation类是属于真实主题类角色,ProxyPoint类是属于代理类角色。

最后,我们来新建一个客户端类,通过该类来做测试。

package com.meimeixia.pattern.proxy.static_proxy;

/**
 * @author liayun
 * @create 2021-06-21 21:16
 */
public class Client {
    public static void main(String[] args) {
        // 创建代售点类对象
        ProxyPoint proxyPoint = new ProxyPoint();
        // 调用方法进行买票
        proxyPoint.sell();
    }
}

运行以上客户端类的代码,打印结果如下图所示,可以看到确实是我们想要的结果,即代售点最终还是调用火车站卖票的方法进行卖票,只不过在卖票之前,它要收取一些服务费用。

java是结构化编程语言吗 java结构模式有哪几种_java是结构化编程语言吗_04

从上面代码中可以看出测试类直接访问的是ProxyPoint类对象,也就是说ProxyPoint作为访问对象和目标对象的中介,避免了访问对象直接去访问目标对象。同时也对sell方法进行了增强,也就是代理点收取了一些服务费用。

有关增强,我得多说一嘴,你想怎么增强都可以,比如说,

  • 对参数进行增强,当然目前是没有参数的,所以没办法去实现
  • 对方法体进行增强
  • 对返回值进行增强。目前来说,我们这里面只能对方法体进行增强

至此,以上静态代理模式的案例,我就讲完了,不知大家有没有完全理解呢?

动态代理

上面我也已经说过了,动态代理又分为JDK代理和CGLIB代理两种,所以下面我就分别来为大家详细介绍一下它们。

JDK动态代理

我们依旧还是通过以上火车站卖票的案例来学习JDK动态代理,只不过现在我们是对上面静态代理里面的卖火车票的案例进行了一个改进。在改进之前,咱们得先来说一说JDK提供的动态代理。

Java中提供了一个动态代理类Proxy,Proxy并不是我们上述所说的代理对象的类(即不是我们上面所说的代理类),而是提供了一个创建代理对象的静态方法(即newProxyInstance方法)来获取代理对象。

为什么在JDK动态代理里面没有代理类呢?这是因为动态代理是在程序运行阶段动态的在内存中去生成代理类。

明确了以上JDK动态代理的概念之后,接下来我们就要改进上面静态代理里面的卖火车票的案例了。

首先,在com.meimeixia.pattern.proxy包下新建一个子包,即jdk_proxy,也即JDK动态代理的具体代码我们是放在了该包下。

然后,将以上SellTickets接口和TrainStation类拷贝到jdk_proxy包下,因为在JDK动态代理里面,我们也要用到这个卖火车票的接口和火车站类。拷贝过来之后,在jdk_proxy包下再创建一个类,该类我们命名为ProxyFactory,即获取代理对象的工厂类。

由于该类写起来还是比较复杂的,所以我们先暂时将ProxyFactory类写成下面这样。

package com.meimeixia.pattern.proxy.jdk_proxy;

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

/**
 * 获取代理对象的工厂类,代理类也实现了对应的接口
 * @author liayun
 * @create 2021-06-21 23:26
 */
public class ProxyFactory {
    // 声明目标对象,目标对象就是火车站类对象
    private TrainStation station = new TrainStation();

    /**
     * 既然ProxyFactory是工厂类,那么毋庸置疑,在它里面我们需要提供一个获取代理对象的方法
     * @return 大家要记住,代理类也实现了对应的接口,因此该方法的返回值类型我们就写为了SellTickets接口
     */
    // 获取代理对象的方法
    public SellTickets getProxyObject() {
        // 返回代理对象。那么代理对象如何去创建呢?
        /*
         * Proxy类中的newProxyInstance方法所需要的三个参数:
         *      ClassLoader loader:类加载器,用于加载代理类(我们说了,代理类是在程序运行过程中动态的在内存中生成的),可以通过目标对象获取类加载器
         *      Class<?>[] interfaces:代理类实现的接口的字节码对象。由于目标对象所属类也实现了同样的接口,所以我们可以通过目标对象来获取对应接口的字节码对象
         *      InvocationHandler h:代理对象的调用处理程序。不过要注意,InvocationHandler是一个接口,所以你不妨以匿名内部类的形式将该参数体现出来
         */
        SellTickets proxyObject = (SellTickets) Proxy.newProxyInstance(
                station.getClass().getClassLoader(),
                station.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("invoke方法执行了");
                        return null;
                    }
                }
        );
        return proxyObject;
    }
}

写至此,我觉得有必要将一些重要的部分拿出来详细讲讲。

第一点,Proxy类中提供了一个创建代理对象的静态方法(即newProxyInstance方法)来获取代理对象,这个想必大家都知道了,但是对于newProxyInstance方法所需要的参数,有些同学可能还并不是很清楚,所以我有必要为大家详细讲清楚newProxyInstance方法所需要的参数。

  • 第一个参数,ClassLoader loader:类加载器,用于加载代理类(我们说了,代理类是在程序运行过程中动态的在内存中生成的),可以通过目标对象获取类加载器哟~
  • 第二个参数,Class<?>[] interfaces:代理类实现的接口的字节码对象。由于目标对象所属类也实现了同样的接口,所以我们可以通过目标对象来获取对应接口的字节码对象
  • 第三个参数,InvocationHandler h代理对象的调用处理程序。不过要注意,InvocationHandler是一个接口,所以我们可以以匿名内部类的形式将该参数体现出来

对于newProxyInstance方法所需要的前两个参数,想必大家理解起来应该不难,但是对于第三个参数就不一定了,代理对象的调用处理程序所表示的含义是什么呢?

除了上面这个问题,我们还得搞清楚另外一个问题。从上面代码来看,我们是在newProxyInstance方法的最后一个参数处传入了一个匿名内部类,并且我们还在重写的invoke方法里面输出了一句话,那么有没有同学想过invoke方法是来干嘛的啊?以及它又是什么时候被调用的呢?

带着这些问题,我们编写一个客户端类来测试一下,从测试中来找答案。

package com.meimeixia.pattern.proxy.jdk_proxy;

/**
 * @author liayun
 * @create 2021-06-21 23:45
 */
public class Client {
    public static void main(String[] args) {
        // 创建代理对象
        // 1. 创建代理工厂对象
        ProxyFactory factory = new ProxyFactory();
        // 2. 使用factory对象的方法获取代理对象
        SellTickets proxyObject = factory.getProxyObject();
        // 3. 调用卖票的方法
        proxyObject.sell();
    }
}

大家觉得运行以上测试类的代码,invoke方法会不会执行呢?如下图所示,可以看到invoke方法执行了。

java是结构化编程语言吗 java结构模式有哪几种_java是结构化编程语言吗_05

现在我们就可以来说说代理对象的调用处理程序所表示的含义是什么了。我们通过代理对象去调用方法,其本质调用的就是invoke方法,而invoke方法执行的就是业务逻辑处理的代码,故调用invoke方法就能进行业务逻辑处理。

明确了代理对象的调用处理程序之后,咱们再来重点说一下invoke方法中的参数。

  • 第一个参数,Object proxy:代理对象。和proxyObject对象是同一个对象哟,只不过它在invoke方法中基本不用
  • 第二个参数,Method method:对接口中的方法进行封装的Method对象。在本案例中,它表示的就是sell方法,当然,如果接口里面还有其他方法的话,那么通过代理对象也能调用其他的方法
  • 第三个参数,Object[] args:调用方法的实际参数。在本案例中,我们在调用sell方法时是没有传递任何参数的,所以这块的args参数并没有封装对应的数据。如果你有传递实际参数,那么args参数封装的就是你传递的实际参数

明确了invoke方法中的参数所表示的含义之后,我们还得明确一下invoke方法的返回值,这个怎么去理解呢?我们通过代理对象调用sell方法时是没有返回值的,所以此时invoke方法的返回值就是一个null。如果通过代理对象调用sell方法时是有返回值的,那么该返回值就是由invoke方法返回的具体的值。

明确了invoke方法中的参数以及返回值的含义之后,接下来,我们得继续改进invoke方法中的代码了。

之前咱使用静态代理模式实现火车站卖票案例时就已说过,最终代售点去卖票,还是要去调用火车站卖票的功能,所以我们还得在invoke方法中调用目标对象的方法。那怎么去调用呢?很简单,通过反射的方式调用即可,因为invoke方法中的method参数代表的就是sell方法。

package com.meimeixia.pattern.proxy.jdk_proxy;

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

/**
 * 获取代理对象的工厂类,代理类也实现了对应的接口
 * @author liayun
 * @create 2021-06-21 23:26
 */
public class ProxyFactory {
    // 声明目标对象,目标对象就是火车站类对象
    private TrainStation station = new TrainStation();

    /**
     * 既然ProxyFactory是工厂类,那么毋庸置疑,在它里面我们需要提供一个获取代理对象的方法
     * @return 大家要记住,代理类也实现了对应的接口,因此该方法的返回值类型我们就写为了SellTickets接口
     */
    // 获取代理对象的方法
    public SellTickets getProxyObject() {
        // 返回代理对象。那么代理对象如何去创建呢?
        /*
         * Proxy类中的newProxyInstance方法所需要的三个参数:
         *      ClassLoader loader:类加载器,用于加载代理类(我们说了,代理类是在程序运行过程中动态的在内存中生成的),可以通过目标对象获取类加载器
         *      Class<?>[] interfaces:代理类实现的接口的字节码对象。由于目标对象所属类也实现了同样的接口,所以我们可以通过目标对象来获取对应接口的字节码对象
         *      InvocationHandler h:代理对象的调用处理程序。不过要注意,InvocationHandler是一个接口,所以你不妨以匿名内部类的形式将该参数体现出来
         */
        SellTickets proxyObject = (SellTickets) Proxy.newProxyInstance(
                station.getClass().getClassLoader(),
                station.getClass().getInterfaces(),
                new InvocationHandler() {
                    /*
                     * invoke方法中的参数:
                     *      Object proxy:代理对象,和proxyObject对象是同一个对象哟,只不过它在invoke方法中基本不用
                     *      Method method:对接口中的方法进行封装的Method对象。在本案例中,它表示的就是sell方法,当然,如果接口里面还有其他方法的话,那么通过代理对象也能调用其他的方法
                     *      Object[] args:调用方法的实际参数。在本案例中,我们在调用sell方法时是没有传递任何参数的,所以这块的args参数并没有封装对应的数据。
                     *                     如果你有传递实际参数,那么args参数封装的就是你传递的实际参数
                     *
                     * 返回值:方法的返回值
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // System.out.println("invoke方法执行了");
                        // return null;
                        System.out.println("代售点收取一定的服务费用(JDK动态代理)");
                        // 执行目标对象(即火车站类对象)的方法
                        Object obj = method.invoke(station, args);
                        return obj; // 注意,目前我们通过代理对象调用sell方法时是没有返回值的,所以invoke方法返回的就是null
                    }
                }
        );
        return proxyObject;
    }
}

以上invoke方法被我们改进完毕之后,接下来,我们来运行一下测试类,看其是否能正常运行。如下图所示,可以看到测试类运行成功了,而且我们还能看出代售点卖票其本质还是调用火车站卖票的功能,只不过我们对它进行了一个增强,也就是代售点在卖票之前要收取一定的服务费用。

java是结构化编程语言吗 java结构模式有哪几种_设计模式_06

至此,我们就对火车站类对象里面的sell方法进行了增强,而且我们还是在没有修改火车站类的基础上进行的增强,也就是说我们对火车站类对象进行了一个动态的增强。

这样,使用JDK动态代理模式改进火车站卖票案例我就讲完了,希望大家能看懂,也不枉我一片苦心了!

JDK动态代理的底层原理

我相信有些同学心里肯定有一个大大的问号,虽然是使用JDK动态代理模式改进了火车站卖票案例,但是对于JDK动态代理的底层原理我咋还不是特别理解呢!我感觉我整个人都是懵逼状态的啊😱!

没关系,下面我就来为大家讲讲JDK动态代理的底层原理。

现在大家不妨先思考一个问题,那就是上面火车站卖票案例里面的ProxyFactory是代理类吗?很显然,ProxyFactory并不是代理模式中所说的代理类,它只是一个工厂类,而该工厂类提供了一个获取代理对象的方法。大家一定要清楚一点,就是代理类是程序在运行过程中动态的在内存中生成的类,我们是看不到的。

所以,为了研究JDK动态代理的底层原理,我们得通过阿里巴巴开源的Java诊断工具(即Arthas,翻译为阿尔萨斯)去查看代理类的结构。

有些同学可能会问了,阿里巴巴开源的Java诊断工具(即Arthas)如何下载呢?一般而言都是从官网去下载,大家打开Google Chrome浏览器,直接在浏览器地址栏中输入如下url地址回车即可进行下载。

https://arthas.aliyun.com/arthas-boot.jar

你会发现下载下来的是arthas-boot.jar这样一个jar包,该jar包下载下来之后,你想放在哪儿随意,不过我是将其放在了桌面上,因为待会使用起来可能会很方便。

此外,在通过Arthas查看动态生成的代理类的结构之前,我们得预先做好相应准备。回到客户端类当中,我们得预先做两件事情,第一件事是将代理类的名称打印出来;第二件事是让程序一直执行,想要让程序一直执行很简单,直接写一个死循环就可以了。有些同学可能会问了,为什么要让程序一直执行呢?因为代理类是在内存中动态生成的,如果我们的程序结束了,那么内存就会被释放掉,自然代理类我们就不会拿得到了。

package com.meimeixia.pattern.proxy.jdk_proxy;

/**
 * @author liayun
 * @create 2021-06-21 23:45
 */
public class Client {
    public static void main(String[] args) {
        // 创建代理对象
        // 1. 创建代理工厂对象
        ProxyFactory factory = new ProxyFactory();
        // 2. 使用factory对象的方法获取代理对象
        SellTickets proxyObject = factory.getProxyObject();
        // 3. 调用卖票的方法
        proxyObject.sell();

        System.out.println(proxyObject.getClass()); // 先将代理类的名称打印出来

        // 然后让程序一直执行
        while (true) {}

    }
}

现在我们来运行以上客户端类,如下图所示,可以看到打印出了com.sun.proxy.$Proxy0这样一个全类名,它就是在内存中动态生成的代理类的全类名。

java是结构化编程语言吗 java结构模式有哪几种_设计模式_07

然后,我们就得通过arthas-boot.jar来获取代理类了。那如何进行获取呢?很简单,只要大家遵循下面的步骤就能获取到。

第一步,打开CMD命令行窗口,切到arthas-boot.jar所在的目录下。

java是结构化编程语言吗 java结构模式有哪几种_动态代理_08

第二步,使用java -jar arthas-boot.jar命令来执行arthas-boot.jar这个jar包。

java是结构化编程语言吗 java结构模式有哪几种_目标对象_09

这时,你会看到共有三个选项可供选择,那到底我们应该选择哪一项呢?看到第1个选项了没,它说是不是让我们去选择com.meimeixia.pattern.proxy.jdk_proxy.Client这个程序啊!那我们就去选择这个程序呗!也就是选择第1个选项。

java是结构化编程语言吗 java结构模式有哪几种_动态代理_10

第三步,使用jad com.sun.proxy.$Proxy0命令将在内存中动态生成的代理类拉取下来。

java是结构化编程语言吗 java结构模式有哪几种_目标对象_11

拉取下来之后,为了方便进一步研究,大家可以将以上代理类的代码拷贝到记事本或者Notepad++中。这里,为了让大家看清楚拉取下来的代理类的代码,我就将其贴出来了,如下所示,还挺长的。

package com.sun.proxy;

import com.meimeixia.pattern.proxy.jdk_proxy.SellTickets;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements SellTickets {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m3 = Class.forName("com.meimeixia.pattern.proxy.jdk_proxy.SellTickets").getMethod("sell", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            return;
        }
        catch (NoSuchMethodException noSuchMethodException) {
            throw new NoSuchMethodError(noSuchMethodException.getMessage());
        }
        catch (ClassNotFoundException classNotFoundException) {
            throw new NoClassDefFoundError(classNotFoundException.getMessage());
        }
    }

    public final boolean equals(Object object) {
        try {
            return (Boolean)this.h.invoke(this, m1, new Object[]{object});
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final String toString() {
        try {
            return (String)this.h.invoke(this, m2, null);
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final int hashCode() {
        try {
            return (Integer)this.h.invoke(this, m0, null);
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final void sell() {
        try {
            this.h.invoke(this, m3, null);
            return;
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
}

温馨提示:关于阿里巴巴开源的Java诊断工具(即Arthas)的其他用法,大家可以去看看官网,或者在网上搜索一下其他的资料,例如别人写的博客之类的,但在这里我就不再具体讲解该工具的用法了,我们的关注点是要放在设计模式上。

以上代理类的代码太长了,不便于我们研究,所以我就做了一下调整,只将要研究的重点代码摘取出来了,精简成了下面这样。

package com.sun.proxy;

import com.meimeixia.pattern.proxy.jdk_proxy.SellTickets;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements SellTickets {
    private static Method m3;

    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }

    static {
		m3 = Class.forName("com.meimeixia.pattern.proxy.jdk_proxy.SellTickets").getMethod("sell", new Class[0]);
    }

    public final void sell() {
		this.h.invoke(this, m3, null);
    }
}

这样,代理类的代码看着就更加清晰一些了。

从上面的类中,我们可以看到以下几个信息:

  • 代理类(即$Proxy0)不仅继承了Proxy类还实现了SellTickets接口。这也就印证了我们之前说的真实类和代理类实现了同样的接口
  • 代理类(即$Proxy0)将我们提供了的匿名内部类对象传递给了父类

第一点很容易就能看出来,倒是第二点就有点难了,不过也没关系,下面我分析以上代理类中的代码时就会向大家讲清楚。

大家注意看,在静态代码块里面,是先获取到SellTickets接口的字节码对象之后,再去获取它里面的方法对象,也就是说获取的是SellTickets接口里面的表示sell方法的Method对象。获取到之后立马就赋给了静态变量m3。

分析完以上代理类的静态代码块之后,我们再来分析一下其构造方法。发现该代理类并没有提供无参构造,而只是提供了一个有参构造,并且还接收了一个InvocationHandler接口类型的参数,接收之后还将其直接赋给了父类(即Proxy)。

所以,接下来,我们就来看一下Proxy类,发现该类里面有一个InvocationHandler接口类型的成员变量,如下图所示。

java是结构化编程语言吗 java结构模式有哪几种_代理类_12

当我们使用Proxy类里面的静态方法(即newProxyInstance方法)来创建代理对象时,肯定是调用的代理类(即$Proxy0)中的有参构造方法,而该有参构造方法中需要传递一个InvocationHandler接口类型的东东,刚刚好,我们以匿名内部类的形式创建了一个InvocationHandler接口的子实现类对象,如下图所示。这样,该子实现类对象是不是直接赋给了父类(即Proxy)中protected InvocationHandler h这么一个成员变量啊!

java是结构化编程语言吗 java结构模式有哪几种_代理类_13

由于我们现在是通过代理对象来调用sell方法的,而该代理对象就是以上代理类(即$Proxy0)的对象,所以我们就来看看代理类中的sell方法都做了些什么。

很显然,在sell方法里面又调用了当前成员对象(即h,它是InvocationHandler接口类型的)里面的invoke方法,而这个h成员对象就是我们自己定义的InvocationHandler接口的子实现类对象,也就是说sell方法里面实际上调用的就是该子实现类对象中的invoke方法,如下图所示。

java是结构化编程语言吗 java结构模式有哪几种_java是结构化编程语言吗_14

归根结底一句话,我们通过代理对象来调用sell方法,实质上调用的是以上invoke方法,不知大家明白了没?

至此,JDK动态代理的底层原理我就给大家讲解清楚了,相信大家对JDK动态代理的理解更加深刻了。

JDK动态代理的代码执行流程

了解了JDK动态代理的底层原理之后,接下来我们来看看JDK动态代理的代码执行流程是什么样子的。

上面我已经分析得差不多了,所以这里我就直接给出JDK动态代理的代码执行流程了。

  1. 在测试类中通过代理对象调用sell方法
  2. 根据多态的特性,执行的是代理类(即$Proxy0)中的sell方法
  3. 代理类(即$Proxy0)中的sell方法又调用了InvocationHandler接口的子实现类对象的invoke方法,也就是调用了我们自己定义的InvocationHandler接口的子实现类对象的invoke方法
  4. invoke方法通过反射执行了真实对象所属类(即TrainStation)中的sell方法。当然,我们还可以对其进行一个增强

CGLIB动态代理

这里,我们还是通过以上火车站卖票的案例来讲述CGLIB动态代理,即使用CGLIB动态代理来实现火车站卖票的案例。

如果没有定义SellTickets接口,而只是定义了TrainStation(即火车站类),那么此时JDK动态代理还能用吗?很显然JDK动态代理是无法使用了,因为JDK动态代理要求必须定义接口,它是对接口进行代理的,现在没有定义接口,那么自然就不能使用JDK动态代理了。

既然不能使用JDK动态代理,那么应该怎么办呢?我们可以使用CGLIB动态代理来实现哟!接下来,我们就来看一下什么是CGLIB。

CGLIB是一个功能强大,高性能的代码生成包(也就是说CGLIB就是用来生成代码的,而且大家注意了,它也是在内存中动态的去生成哟😋)。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。

总之,一句话,若有接口,则你可以使用JDK动态代理带来实现代理;若没有接口,则你就可以使用CGLIB进行代理了。

由于CGLIB是第三方提供的包,所以我们还需要在咱们的maven工程中引入对应jar包的坐标。

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

引入成功了之后,接下来我们就要开始编写咱们的代码了。

首先,在com.meimeixia.pattern.proxy包下新建一个子包,即cglib_proxy,也即CGLIB动态代理的具体代码我们是放在了该包下。

然后,新建火车站类,即TrainStation。注意了,此时并不需要再新建SellTickets接口了,因为我们现在是要使用CGLIB进行代理。

package com.meimeixia.pattern.proxy.cglib_proxy;

/**
 * 火车站类
 * @author liayun
 * @create 2021-06-21 20:57
 */
public class TrainStation {
    public void sell() {
        System.out.println("火车站卖票");
    }
}

接着,新建工厂类,即ProxyFactory,该工厂就是用来获取代理对象的。理所应当地,我们应该在该工厂类中定义一个获取代理对象的方法,那么大家有没想过该方法的返回值类型应该是什么呢?大家一定要注意了,使用CGLIB进行代理的话,获得的代理类就是目标对象所属类的子类。

所以,很显然,获取代理对象的方法的返回值类型应该是目标对象所属类,即TranStation,因为生成的代理类就是属于该类的一个子类。

明确了上面这点之后,接下来我们就得逐步完善获取代理对象的方法了,就是怎么使用CGLIB来进行代理。

那如何使用CGLIB来进行代理呢?很简单哟!只须遵守以下步骤即可。

第一步,创建Enhancer类对象,该类有点类似于咱们JDK动态代理中的Proxy类。那么很明显,该类就是用来获取代理对象的。注意了,该类是属于CGLIB里面的,所以我们要导入相对应的包哟😁

第二步,设置父类的字节码对象。为啥子要这样做呢?因为使用CGLIB生成的代理类是属于目标类的子类的,也就是说代理类是要继承自目标类的。

第三步,设置回调函数。这一步特别重要,所以我需要着重给大家讲解一下。

什么是回调函数呢?我们通过代理对象去调用sell方法时,你有没有想过,它执行的到底是哪一个方法呢?现在一时搞不清,没关系,我们换个思路出发,就来看看如何设置回调函数。

设置回调函数,其实很简单,无非就是调用Enhancer类对象里面的setCallback方法,可难的是应该向该方法中传递什么。

enhancer.setCallback(...);

其实,我们应该向该方法中传递一个对象,那么是哪个类的对象呢?注意了,是MethodInterceptor接口的子实现类对象哟!所以,在这里我们的做法就很简单了,即让工厂类去实现MethodInterceptor接口,并重写它里面的intercept方法,就像下面这样。

package com.meimeixia.pattern.proxy.cglib_proxy;

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

import java.lang.reflect.Method;

/**
 * 代理对象工厂,用来获取代理对象
 * @author liayun
 * @create 2021-07-30 17:54
 */
public class ProxyFactory implements MethodInterceptor {

    // 声明火车站对象
    private TrainStation station = new TrainStation();

    public TrainStation getProxyObject() {
        // 1. 创建Enhancer类对象,它类似于咱们JDK动态代理中的Proxy类,很明显,该类就是用来获取代理对象的
        Enhancer enhancer = new Enhancer();
        // 2. 设置父类的字节码对象。为啥子要这样做呢?因为使用CGLIB生成的代理类是属于目标类的子类的,也就是说代理类是要继承自目标类的
        enhancer.setSuperclass(TrainStation.class);
        // 3. 设置回调函数
        enhancer.setCallback(...);
        // 4. 创建代理对象
        TrainStation proxyObject = (TrainStation) enhancer.create();
        return proxyObject;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
         System.out.println("方法执行了");
         return null;
    }
}

现在,你该知道setCallback方法里面应该传递什么了吧!既然该方法要的是一个MethodInterceptor接口的子实现类对象,而ProxyFactory工厂类本身就是MethodInterceptor接口的子实现类,所以我们向setCallback方法传递ProxyFactory工厂类自身对象(即this)就可以了。

package com.meimeixia.pattern.proxy.cglib_proxy;

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

import java.lang.reflect.Method;

/**
 * 代理对象工厂,用来获取代理对象
 * @author liayun
 * @create 2021-07-30 17:54
 */
public class ProxyFactory implements MethodInterceptor {

    // 声明火车站对象
    private TrainStation station = new TrainStation();

    public TrainStation getProxyObject() {
        // 1. 创建Enhancer类对象,它类似于咱们JDK动态代理中的Proxy类,很明显,该类就是用来获取代理对象的
        Enhancer enhancer = new Enhancer();
        // 2. 设置父类的字节码对象。为啥子要这样做呢?因为使用CGLIB生成的代理类是属于目标类的子类的,也就是说代理类是要继承自目标类的
        enhancer.setSuperclass(TrainStation.class);
        // 3. 设置回调函数
        enhancer.setCallback(this);
        // 4. 创建代理对象
        TrainStation proxyObject = (TrainStation) enhancer.create();
        return proxyObject;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
         System.out.println("方法执行了");
         return null;
    }
}

哎呀,一开始提出的问题还没解决啊!也就是我们通过代理对象去调用sell方法时,还不知道它执行的到底是哪一个方法呢?难道是上面这个intercept方法吗?又或者说intercept方法它是在什么时候执行的呢?别急,下面我就会讲到。

第四步,创建代理对象。创建代理对象的话,我们使用的是Enhancer类对象里面的create方法,当然,想必大家也都知道了,该代理对象就是目标类的子类对象,所以这一块肯定是要进行一个强制类型转换的。

经过上面四个步骤,相信你一定能写出像上面那样的ProxyFactory类的代码。我就不信我都讲得这么清楚了,你还写不出来。

接下来,为了搞清楚上面的一系列问题,我们就要新建一个客户端类来测试一下了,从测试结果中寻找问题的答案。

package com.meimeixia.pattern.proxy.cglib_proxy;

/**
 * @author liayun
 * @create 2021-07-30 18:11
 */
public class Client {
    public static void main(String[] args) {
        // 创建代理工厂对象
        ProxyFactory factory = new ProxyFactory();
        // 获取代理对象
        TrainStation proxyObject = factory.getProxyObject();
        // 调用代理对象中的sell方法进行卖票
        proxyObject.sell();
    }
}

运行以上测试类,结果如下图所示,现在我们就可以得出这样一个结论了,即通过代理对象调用sell方法时,其实调用的intercept方法,这就是我们为什么要设置回调函数的原因。大家可千万要注意了,设置回调函数时,设置的是intercept方法所属类的对象哟!当然了,在本案例中,设置的就是this。

java是结构化编程语言吗 java结构模式有哪几种_目标对象_15

明确了intercept方法什么时候被调用之后,接下来,我们就得增强目标对象的方法(即火车站对象的卖票方法)了。

package com.meimeixia.pattern.proxy.cglib_proxy;

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

import java.lang.reflect.Method;

/**
 * 代理对象工厂,用来获取代理对象
 * @author liayun
 * @create 2021-07-30 17:54
 */
public class ProxyFactory implements MethodInterceptor {

    // 声明火车站对象
    private TrainStation station = new TrainStation();

    public TrainStation getProxyObject() {
        // 1. 创建Enhancer类对象,它类似于咱们JDK动态代理中的Proxy类,很明显,该类就是用来获取代理对象的
        Enhancer enhancer = new Enhancer();
        // 2. 设置父类的字节码对象。为啥子要这样做呢?因为使用CGLIB生成的代理类是属于目标类的子类的,也就是说代理类是要继承自目标类的
        enhancer.setSuperclass(TrainStation.class);
        // 3. 设置回调函数
        enhancer.setCallback(this);
        // 4. 创建代理对象
        TrainStation proxyObject = (TrainStation) enhancer.create();
        return proxyObject;
    }

    /**
     * intercept方法参数说明:
     * @param Object o:代理对象
     * @param Method method:真实对象中的方法的Method实例。在本案例中,它表示的就是sell方法
     * @param Object[] objects:调用方法的实际参数
     * @param MethodProxy methodProxy:代理对象中的方法的Method实例
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
         // System.out.println("方法执行了");
         // return null;
        System.out.println("代售点收取一定的服务费用(CGLIB代理)");
        // 通过反射的方式去调用目标对象的方法
        Object obj = method.invoke(station, objects);
        return obj; // 注意,目前我们通过代理对象调用sell方法时是没有返回值的,所以invoke方法返回的就是null
    }
}

此时,再来运行客户端类的测试代码,结果如下图所示,发现通过代理对象调用sell方法,其根本也是调用火车站里面的卖票方法,只不过我们在代理对象中对卖票的功能进行了一个增强,因为代售点是要收取一定的服务费用的。

java是结构化编程语言吗 java结构模式有哪几种_java是结构化编程语言吗_16

以上就是CGLIB动态代理的一个实现案例,我就讲到这里了,应该是讲得非常清楚了。

总结

最后,我对CGLIB动态代理的实现做一个总结,如下。

大家一定要记住使用Enhancer类对象里面的create方法可以获取到代理对象,只是在这之前我们需要做两件事,分别是:

  1. 指定父类(代理类的父类哟~)的字节码对象
  2. 当我们通过代理对象调用方法时,其本质调用的是MethodInterceptor规范接口里面的intercept方法。在以上案例中,我们通过代理对象调用具体的sell方法时,实际上执行的就是intercept方法

三种代理的对比

在上面,我们使用CGLIB动态代理实现了火车站卖票的案例,相信大家对于CGLIB动态代理也有了一定的认识。当然,对于CGLIB动态代理的底层实现原理,在本讲我就不再为大家进行详细讲述了。有兴趣的同学可以自己使用阿里巴巴提供的Java诊断工具进行一个代理类的获取,然后自己去分析一下。

在本小节,我们来对比一下以上三种代理,即静态代理、JDK动态代理和CGLIB动态代理。

JDK动态代理和CGLIB动态代理

使用CGLIB实现动态代理,CGLIB底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射的效率要高。唯一需要注意的是,CGLIB不能对声明为final的类或者方法进行代理,因为CGLIB原理是动态生成被代理类的子类(或者目标类的子类)。

在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLIB代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLIB代理。而现在几乎所有的公司基本上都是使用的JDK1.8及以上的版本,所以又可以这样说,JDK代理的效率要高于CGLIB的。

那么以后我们在真正使用的时候,到底应该使用的是JDK代理还是CGLIB代理呢?若有接口则使用JDK动态代理,若没有接口则使用CGLIB代理。

动态代理和静态代理

动态代理与静态代理相比较,最大的好处是接口(也可以是类)中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)了。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转(或者重写)了。

以上这段话说的什么意思啊?这里我给大家解释一下。

如果是JDK动态代理的话,那么它集中都会调用invoke方法,因为JDK动态代理是对接口里面的方法进行代理的,而要是接口里面定义了多个方法,那么通过代理对象调用任何一个方法,最终执行的都是invoke方法,所以这是不是集中进行了一个处理啊!

对于CGLIB动态代理也是同样的一个道理,MethodInterceptor规范接口里面的intercept方法对我们目标对象中的方法进行一个增强,也即对目标类进行了一个代理,而要是目标类里面有多个方法的话,那么通过代理对象调用任何一个方法,最终执行的都将会是这个intercept方法,这是不是也集中进行了一个处理啊!

最后,如果接口增加了一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法,这就增加了代码维护的复杂度。而动态代理则不会出现该问题,因为你在接口或者类里面去新添加了一个方法的话,我们的代理类不需要进行改变的,因为它是动态的在内存中生成的。

代理模式的优缺点以及使用场景

接下来,我们来看下代理模式的优缺点以及使用场景。

优缺点

优点

代理模式的优点,我总结了下面三个。

  1. 代理模式在客户端与目标对象之间起到了一个中介作用和保护目标对象的作用。有了代理模式之后,访问者只需要去访问代理对象而无须直接去访问目标对象了,这就对目标对象起到了一个保护的作用
  2. 代理对象可以扩展目标对象的功能。也就是说,代理对象可以对目标对象里面的功能进行增强,例如,在上面火车站卖票的案例中,代售点就是代理对象,它可以对火车站卖票的功能进行增强,增强的逻辑就是收取一些服务费用。当然了,你可以在你自己的业务里面进行相应的一个增强
  3. 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,注意,主要降低的是访问者和目标对象之间的一个耦合度

缺点

增加了系统的复杂度。对于代理对象,尤其是动态代理,你会发现实现起来还是稍微有点麻烦的,如果你在不了解它底层原理的情况下去实现,那么会特别特别麻烦,而且特别不好理解。

使用场景

使用场景的话,我也总结了下面三个。

  1. 远程(Remote)代理
    本地服务通过网络请求远程服务(也就是说远程的去调用里面的方法或者功能)。为了实现本地到远程的通信,我们需要实现网络通信,处理其中可能的异常。为了良好的代码设计和可维护性,我们应将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节。
    其实,这个就是RPC思想,也即远程去调用方法。后期我们可能会用到很多RPC的框架,它底层就是这么一个思想,也就是远程代理,只不过在这儿我就不做详细的讲解了。
  2. 防火墙(Firewall)代理
    当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。
    其实,这个就是所谓的VPN,通过它我们就能访问外边的一些网络了。
  3. 保护(Protect or Access)代理
    控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。
    使用了代理模式之后,访问者直接访问的是代理对象而不再是目标对象,这样,在代理对象里面,我们就可以给不同的用户提供不同级别的使用权限了。