阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

代理

255次阅读
没有评论

共计 6616 个字符,预计需要花费 17 分钟才能阅读完成。

为其他对象提供一种代理以控制对这个对象的访问。

代理模式,即 Proxy,它和 Adapter 模式很类似。我们先回顾 Adapter 模式,它用于把 A 接口转换为 B 接口:

public class BAdapter implements B {private A a;
    public BAdapter(A a) {this.a = a;
    }
    public void b() {a.a();
    }
}

而 Proxy 模式不是把 A 接口转换成 B 接口,它还是转换成 A 接口:

public class AProxy implements A {private A a;
    public AProxy(A a) {this.a = a;
    }
    public void a() {this.a.a();}
}

合着 Proxy 就是为了给 A 接口再包一层,这不是脱了裤子放屁吗?

当然不是。我们观察 Proxy 的实现 A 接口的方法:

public void a() {this.a.a();}

这样写当然没啥卵用。但是,如果我们在调用 a.a() 的前后,加一些额外的代码:

public void a() {if (getCurrentUser().isRoot()) {this.a.a();} else {throw new SecurityException("Forbidden");
    }
}

这样一来,我们就实现了权限检查,只有符合要求的用户,才会真正调用目标方法,否则,会直接抛出异常。

有的童鞋会问,为啥不把权限检查的功能直接写到目标实例 A 的内部?

因为我们编写代码的原则有:

  • 职责清晰:一个类只负责一件事;
  • 易于测试:一次只测一个功能。

用 Proxy 实现这个权限检查,我们可以获得更清晰、更简洁的代码:

  • A 接口:只定义接口;
  • ABusiness 类:只实现 A 接口的业务逻辑;
  • APermissionProxy 类:只实现 A 接口的权限检查代理。

如果我们希望编写其他类型的代理,可以继续增加类似 ALogProxy,而不必对现有的 A 接口、ABusiness 类进行修改。

实际上权限检查只是代理模式的一种应用。Proxy 还广泛应用在:

远程代理

远程代理即 Remote Proxy,本地的调用者持有的接口实际上是一个代理,这个代理负责把对接口的方法访问转换成远程调用,然后返回结果。Java 内置的 RMI 机制就是一个完整的远程代理模式。

虚代理

虚代理即 Virtual Proxy,它让调用者先持有一个代理对象,但真正的对象尚未创建。如果没有必要,这个真正的对象是不会被创建的,直到客户端需要真的必须调用时,才创建真正的对象。JDBC 的连接池返回的 JDBC 连接(Connection 对象)就可以是一个虚代理,即获取连接时根本没有任何实际的数据库连接,直到第一次执行 JDBC 查询或更新操作时,才真正创建实际的 JDBC 连接。

保护代理

保护代理即 Protection Proxy,它用代理对象控制对原始对象的访问,常用于鉴权。

智能引用

智能引用即 Smart Reference,它也是一种代理对象,如果有很多客户端对它进行访问,通过内部的计数器可以在外部调用者都不使用后自动释放它。

我们来看一下如何应用代理模式编写一个 JDBC 连接池(DataSource)。我们首先来编写一个虚代理,即如果调用者获取到 Connection 后,并没有执行任何 SQL 操作,那么这个 Connection Proxy 实际上并不会真正打开 JDBC 连接。调用者代码如下:

DataSource lazyDataSource = new LazyDataSource(jdbcUrl, jdbcUsername, jdbcPassword);
System.out.println("get lazy connection...");
try (Connection conn1 = lazyDataSource.getConnection()) {// 并没有实际打开真正的 Connection
}
System.out.println("get lazy connection...");
try (Connection conn2 = lazyDataSource.getConnection()) {try (PreparedStatement ps = conn2.prepareStatement("SELECT * FROM students")) {// 打开了真正的 Connection
        try (ResultSet rs = ps.executeQuery()) {while (rs.next()) {System.out.println(rs.getString("name"));
            }
        }
    }
}

现在我们来思考如何实现这个 LazyConnectionProxy。为了简化代码,我们首先针对Connection 接口做一个抽象的代理类:

public abstract class AbstractConnectionProxy implements Connection {// 抽象方法获取实际的 Connection:
    protected abstract Connection getRealConnection();

    // 实现 Connection 接口的每一个方法:
    public Statement createStatement() throws SQLException {return getRealConnection().createStatement();
    }

    public PreparedStatement prepareStatement(String sql) throws SQLException {return getRealConnection().prepareStatement(sql);
    }

    ... 其他代理方法...
}

这个 AbstractConnectionProxy 代理类的作用是把 Connection 接口定义的方法全部实现一遍,因为 Connection 接口定义的方法太多了,后面我们要编写的 LazyConnectionProxy 只需要继承 AbstractConnectionProxy,就不必再把Connection 接口方法挨个实现一遍。

LazyConnectionProxy实现如下:

public class LazyConnectionProxy extends AbstractConnectionProxy {private Supplier<Connection> supplier;
    private Connection target = null;

    public LazyConnectionProxy(Supplier<Connection> supplier) {this.supplier = supplier;
    }

    // 覆写 close 方法:只有 target 不为 null 时才需要关闭:
    public void close() throws SQLException {if (target != null) {System.out.println("Close connection:" + target);
            super.close();}
    }

    @Override
    protected Connection getRealConnection() {if (target == null) {target = supplier.get();
        }
        return target;
    }
}

如果调用者没有执行任何 SQL 语句,那么 target 字段始终为 null。只有第一次执行 SQL 语句时(即调用任何类似prepareStatement() 方法时,触发 getRealConnection() 调用),才会真正打开实际的 JDBC Connection。

最后,我们还需要编写一个 LazyDataSource 来支持这个LazyConnectionProxy

public class LazyDataSource implements DataSource {private String url;
    private String username;
    private String password;

    public LazyDataSource(String url, String username, String password) {this.url = url;
        this.username = username;
        this.password = password;
    }

    public Connection getConnection(String username, String password) throws SQLException {return new LazyConnectionProxy(() -> {try {Connection conn = DriverManager.getConnection(url, username, password);
                System.out.println("Open connection:" + conn);
                return conn;
            } catch (SQLException e) {throw new RuntimeException(e);
            }
        });
    }
    ...
}

我们执行代码,输出如下:

get lazy connection...
get lazy connection...
Open connection: com.mysql.jdbc.JDBC4Connection@7a36aefa
小明
小红
小军
小白
...
Close connection: com.mysql.jdbc.JDBC4Connection@7a36aefa

可见第一个 getConnection() 调用获取到的 LazyConnectionProxy 并没有实际打开真正的 JDBC Connection。

使用连接池的时候,我们更希望能重复使用连接。如果调用方编写这样的代码:

DataSource pooledDataSource = new PooledDataSource(jdbcUrl, jdbcUsername, jdbcPassword);
try (Connection conn = pooledDataSource.getConnection()) {
}
try (Connection conn = pooledDataSource.getConnection()) {// 获取到的是同一个 Connection
}
try (Connection conn = pooledDataSource.getConnection()) {// 获取到的是同一个 Connection
}

调用方并不关心是否复用了 Connection,但从PooledDataSource 获取的 Connection 确实自带这个优化功能。如何实现可复用 Connection 的连接池?答案仍然是使用代理模式。

public class PooledConnectionProxy extends AbstractConnectionProxy {// 实际的 Connection:
    Connection target;
    // 空闲队列:
    Queue<PooledConnectionProxy> idleQueue;

    public PooledConnectionProxy(Queue<PooledConnectionProxy> idleQueue, Connection target) {this.idleQueue = idleQueue;
        this.target = target;
    }

    public void close() throws SQLException {System.out.println("Fake close and released to idle queue for future reuse:" + target);
        // 并没有调用实际 Connection 的 close()方法,
        // 而是把自己放入空闲队列:
        idleQueue.offer(this);
    }

    protected Connection getRealConnection() {return target;
    }
}

复用连接的关键在于覆写 close() 方法,它并没有真正关闭底层 JDBC 连接,而是把自己放回一个空闲队列,以便下次使用。

空闲队列由 PooledDataSource 负责维护:

public class PooledDataSource implements DataSource {private String url;
    private String username;
    private String password;

    // 维护一个空闲队列:
    private Queue<PooledConnectionProxy> idleQueue = new ArrayBlockingQueue<>(100);

    public PooledDataSource(String url, String username, String password) {this.url = url;
        this.username = username;
        this.password = password;
    }

    public Connection getConnection(String username, String password) throws SQLException {// 首先试图获取一个空闲连接:
        PooledConnectionProxy conn = idleQueue.poll();
        if (conn == null) {// 没有空闲连接时,打开一个新连接:
            conn = openNewConnection();} else {System.out.println("Return pooled connection:" + conn.target);
        }
        return conn;
    }

    private PooledConnectionProxy openNewConnection() throws SQLException {Connection conn = DriverManager.getConnection(url, username, password);
        System.out.println("Open new connection:" + conn);
        return new PooledConnectionProxy(idleQueue, conn);
    }
    ...
}

我们执行调用方代码,输出如下:

Open new connection: com.mysql.jdbc.JDBC4Connection@61ca2dfa
Fake close and released to idle queue for future reuse: com.mysql.jdbc.JDBC4Connection@61ca2dfa
Return pooled connection: com.mysql.jdbc.JDBC4Connection@61ca2dfa
Fake close and released to idle queue for future reuse: com.mysql.jdbc.JDBC4Connection@61ca2dfa
Return pooled connection: com.mysql.jdbc.JDBC4Connection@61ca2dfa
Fake close and released to idle queue for future reuse: com.mysql.jdbc.JDBC4Connection@61ca2dfa

除了第一次打开了一个真正的 JDBC Connection,后续获取的 Connection 实际上是同一个 JDBC Connection。但是,对于调用方来说,完全不需要知道底层做了哪些优化。

我们实际使用的 DataSource,例如 HikariCP,都是基于代理模式实现的,原理同上,但增加了更多的如动态伸缩的功能(一个连接空闲一段时间后自动关闭)。

有的童鞋会发现 Proxy 模式和 Decorator 模式有些类似。确实,这两者看起来很像,但区别在于:Decorator 模式让调用者自己创建核心类,然后组合各种功能,而 Proxy 模式决不能让调用者自己创建再组合,否则就失去了代理的功能。Proxy 模式让调用者认为获取到的是核心类接口,但实际上是代理类。

练习

使用代理模式编写一个 JDBC 连接池。

下载练习

小结

代理模式通过封装一个已有接口,并向调用方返回相同的接口类型,能让调用方在不改变任何代码的前提下增强某些功能(例如,鉴权、延迟加载、连接池复用等)。

使用 Proxy 模式要求调用方持有接口,作为 Proxy 的类也必须实现相同的接口类型。

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-08-05发表,共计6616字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中

星哥玩云

星哥玩云
星哥玩云
分享互联网知识
用户数
4
文章数
19351
评论数
4
阅读量
7978909
文章搜索
热门文章
星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛 NAS-6:抖音视频同步工具,视频下载自动下载保存 前言 各位玩 NAS 的朋友好,我是星哥!...
星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛 NAS-3:安装飞牛 NAS 后的很有必要的操作 前言 如果你已经有了飞牛 NAS 系统,之前...
我把用了20年的360安全卫士卸载了

我把用了20年的360安全卫士卸载了

我把用了 20 年的 360 安全卫士卸载了 是的,正如标题你看到的。 原因 偷摸安装自家的软件 莫名其妙安装...
再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见 zabbix!轻量级自建服务器监控神器在 Linux 的完整部署指南 在日常运维中,服务器监控是绕不开的...
飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛 NAS 中安装 Navidrome 音乐文件中文标签乱码问题解决、安装 FntermX 终端 问题背景 ...
阿里云CDN
阿里云CDN-提高用户访问的响应速度和成功率
随机文章
星哥带你玩飞牛NAS-16:飞牛云NAS换桌面,fndesk图标管理神器上线!

星哥带你玩飞牛NAS-16:飞牛云NAS换桌面,fndesk图标管理神器上线!

  星哥带你玩飞牛 NAS-16:飞牛云 NAS 换桌面,fndesk 图标管理神器上线! 引言 哈...
星哥带你玩飞牛NAS-16:不再错过公众号更新,飞牛NAS搭建RSS

星哥带你玩飞牛NAS-16:不再错过公众号更新,飞牛NAS搭建RSS

  星哥带你玩飞牛 NAS-16:不再错过公众号更新,飞牛 NAS 搭建 RSS 对于经常关注多个微...
星哥带你玩飞牛NAS硬件02:某鱼6张左右就可拿下5盘位的飞牛圣体NAS

星哥带你玩飞牛NAS硬件02:某鱼6张左右就可拿下5盘位的飞牛圣体NAS

星哥带你玩飞牛 NAS 硬件 02:某鱼 6 张左右就可拿下 5 盘位的飞牛圣体 NAS 前言 大家好,我是星...
免费无广告!这款跨平台AI RSS阅读器,拯救你的信息焦虑

免费无广告!这款跨平台AI RSS阅读器,拯救你的信息焦虑

  免费无广告!这款跨平台 AI RSS 阅读器,拯救你的信息焦虑 在算法推荐主导信息流的时代,我们...
星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛 NAS-6:抖音视频同步工具,视频下载自动下载保存 前言 各位玩 NAS 的朋友好,我是星哥!...

免费图片视频管理工具让灵感库告别混乱

一言一句话
-「
手气不错
12.2K Star 爆火!开源免费的 FileConverter:右键一键搞定音视频 / 图片 / 文档转换,告别多工具切换

12.2K Star 爆火!开源免费的 FileConverter:右键一键搞定音视频 / 图片 / 文档转换,告别多工具切换

12.2K Star 爆火!开源免费的 FileConverter:右键一键搞定音视频 / 图片 / 文档转换...
每天一个好玩的网站-手机博物馆-CHAZ 3D Experience

每天一个好玩的网站-手机博物馆-CHAZ 3D Experience

每天一个好玩的网站 - 手机博物馆 -CHAZ 3D Experience 一句话介绍:一个用 3D 方式重温...
恶意团伙利用 PHP-FPM 未授权访问漏洞发起大规模攻击

恶意团伙利用 PHP-FPM 未授权访问漏洞发起大规模攻击

恶意团伙利用 PHP-FPM 未授权访问漏洞发起大规模攻击 PHP-FPM(FastCGl Process M...
手把手教你,购买云服务器并且安装宝塔面板

手把手教你,购买云服务器并且安装宝塔面板

手把手教你,购买云服务器并且安装宝塔面板 前言 大家好,我是星哥。星哥发现很多新手刚接触服务器时,都会被“选购...
星哥带你玩飞牛NAS硬件03:五盘位+N5105+双网口的成品NAS值得入手吗

星哥带你玩飞牛NAS硬件03:五盘位+N5105+双网口的成品NAS值得入手吗

星哥带你玩飞牛 NAS 硬件 03:五盘位 +N5105+ 双网口的成品 NAS 值得入手吗 前言 大家好,我...