从零开始学习Java设计模式 | 结构型模式篇:代理模式

Posted 李阿昀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始学习Java设计模式 | 结构型模式篇:代理模式相关的知识,希望对你有一定的参考价值。

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

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

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

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

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

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

概述

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

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

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

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

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

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

结构

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

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

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

静态代理

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

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

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

从以上类图中可以看到,有一个接口,即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();
    }
}

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

从上面代码中可以看出测试类直接访问的是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方法执行了。

现在我们就可以来说说代理对象的调用处理程序所表示的含义是什么了。我们通过代理对象去调用方法,其本质调用的就是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方法被我们改进完毕之后,接下来,我们来运行一下测试类,看其是否能正常运行。如下图所示,可以看到测试类运行成功了,而且我们还能看出代售点卖票其本质还是调用火车站卖票的功能,只不过我们对它进行了一个增强,也就是代售点在卖票之前要收取一定的服务费用。

至此,我们就对火车站类对象里面的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这样一个全类名,它就是在内存中动态生成的代理类的全类名。

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

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

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

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

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

拉取下来之后,为了方便进一步研究,大家可以将以上代理类的代码拷贝到记事本或者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) 从零开始学习Java设计模式 | 结构型模式篇:组合模式

从零开始学习Java设计模式 | 结构型模式篇:组合模式

从零开始学习Java设计模式 | 结构型模式篇:装饰者模式

从零开始学习Java设计模式 | 结构型模式篇:装饰者模式

从零开始学习Java设计模式 | 结构型模式篇:外观模式

从零开始学习Java设计模式 | 结构型模式篇:外观模式