设计模式之代理模式

1.定义

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

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

2.结构

代理模式分为三种角色:

  • 抽象主题类(Subject): 通过接口或抽象类声明真实主题和代理对象实现的业务方法
  • 真实主题类(Real Subject): 实现了抽象主题中的具体业务, 是代理对象所代表的真实对象, 是最终要引用的对象
  • 代理类(Proxy): 提供了与真实主题相同的接口, 其内部含有对真实主题的引用, 它可以访问,扩展和控制真实主题的功能

3.静态代理

我们通过案例来感受一下静态代理。

【例】火车站卖票

如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个卖票功能就是抽象主题类, 火车站就是真实主题类, 代售点则是代理对象

java 复制代码
package com.zizhou.proxy.staticlly;

/**
 * 抽象主题:卖票方法接口
 */
public interface SellTickets {
    void sell();
}

package com.zizhou.proxy.staticlly;

/**
 * 真实主题类: 真实目标对象
 */
public class TrainStation implements SellTickets{
    @Override
    public void sell() {
        System.out.println("火车站卖票");
    }
}

package com.zizhou.proxy.staticlly;

/**
 * 代理对象: 代售点
 */
public class ProxyPoint implements SellTickets{
    private TrainStation station = new TrainStation();

    @Override
    public void sell() {
        System.out.println("代售点收取一些服务费用...");
        station.sell();
    }

}

package com.zizhou.proxy.staticlly;

/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        ProxyPoint proxyPoint = new ProxyPoint();
        proxyPoint.sell();
    }
}

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

4.jdk动态代理

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

代码如下:

java 复制代码
package com.zizhou.proxy.jdk;

/**
 * 抽象主题:卖票方法接口
 */
public interface SellTickets {
    void sell();
}

/**
 * 真实主题类: 真实目标对象
 */
public class TrainStation implements SellTickets {
    @Override
    public void sell() {
        System.out.println("火车站卖票");
    }
}

/**
 * 代理工厂,用来创建代理对象
 */
public class ProxyFactory {
    private TrainStation station = new TrainStation();

    public SellTickets getProxyObject() {
        /**
         * newProxyInstance()方法参数说明:
         *  ClassLoader loader: 类加载器,用于加载代理类,使用真实对象的类加载器即可
         *  Class<?>[] interfaces: 真实对象所实现的接口,代理模式代理对象和真实对象实现相同的接口
         *  InvocationHandler h: 代理对象的调用处理程序
         */
        SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(
                station.getClass().getClassLoader(), station.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    /**
                     *  InvocationHandler中的invoke()方法参数说明:
                     *  Object proxy: 代理对象
                     *  Method method: 对应于代理对象上调用的接口方法的Method实例
                     *  Object[] args: 代理对象调用接口方法时传递的实际参数
                     */
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("代售点收取一点代理费用(JDK动态代理方式)....");
                        // 执行真实对象
                        return method.invoke(station, args);
                    }
                });
        return sellTickets;
    }
}

// 测试类
public class Client {
    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory();
        SellTickets sellTickets = proxyFactory.getProxyObject();
        sellTickets.sell();
    }
}

使用了动态代理, 我们思考下面问题:

  • ProxyFactory是代理类吗?
    ProxyFactory不是代理模式中所说的代理类, 而代理类是程序在运行过程中动态的生成在内存中的类.
java 复制代码
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);
    }

    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 void sell() {
        try {
            this.h.invoke(this, m3, null);
            return;
        }
        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);
        }
    }

    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.zizhou.proxy.jdk.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());
        }
    }
}

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

  • 代理类$Proxy0实现了SellTickets接口, 这也就印证了真实目标对象和代理类实现了相同的接口
  • 代理类$Proxy0将我们提供的匿名内部类(InvocationHandler)对象传递给了父类
    动态代理的执行流程是什么样?
    下面是摘取的重点代码
java 复制代码
// 程序运行过程中动态生成的代理类
public final class $Proxy0 extends Proxy implements SellTickets {
    private static Method m3;

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

    public final void sell() {
        this.h.invoke(this, m3, null);
    }
    static {
        m3 = Class.forName("com.zizhou.proxy.jdk.SellTickets").getMethod("sell", new Class[0]);
    }
}

// java提供的动态代理相关类
public class Proxy implements java.io.Serializable {
    protected Proxy(InvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
    }
}

// 代理工厂类
public class ProxyFactory {
    private TrainStation station = new TrainStation();

    public SellTickets getProxyObject() {
        SellTickets sellTickets = (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("代售点收取一点代理费用(JDK动态代理方式)....");
                        // 执行真实对象
                        return method.invoke(station, args);
                    }
                });
        return sellTickets;
    }
}
// 客户端
public class Client {
    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory();
        SellTickets sellTickets = proxyFactory.getProxyObject();
        sellTickets.sell();
    }
}

执行流程如下:

  1. 在测试类中通过代理对象调用sell()方法
  2. 根据多态的特性, 执行的是代理类$Proxy0中的sell()方法
  3. 代理类$Proxy0中的sell()方法中又调用了InvocationHandler接口的子实现类对象的invoke方法
  4. invoke方法通过反射执行了真实对象所属类TrainStation中的sell()方法

5.CGLib动态代理

如果没有定义SellTickets接口, 只定义了TrainStation类.很显然JDK动态代理是无法使用的, 因为JDK动态代理要求必须定义接口, 对接口进行管理.

CGLib是一个功能强大, 高性能的代码生成包.它为没有实现接口的类提供代理, 为JDK的动态代理提供了很好的补充

CGLib是第三方提供的包, 所以需要引入jar包的坐标

xml 复制代码
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2.2</version>
</dependency>
java 复制代码
package com.zizhou.proxy.cglib;


/**
 * 真实主题类: 真实目标对象
 */
public class TrainStation {
    public void sell() {
        System.out.println("火车站卖票");
    }
}

/**
 * 代理工厂,用来创建代理对象
 */
public class ProxyFactory implements MethodInterceptor {
    private TrainStation trainStation = new TrainStation();

    public TrainStation getProxyObject() {
        // 创建Enhancer对象, 类似于JDK动态代理的Proxy类,下一步就是设置几个参数
        Enhancer enhancer = new Enhancer();
        // 设置父类的字节码对象
        enhancer.setSuperclass(trainStation.getClass());
        // 设置回调函数
        enhancer.setCallback(this);
        // 创建代理对象
        TrainStation obj = (TrainStation) enhancer.create();
        return obj;
    }

    /**
     * intercept()方法参数说明
     * @param o 代理对象
     * @param method 真实对象中方法的Method实例
     * @param args 实际参数
     * @param methodProxy 代理方法中方法的Method实例
     * @return
     * @throws Throwable
     */
    @Override
    public TrainStation intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("代理点收取一点服务费(CGLib方式)...");
        TrainStation obj = (TrainStation) methodProxy.invokeSuper(o, args);
        return obj;
    }
}

//测试类
public class Client {
    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory();
        TrainStation station = proxyFactory.getProxyObject();
        station.sell();
    }
}

6.三种代理模式的对比

6.1 JDK动态代理和CGLib动态代理

  • 使用CGLib实现动态代理, CGLib底层是采用ASM字节码生成框架, 使用字节码技术生成代理类, 在JDK1.6之前比使用java反射效率要高.唯一需要注意的是, CGLib不能对声明为final的类或者方法进行代理, 因为CGLib的原理是动态生成被代理类的子类
  • 在JDK1.6,1.7,1.8逐步对JDK动态代理优化后, 在调用次数比较少的情况下, JDK代理效率要高于CGLib代理效率, 只有当进行大量调用的时候,JDK动态代理在1.6, 1.7比CGLib动态代理效率要低一点,但是到JDK1.8的时候, JDK动态代理效率高于CGLib动态代理. 所以,

如果有接口使用JDK动态代理, 没有接口使用CGLib动态代理

6.2 动态代理和静态代理

  • 动态代理和静态代理相比较, 最大的好处在于接口中声明的所有方法都被转移到调用处理器一个集中的方法中(InvocationHandler.invoke)处理. 这样, 在接口方法数量比较多的时候, 我们可以进行灵活处理, 而不需要像静态代理那样每一个方法进行中转
  • 如果接口增加一个方法, 静态代理模式除了所有实现类需要实现这个方法外, 所有代理类也需要实现此方法. 增加了代码维护的复杂度, 而动态代理不会出现该问题

7.优缺点

优点:

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

缺点:

  • 增加了系统的复杂度

8.使用场景

  • 远程(Remote)代理
    本地服务通过网络请求远程服务. 为了实现本地到远程的通信, 我们需要实现网络通信, 处理其中可能的异常. 为良好的代码设计和可维护性, 我们将网络通信部分隐藏起来, 只暴露给本地服务一个接口, 通过该接口即可访问远程服务提供的功能, 而不必过多关心通信部分的细节
  • 防火墙(Firewall)代理
    当你将浏览器配置成代理功能时, 防火墙就将浏览器的请求转给互联网; 当互联网返回响应时, 代理服务器再把它转给你的浏览器
  • 保护(Protect Or Access)代理
    控制一个对象的访问, 如果需要, 可以给不同的用户提供不同级别的使用权限
相关推荐
DC妙妙屋25 分钟前
10.24.2024刷华为OD C题型(四) -- 对象list按照多个属性排序
1024程序员节
bitenum44 分钟前
qsort函数的学习与使用
c语言·开发语言·学习·算法·visualstudio·1024程序员节
Joe.Smith1 小时前
23种设计模式
java·设计模式
EmotionFlying2 小时前
(11)(2.1.6) Hobbywing DroneCAN ESC(一)
copter·ardupilot·1024程序员节·电调和电机
伏飞而行2 小时前
六、元素应用CSS的习题
1024程序员节
idealzouhu2 小时前
Spring Boot 实现文件上传下载功能
java·spring boot·1024程序员节
VaporGas2 小时前
Java设计模式-单例模式和工厂模式的思路解析
java·单例模式·设计模式
sudo_Ene3 小时前
Isaac Sim Docker 部署并使用过程记录
笔记·学习·docker·1024程序员节·isaacsim
天使的同类3 小时前
uniapp使用easyinput文本框显示输入的字数和限制的字数
1024程序员节
Elastic 中国社区官方博客4 小时前
GraphQL 与 Elasticsearch 相遇:使用 Hasura DDN 构建可扩展、支持 AI 的应用程序
大数据·后端·elasticsearch·搜索引擎·全文检索·graphql·1024程序员节