结构型-代理(Proxy)

4/1/2022 设计模式

摘要

JDK:1.8.0_202

# 一:场景问题

# 1.1 访问多条数据

需要一次性展示某部门下的所有员工,只需要显示名字即可,必要的时候可以选择并查看某个员工的详细信息。

# 1.2 不用模式的解决方案

设计部门编号的时候,是按照层级来进行编码的,比如:上一级部门的编码为 "01",那么本级的编码就是 "0101"、"0102" … … 以此类推,下一级的编码就是 "010101"、"010102" … …

模拟数据库数据:

public class DB {

    public static List<String> selectAll = new ArrayList<>();

    /*
     * 模拟数据库中的USER表中所有列数据
     */
    static {
        selectAll.add("010101,user0001,张三1,男");
        selectAll.add("010101,user0002,张三2,男");
        selectAll.add("010102,user0003,张三3,男");
        selectAll.add("010201,user0004,张三4,男");
        selectAll.add("010201,user0005,张三5,男");
        selectAll.add("010202,user0006,张三6,男");
    }

    private DB() {

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

用户数据实体对象:

public class UserModel {

    /**
     * 用户Id
     */
    private String userId;

    /**
     * 姓名
     */
    private String name;

    /**
     * 部门id
     */
    private String depId;

    /**
     * 性别
     */
    private String sex;

    public UserModel(String userId, String name, String depId, String sex) {
        this.userId = userId;
        this.name = name;
        this.depId = depId;
        this.sex = sex;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDepId() {
        return depId;
    }

    public void setDepId(String depId) {
        this.depId = depId;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public static UserModel convert(String message) {
        String[] messages = message.split(",");
        return new UserModel(messages[1], messages[2], messages[0], messages[3]);
    }

    @Override
    public String toString() {
        return "UserModel{" +
                "userId='" + userId + '\'' +
                ", name='" + name + '\'' +
                ", depId='" + depId + '\'' +
                ", sex='" + sex + '\'' +
                '}';
    }
}
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

模拟数据库查询操作:

/**
 * 模拟操作数据库执行查询语句得出结果
 */
public class UserManager {

    private UserManager() {

    }

    /**
     * 通过 部门Id 查找数据库 得到 相关人员信息
     *
     * @param depId 部门Id
     * @return 人员信息
     */
    public static Collection<UserModel> getUserByDepId(String depId) {
        return DB.selectAll.stream().filter(e -> e.startsWith(depId)).map(UserModel::convert).collect(Collectors.toList());
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

客户端:

public class Client {

    public static void main(String[] args) {
        Collection<UserModel> temp = UserManager.getUserByDepId("0101");
        System.out.println("结果为:");
        temp.forEach(System.out::println);
    }

}
1
2
3
4
5
6
7
8
9

运行结果:

# 1.3 有何问题

上面的实现看起来很简单,功能也正确,但是蕴含一个较大的问题,那就是:当一次性访问的数据条数过多,而且每条描述的数据量又很大的话,那会消耗较多的内存

但是从客户使用角度来说,有很大的随机性,客户既可能访问每一条数据,也可能一条都不访问。也就是说,一次性访问很多条数据,消耗了大量内存,但是很可能是浪费掉了,客户根本就不会去访问那么多数据,对于每条数据,客户只需要看看姓名而已

那么该怎么实现,才能既把多条用户数据的姓名显示出来,而又能节省内存空间,当然还要实现在客户想要看到更多数据的时候,能正确访问到数据呢?

# 二:解决方案

用来解决上述问题的一个合理的解决方案就是代理模式。

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

# 2.1 解决思路

仔细分析上面的问题,一次性访问多条数据,这个可能性是很难避免的,是客户的需要。也就是说,要想节省内存,就不能从减少数据条数入手了,那就只能从减少每条数据的数据量上来考虑。

一个基本的思路如下:由于客户访问这多条用户数据的时候,基本上只需要看到用户的姓名,因此可以考虑刚开始从数据库查询返回的用户数据就只有用户编号和用户姓名,当客户想要详细查看某个用户的数据的时候,再次根据用户编号到数据库中获取完整的用户数据。这样一来,就可以在满足客户功能的前提下,大大减少对内存的消耗,只是每次需要重新查询一下数据库,算是一个以时间换空间的策略。

可是该如何来表示这个只有用户编号和姓名的对象呢?它还需要实现在必要的时候访问数据库去重新获取完整的用户数据

代理模式引入一个Proxy对象来解决这个问题,刚开始只有用户编号和姓名的时候,不是一个完整的用户对象,而是一个代理对象,当需要访问完整的用户数据的时候,代理会从数据库中重新获取相应的数据,通常情况下是当客户需要访问除了用户编号和姓名之外的数据的时候,代理才会重新去获取数据。

# 2.2 模式结构和说明

classDiagram Subject <.. Client Subject <|.. RealSubject Subject <|.. Proxy RealSubject<--*Proxy class Subject{ <<interface>> +request(String)* void } class RealSubject{ +request(String) void } class Proxy{ -RealSubject realSubject +Proxy(RealSubject) +request(String) void }
  • Proxy:代理对象,通常具有如下功能:
    1. 实现与具体的目标对象一样的接口,这样就可以使用代理来代替具体的目标对象
    2. 保存一个指向具体目标对象的引用,可以在需要的时候调用具体的目标对象,可以控制对具体目标对象的访问,并可能负责创建和删除它
  • Subject:目标接口,定义代理和具体目标对象的接口,这样就可以在任何使用具体目标对象的地方使用代理对象
  • RealSubject:具体的目标对象,真正实现目标接口要求的功能。

# 2.3 示例代码

目标接口:

/**
 * 抽象的目标接口,定义具体的目标对象和代理公用的接口
 */
public interface Subject {

    /**
     * 示意方法:一个抽象的请求方法
     */
    void request();

}
1
2
3
4
5
6
7
8
9
10
11

具体目标对象的实现:

/**
 * 具体的目标对象,是真正被代理的对象
 */
public class RealSubject implements Subject {

    @Override
    public void request() {
        // 执行具体的功能处理
    }

}
1
2
3
4
5
6
7
8
9
10
11

代理对象的实现:

/**
 * 代理对象
 */
public class Proxy implements Subject{

    /**
     * 持有被代理的具体的目标对象
     */
    private RealSubject realSubject = null;

    /**
     * 构造方法,传入被代理的具体的目标对象
     *
     * @param realSubject 被代理的具体的目标对象
     */
    public Proxy(RealSubject realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public void request() {
        // 在转调具体的目标对象前,可以执行一些功能处理

        // 转调具体的目标对象的方法
        realSubject.request();

        // 在转调具体的目标对象后,可以执行一些功能处理

    }
}
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

# 2.4 重写案例

要使用代理模式来重写示例,首先就需要为用户对象定义一个接口,然后实现相应的用户对象的代理,这样在使用用户对象的地方,就使用这个代理对象就可以了。

这个代理对象,在起初创建的时候,只需要装载用户编号和姓名这两个基本的数据,然后在客户需要访问除这两个属性外的数据的时候,才再次从数据库中查询并装载数据,从而达到节省内存的目的,因为如果用户不去访问详细的数据,那么那些数据就不需要被装载,那么对内存的消耗就会减少。

模拟数据库数据:

public class TestDB {

    public static List<String> selectPart = new ArrayList<>();

    public static List<String> selectOne = new ArrayList<>();

    /*
     * 模拟数据库中的USER表中所有列数据
     */
    static {
        selectPart.add("010101,user0001,张三1");
        selectPart.add("010101,user0002,张三2");
        selectPart.add("010102,user0003,张三3");
        selectPart.add("010201,user0004,张三4");
        selectPart.add("010201,user0005,张三5");
        selectPart.add("010202,user0006,张三6");

        selectOne.add("user0001,张三1,010101,男");
        selectOne.add("user0002,张三2,010101,男");
        selectOne.add("user0003,张三3,010102,男");
        selectOne.add("user0004,张三4,010201,男");
        selectOne.add("user0005,张三5,010201,男");
        selectOne.add("user0006,张三6,010202,男");
    }

    private TestDB() {
    }

}
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

模拟数据库实体:

public class UserModel implements UserModelApi {

    /**
     * 用户Id
     */
    private String userId;

    /**
     * 姓名
     */
    private String name;

    /**
     * 部门id
     */
    private String depId;

    /**
     * 性别
     */
    private String sex;

    public UserModel(String userId, String name) {
        this.userId = userId;
        this.name = name;
    }

    @Override
    public String getUserId() {
        return userId;
    }

    @Override
    public void setUserId(String userId) {
        this.userId = userId;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getDepId() {
        return depId;
    }

    @Override
    public void setDepId(String depId) {
        this.depId = depId;
    }

    @Override
    public String getSex() {
        return sex;
    }

    @Override
    public void setSex(String sex) {
        this.sex = sex;
    }

    public static UserModel convert(String message) {
        String[] messages = message.split(",");
        return new UserModel(messages[1], messages[2]);
    }

    @Override
    public String toString() {
        return "UserModel{" +
                "userId='" + userId + '\'' +
                ", name='" + name + '\'' +
                ", depId='" + depId + '\'' +
                ", sex='" + sex + '\'' +
                '}';
    }

}
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

代理对象:

public class Proxy implements UserModelApi {

    /**
     * 持有被代理的具体的目标对象
     */
    private UserModel realSubject = null;

    /**
     * 构造方法,传入被代理的具体的目标对象
     *
     * @param realSubject 被代理的具体的目标对象
     */
    public Proxy(UserModel realSubject) {
        this.realSubject = realSubject;
    }

    /**
     * 标示是否已经重新装载过数据了
     */
    private boolean loaded = false;

    @Override
    public String getUserId() {
        return realSubject.getUserId();
    }

    @Override
    public void setUserId(String userId) {
        realSubject.setUserId(userId);
    }

    @Override
    public String getName() {
        return realSubject.getName();
    }

    @Override
    public void setName(String name) {
        realSubject.setName(name);
    }

    @Override
    public String getDepId() {
        // 需要判断是否已经转载过了
        if (!this.loaded) {
            // 从数据库中重新加载
            reload();
            // 设置重新转载的标志为 true
            this.loaded = true;
        }
        return realSubject.getDepId();
    }

    @Override
    public void setDepId(String depId) {
        realSubject.setDepId(depId);
    }

    @Override
    public String getSex() {
        if (!this.loaded) {
            reload();
            this.loaded = true;
        }
        return realSubject.getSex();
    }

    @Override
    public void setSex(String sex) {
        realSubject.setSex(sex);
    }

    /**
     * 重新查询数据库以获取完整的用户数据
     */
    private void reload() {
        String temp = TestDB.selectOne.stream().filter(e -> e.startsWith(realSubject.getUserId() + ",")).findFirst().get();
        String[] user = temp.split(",");
        realSubject.setDepId(user[2]);
        realSubject.setSex(user[3]);
    }

    public static Proxy convert(String message) {
        return new Proxy(UserModel.convert(message));
    }

    @Override
    public String toString() {
        return "userId=" + getUserId() + ",name=" + getName() + ",depId=" + getDepId() + ",sex=" + getSex();
    }
}
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

模拟数据库操作:

从数据库查询值的时候,不需要全部获取了,只需要查询用户编号和姓名的数据就可以了;

把数据库中获取的值转变成对象的时候,创建的对象不再是UserModel,而是代理对象,而且设置值的时候,也不是全部都设置,只是设置用户编号和姓名两个属性的值;

/**
 * 实现示例要求的功能
 */
public class UserManager {

    private UserManager() {

    }

    /**
     * 根据部门编号来获取部门下的所有人员
     *
     * @param depId 部门编号
     * @return 部门下的所有人员
     */
    public static Collection<UserModelApi> getUserByDepId(String depId) {
        return TestDB.selectPart.stream().filter(e -> e.startsWith(depId)).map(Proxy::convert).collect(Collectors.toList());
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

客户端:

/**
 * 实现示例要求的功能
 */
public class UserManager {

    private UserManager() {

    }

    /**
     * 根据部门编号来获取部门下的所有人员
     *
     * @param depId 部门编号
     * @return 部门下的所有人员
     */
    public static Collection<UserModelApi> getUserByDepId(String depId) {
        return TestDB.selectPart.stream().filter(e -> e.startsWith(depId)).map(Proxy::convert).collect(Collectors.toList());
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

运行结果:

仔细查看上面的结果数据会发现,如果只是访问用户编号和用户姓名的数据,是不需要重新查询数据库的,只有当访问到这两个数据以外的数据时,才需要重新查询数据库以获得完整的数据。这样一来,如果客户不访问除这两个数据以外的数据,那么就不需要重新查询数据库,也就不需要装载那么多数据,从而节省内存。

# 2.5 1+N次查询

上面这种实现方式有一个潜在的问题,就是如果客户对每条用户数据都要求查看详细的数据的话,那么总的查询数据库的次数会是1+N次之多。

第一次查询,获取到N条数据的用户编号和姓名,然后展示给客户看。如果这个时候,客户对每条数据都点击查看详细信息的话,那么每一条数据都需要重新查询数据库,那么最后总的查询数据库的次数就是1+N次了。

从上面的分析可以看出,这种做法最合适的场景就是:客户大多数情况下只需要查看用户编号和姓名,而少量的数据需要查看详细数据。这样既节省了内存,又减少了操作数据库的次数。

看到这里,可能会有朋友想起,Hibernate这类ORM的框架,在Lazy Load的情况下,也存在1+N次查询的情况,原因就在于,Hibernate的Lazy Load就是使用代理来实现的。

# 三:模式讲解

# 3.1 认识代理模式

1. 代理模式的功能

代理模式是通过创建一个代理对象,用这个代理对象去代表真实的对象,客户端得到这个代理对象过后,对客户端没有什么影响,就跟得到了真实对象一样来使用。

当客户端操作这个代理对象的时候,实际上功能最终还是会由真实的对象来完成,只不过是通过代理操作的,也就是客户端操作代理,代理操作真正的对象。

正是因为有代理对象夹在客户端和被代理的真实对象中间,相当于一个中转,那么在中转的时候就有很多花招可以玩,比如:判断一下权限,如果没有足够的权限那就不给你中转了,等等。

2. 代理的分类

事实上代理又被分成多种,大致有如下一些:

  • 虚代理:根据需要来创建开销很大的对象,该对象只有在需要的时候才会被真正创建

  • 远程代理:用来在不同的地址空间上代表同一个对象,这个不同的地址空间可以是在本机,也可以在其它机器上,在Java里面最典型的就是RMI技术;

  • Copy-on-Write代理:在客户端操作的时候,只有对象确实改变了,才会真的拷贝(或克隆)一个目标对象,算是虚代理的一个分支;

  • 保护代理:控制对原始对象的访问,如果有需要,可以给不同的用户提供不同的访问权限,以控制他们对原始对象的访问;

  • Cache代理:为那些昂贵的操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果;

  • 防火墙代理:保护对象不被恶意用户访问和操作;

  • 同步代理:使多个用户能够同时访问目标对象而没有冲突;

  • 智能指引:在访问对象时执行一些附加操作,比如:对指向实际对象的引用计数、第一次引用一个持久对象时,将它装入内存等;

在这些代理类型中,最常见的是:虚代理、保护代理、远程代理和智能指引这几种。在Java中,远程代理的典型体现是RMI技术。

3. 虚代理的示例

前面的例子就是一个典型的虚代理的实现。

起初每个代理对象只有用户编号和姓名的数据,直到需要的时候,才会把整个用户的数据装载到内存中来。

也就是说,要根据需要来装载整个UserModel的数据,虽然用户数据对象是前面已经创建好了的,但是只有用户编号和姓名的数据,可以看成是一个 "虚" 的对象,直到通过代理把所有的数据都设置好,才算是一个完整的用户数据对象。

4. Copy-on-Write

拷贝一个大的对象是很消耗资源的,如果这个被拷贝的对象从上次操作以来,根本就没有被修改过,那么再拷贝这个对象是没有必要的,白白消耗资源而已。那么就可以使用代理来延迟拷贝的过程,可以等到对象被修改的时候才真的对它进行拷贝。

Copy-on-Write可以大大降低拷贝大对象的开销,因此它算是一种优化方式,可以根据需要来拷贝或者克隆对象。

5. 具体目标和代理的关系

从代理模式的结构图来看,好像是有一个具体目标类就有一个代理类,其实不是这样的。如果代理类能完全通过接口来操作它所代理的目标对象,那么代理对象就不需要知道具体的目标对象,这样就无须为每一个具体目标类都创建一个代理类了

但是,如果代理类必须要实例化它代理的目标对象,那么代理类就必须知道具体被代理的对象,这种情况下,一个具体目标类通常会有一个代理类。这种情况多出现在虚代理的实现里面。

# 3.2 静动态代理

静动态代理

# 3.3 优缺点

代理模式在客户和被客户访问的对象之间,引入了一定程度的间接性,客户是直接使用代理,让代理来与被访问的对象进行交互。不同的代理类型,这种附加的间接性有不同的用途,也就是有不同的特点:

  • 远程代理:隐藏了一个对象存在于不同的地址空间的事实,也即是客户通过远程代理去访问一个对象,根本就不关心这个对象在哪里,也不关心如何通过网络去访问到这个对象,从客户的角度来讲,它只是在使用代理对象而已。

  • 虚代理:可以根据需要来创建 "大" 对象,只有到必须创建对象的时候,虚代理才会创建对象,从而大大加快程序运行速度,并节省资源。通过虚代理可以对系统进行优化。

  • 保护代理:可以在访问一个对象的前后,执行很多附加的操作,除了进行权限控制之外,还可以进行很多跟业务相关的处理,而不需要修改被代理的对象。也就是说,可以通过代理来给目标对象增加功能。

  • 智能指引:跟保护代理类似,也是允许在访问一个对象的前后,执行很多附加的操作,这样一来就可以做很多额外的事情,比如:引用计数等。

# 3.4 思考代理模式

1. 代理模式的本质

代理模式的本质:控制对象访问

代理模式通过代理目标对象,把代理对象插入到客户和目标对象之间,从而为客户和目标对象引入一定的间接性,正是这个间接性,给了代理对象很多的活动空间,代理对象可以在调用具体的目标对象前后,附加很多操作,从而实现新的功能或是扩展目标对象的功能,更狠的是,代理对象还可以不去创建和调用目标对象,也就是说,目标对象被完全代理掉了,或是被替换掉了。

从实现上看,代理模式主要是使用对象的组合和委托,尤其是在静态代理的实现里面,会看得更清楚。但是也可以采用对象继承的方式来实现代理,这种实现方式在某些情况下,比使用对象组合还要来得简单。

2. 何时选用代理模式

建议在如下情况中,选用代理模式:

需要为一个对象在不同的地址空间提供局部代表的时候,可以使用远程代理;

需要按照需要创建开销很大的对象的时候,可以使用虚代理;

需要控制对原始对象的访问的时候,可以使用保护代理;

需要在访问对象的时候执行一些附加操作的时候,可以使用智能指引代理;

# 3.5 相关模式

1. 代理模式和适配器模式

这两个模式有一定的相似性,但也有差异。

这两个模式有相似性,它们都为另一个对象提供间接性的访问,而且都是从自身以外的一个接口向这个对象转发请求。

但是从功能上,两个模式是不一样的。适配器模式主要用来解决接口之间不匹配的问题,它通常是为所适配的对象提供一个不同的接口;而代理模式会实现和目标对象相同的接口。

2. 代理模式和装饰模式

这两个模式从实现上相似,但是功能上是不同的。

装饰模式的实现和保护代理的实现上是类似的,都是在转调其它对象的前后执行一定的功能。但是它们的目的和功能都是不同的。

装饰模式的目的是为了让你不生成子类就可以给对象添加职责,也就是为了动态的增加功能;而代理模式的主要目的是控制对对象的访问

# 四:JDK

# 五:参考文献

最后更新: 10/19/2022, 12:31:23 AM