Saul's blog Saul's blog
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)

Saul.J.Wu

立身之本,不在高低。
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)
  • Java入门基础

  • Java核心基础

  • 设计模式

    • 概念
    • 创建型模式

    • 结构型模式

      • 适配器
      • 桥接
      • 组合
      • 装饰器
      • 外观
      • 享元
      • 代理
        • 概念
        • 应用
          • 远程代理
          • 虚代理
          • 保护代理
          • 智能引用
        • 案例
        • 小结
    • 行为型模式

  • Web开发

  • SpringBoot

  • 微服务

  • Elasticsearch

  • 运维

  • 后端
  • 设计模式
  • 结构型模式
SaulJWu
2021-02-10

代理

# 概念

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

代理模式,即Proxy,它和Adapter模式很类似。

我们先回顾Adapter模式,它用于把A接口转换为B接口:

public BAdapter implements B {
    private A a;
    public BAdapter(A a) {
        this.a = a;
    }
    public void b() {
        a.a();
    }
}
1
2
3
4
5
6
7
8
9

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

public AProxy implements A {
    private A a;
    public AProxy(A a) {
        this.a = a;
    }
    public void a() {
        this.a.a();
    }
}
1
2
3
4
5
6
7
8
9

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

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

public void a() {
    this.a.a();
}
1
2
3

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

public void a() {
    if (getCurrentUser().isRoot()) {
        this.a.a();
    } else {
        throw new SecurityException("Forbidden");
    }
}
1
2
3
4
5
6
7

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

的童鞋会问,为啥不把权限检查的功能直接写到目标实例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"));
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

现在我们来思考如何实现这个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);
    }

    ...其他代理方法...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这个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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

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

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

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);
            }
        });
    }
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

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

get lazy connection...
get lazy connection...
Open connection: com.mysql.jdbc.JDBC4Connection@7a36aefa
小明
小红
小军
小白
...
Close connection: com.mysql.jdbc.JDBC4Connection@7a36aefa
1
2
3
4
5
6
7
8
9

可见第一个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
}
1
2
3
4
5
6
7
8
9

调用方并不关心是否复用了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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

复用连接的关键在于覆写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);
    }
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

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

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
1
2
3
4
5
6

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

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

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

# 小结

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

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

帮我改善此页面 (opens new window)
上次更新: 2021/02/16, 02:24:29
享元
责任链

← 享元 责任链→

最近更新
01
zabbix学习笔记二
02-28
02
zabbix学习笔记一
02-10
03
Linux访问不了github
12-08
更多文章>
Theme by Vdoing | Copyright © 2020-2022 Saul.J.Wu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式