代理模式与动态代理思想 | JavaSE

代理模式与动态代理思想

一、学习目标

完成本章后,你应该能够:

  • 能够解释代理模式(Proxy Pattern)解决的核心问题。
  • 能够区分目标对象、代理对象、接口三种角色。
  • 能够理解“客户端访问代理,代理再调用真实对象”的调用链。
  • 能够手写一个最基础的静态代理。
  • 能够解释为什么日志、权限、事务、耗时统计等功能适合交给代理处理。
  • 能够从静态代理的局限推导出动态代理存在的必要性。
  • 能够理解 JDK 动态代理的大致思想,为下一章学习 Proxy.newProxyInstance 做准备。

二、核心知识

2.1 为什么需要代理

假设现在有一个明星:

public class RealStar {

    public void sing() {
        System.out.println("开始唱歌");
    }
}

明星真正擅长的是:

唱歌
跳舞
表演

但一次商业演出还需要:

谈合同
↓
收钱
↓
准备场地
↓
准备话筒
↓
安排流程
↓
表演
↓
结算

如果全部让明星自己完成:

public void sing() {

    System.out.println("谈合同");

    System.out.println("收钱");

    System.out.println("准备话筒");

    System.out.println("真正开始唱歌");

    System.out.println("结算");
}

业务对象就会承担大量本来不属于核心业务的职责。

于是可以加入:

代理对象(Proxy Object)

代理负责:

准备工作
↓
调用真实对象
↓
收尾工作

真实对象只负责:

核心业务

2.2 什么是代理模式

代理模式可以理解为:

不让客户端直接访问真实对象,而是在客户端与真实对象之间增加一个代理对象,由代理控制、增强或转发对真实对象的访问。

原本:

客户端
↓
真实对象

加入代理后:

客户端
↓
代理对象
↓
真实对象

进一步:

客户端
   ↓
代理.before()
   ↓
真实对象.method()
   ↓
代理.after()

2.3 代理模式中的核心角色

通常可以抽象出三个角色。

1. 抽象主题

例如:

public interface Star {

    void sing(String song);
}

规定:

这一类对象能够做什么。


2. 真实主题

public class RealStar implements Star {

    @Override
    public void sing(String song) {
        System.out.println("明星演唱:" + song);
    }
}

真正负责核心业务。

也叫:

  • Target
  • Real Subject
  • 被代理对象
  • 目标对象

3. 代理对象

public class StarProxy implements Star {

    private final Star target;

    public StarProxy(Star target) {
        this.target = target;
    }

    @Override
    public void sing(String song) {

        System.out.println("准备话筒");

        target.sing(song);

        System.out.println("演出结算");
    }
}

代理也实现:

Star

因此对于客户端而言:

Star star

既可以指向:

RealStar

也可以指向:

StarProxy

2.4 为什么代理和真实对象通常需要相同的抽象

客户端可能只知道:

Star star;

而不知道:

这个 Star 到底是真明星还是代理。

例如:

Star star = new StarProxy(
        new RealStar()
);

star.sing("星雨");

客户端只调用:

star.sing(...)

至于内部流程:

代理处理前置逻辑
↓
真实对象唱歌
↓
代理处理后置逻辑

客户端不需要知道。

这体现了:

面向接口编程。

同时也是多态的实际应用。


2.5 一个完整的静态代理案例

接口:

public interface Star {

    void sing(String song);
}

真实对象:

public class RealStar implements Star {

    @Override
    public void sing(String song) {
        System.out.println(
                "明星演唱:" + song
        );
    }
}

代理:

public class StarProxy implements Star {

    private final Star target;

    public StarProxy(Star target) {
        this.target = target;
    }

    @Override
    public void sing(String song) {

        System.out.println(
                "代理:准备话筒、确认合同"
        );

        target.sing(song);

        System.out.println(
                "代理:处理结算与收尾"
        );
    }
}

测试:

public class ProxyDemo {

    public static void main(String[] args) {

        Star target =
                new RealStar();

        Star proxy =
                new StarProxy(target);

        proxy.sing("星雨");
    }
}

调用链:

main
↓
proxy.sing()
↓
准备话筒、确认合同
↓
target.sing()
↓
明星演唱
↓
处理结算与收尾

这里:

target.sing()

非常关键。

代理最终并没有自己完成核心业务。

它只是:

增强 + 转发。


三、使用方法

3.1 第一步:提取统一接口

例如:

public interface UserService {

    void login();

    void deleteUser();
}

3.2 第二步:创建真实业务对象

public class UserServiceImpl
        implements UserService {

    @Override
    public void login() {
        System.out.println("执行登录业务");
    }

    @Override
    public void deleteUser() {
        System.out.println("执行删除用户业务");
    }
}

真实对象应该主要关注:

核心业务。


3.3 第三步:创建代理

public class UserServiceProxy
        implements UserService {

    private final UserService target;

    public UserServiceProxy(
            UserService target
    ) {
        this.target = target;
    }

    @Override
    public void login() {

        System.out.println(
                "记录日志:准备执行 login"
        );

        target.login();

        System.out.println(
                "记录日志:login 执行结束"
        );
    }

    @Override
    public void deleteUser() {

        System.out.println(
                "记录日志:准备执行 deleteUser"
        );

        target.deleteUser();

        System.out.println(
                "记录日志:deleteUser 执行结束"
        );
    }
}

3.4 第四步:客户端使用代理

UserService target =
        new UserServiceImpl();

UserService proxy =
        new UserServiceProxy(target);

proxy.login();

proxy.deleteUser();

以后应该建立这样的思维:

客户端
不要直接依赖具体实现

        ↓

接口

        ↓

代理

        ↓

真实对象

3.5 代理最重要的价值:增强

所谓:

增强(Enhancement)

就是在不直接修改核心业务代码的情况下增加额外逻辑。

例如原业务:

target.login();

需要增加耗时统计。

可以:

long start = System.nanoTime();

target.login();

long end = System.nanoTime();

System.out.println(end - start);

如果直接写入业务方法:

public void login() {

    long start = System.nanoTime();

    // 真正登录业务

    long end = System.nanoTime();
}

核心业务代码就和性能统计耦合了。

而代理可以:

代理
↓
开始计时
↓
调用目标对象
↓
停止计时
↓
输出结果

真实业务无需知道。


四、原理与进阶

4.1 什么叫横切关注点

假设系统有:

login()
register()
deleteUser()
updateUser()
queryUser()
pay()
createOrder()

很多方法都可能需要:

日志
权限
事务
性能统计
异常记录

这些功能并不属于某一个具体业务。

它们会“横向穿过”很多业务方法:

              登录
               │
日志 ──────────┼────
权限 ──────────┼────
事务 ──────────┼────
性能 ──────────┼────
               │
              删除

这样的职责通常称为:

横切关注点(Cross-cutting Concern)

代理非常适合处理这类问题。


4.2 代理并不是替代真实对象

错误理解:

代理把真实对象取代了。

正确理解:

代理包在真实对象外面。

代理内部通常仍然保存:

private final UserService target;

最终仍然调用:

target.login();

所以代理关系更加类似:

客户端
↓
代理
↓
目标对象

而不是:

客户端
↓
代理

目标对象消失

4.3 静态代理的问题

现在:

UserService

有:

login()
deleteUser()

代理就需要实现:

login()
deleteUser()

如果再增加:

updateUser()

代理也要增加:

updateUser()

再来:

OrderService
ProductService
PaymentService

可能需要:

UserServiceProxy
OrderServiceProxy
ProductServiceProxy
PaymentServiceProxy
...

你会发现大量代码结构几乎一样:

before();

target.xxx();

after();

问题来了:

能不能让 Java 在程序运行期间自动帮我们生成这种代理类?

这正是:

动态代理(Dynamic Proxy)

出现的重要原因之一。


4.4 静态代理与动态代理

| 对比 | 静态代理 | 动态代理 | | ---------------------- | ------------ | ------------------ | | 代理类什么时候产生 | 编译前手写 | 运行期间动态生成 | | 是否需要手写具体代理类 | 通常需要 | 通常不需要逐个手写 | | 方法多时重复代码 | 较多 | 可以统一处理 | | 通用性 | 较低 | 较高 | | 学习难度 | 简单 | 更高 | | 典型用途 | 理解代理思想 | 框架、通用增强 |

静态代理是理解动态代理的基础。


4.5 JDK 动态代理的大致思想

现在只建立模型,不展开 API。

JDK 可以在运行期间根据一组接口生成一个代理类。

例如:

Star 接口
   ↓
JDK 动态生成代理类
   ↓
代理类实现 Star
   ↓
得到代理对象

因此:

Star proxy

仍然可以:

proxy.sing();

但是调用实际上会被转交给一个统一的处理器。

概念流程:

proxy.sing()
      ↓
JDK 生成的代理方法
      ↓
统一调用处理器
      ↓
before
      ↓
target.sing()
      ↓
after

下一章会正式学习:

Proxy.newProxyInstance(...)

4.6 JDK 动态代理为什么和接口关系很大

JDK 动态代理生成的代理类会:

实现指定的一组接口。

例如目标对象:

class UserServiceImpl
        implements UserService

生成的代理对象同样可以表现为:

UserService

所以客户端可以:

UserService service = proxy;

然后:

service.login();

JDK 运行时生成的并不是:

UserServiceImpl

的普通子类代理。

它的核心模型是:

指定接口
↓
运行时生成实现这些接口的代理类
↓
得到代理对象

4.7 代理与装饰器模式有什么区别

两者结构可能很像:

对象包对象

但意图有所不同。

代理模式

重点:

控制访问。

例如:

  • 权限检查
  • 远程访问
  • 延迟加载
  • 日志
  • 事务
  • 安全控制

装饰器模式

重点:

动态增加职责或能力。

例如:

普通咖啡
↓
加牛奶
↓
加糖
↓
加奶油

结构可能相似,但设计目的不同。


4.8 代理与 AOP 的关系

以后学习 Spring 时会遇到:

AOP(Aspect-Oriented Programming,面向切面编程)

例如希望:

所有 Service 方法

执行之前:

记录日志

执行之后:

统计耗时

这是一种:

横切关注点统一处理

的思想。

动态代理是 Java 中实现这类能力的一种重要技术手段。

但不能简单写成:

AOP = 动态代理

更准确的是:

AOP 是一种编程思想,而代理是实现这种思想时常用的技术机制之一。


五、实践应用

5.1 日志

原业务:

service.login();

代理:

输出方法名
↓
输出参数
↓
调用真实业务
↓
输出返回值

5.2 性能统计

记录开始时间
↓
调用目标方法
↓
记录结束时间
↓
计算耗时

这样所有业务类就不需要重复编写:

System.nanoTime()

5.3 权限控制

例如:

调用 deleteUser()
↓
代理检查当前用户权限
↓
ADMIN
    ↓
允许调用目标方法

普通用户
    ↓
拒绝

目标对象可以继续只负责:

删除用户

5.4 事务管理

概念上:

代理
↓
开启事务
↓
目标对象执行
↓
成功 → 提交
失败 → 回滚

核心业务不必在每个方法中重复:

begin
commit
rollback

5.5 远程调用

客户端看起来可能只是:

userService.findUser(1);

但代理内部实际上可能:

序列化参数
↓
发送网络请求
↓
远程服务器执行
↓
接收响应
↓
转换成 Java 对象

这也是代理模式非常经典的应用。


六、常见问题

6.1 有代理以后还能不能直接调用目标对象?

当然可以:

target.login();

但这样会:

绕过代理。

如果日志、权限、事务等功能都位于代理中:

target.login();

就不会执行这些增强逻辑。

因此真正依赖代理增强的系统通常要求客户端访问:

proxy

而不是:

target

6.2 代理是不是一定要修改目标类?

不一定。

代理模式的重要价值之一就是:

尽可能在不侵入核心业务实现的情况下增加额外职责。


6.3 为什么静态代理会产生大量代码?

因为每一个接口方法通常都需要代理重新实现:

@Override
public void login() {
    before();
    target.login();
    after();
}

方法越多、业务接口越多:

重复代理代码

就越严重。

动态代理正是为了解决这种通用化问题。


6.4 JDK 动态代理是不是可以直接代理任何类?

不能简单这样理解。

JDK Proxy 这一套动态代理机制围绕:

接口

生成代理对象。

因此下一章学习 JDK 动态代理时,会重点理解:

Class<?>[] interfaces

为什么如此重要。


6.5 为什么真实对象和代理对象最好通过接口使用?

因为:

Star target

和:

Star proxy

都可以统一表示为:

Star

客户端无需依赖:

RealStar
StarProxy

具体类型。

这就是:

面向接口编程 + 多态。


6.6 代理里是不是可以写所有业务代码?

不建议。

代理更适合:

通用增强
横切职责
访问控制
调用转发

而真正的核心业务仍然应该位于:

target

中。

否则代理本身又会变成一个新的“大泥球”。


6.7 为什么有时候“明明用了代理”,增强却没执行?

首先检查客户端真正调用的是:

proxy.method()

还是:

target.method()

只有经过代理对象的调用,代理才有机会执行增强逻辑。

可以牢记:

调用经过代理
→ 能增强

调用绕过代理
→ 不能增强

七、练习与验收

7.1 知识问答

  1. 什么是代理模式?
  2. 为什么对象会把部分职责交给代理?
  3. 代理对象、目标对象、接口分别承担什么职责?
  4. 为什么代理对象通常需要和目标对象拥有相同的抽象接口?
  5. 什么叫方法增强?
  6. 什么是横切关注点?
  7. 日志、权限、事务、耗时统计为什么适合代理处理?
  8. 静态代理最大的缺点是什么?
  9. 为什么静态代理的缺点会引出动态代理?
  10. JDK 动态代理与接口之间大致是什么关系?

7.2 代码阅读

阅读:

interface Service {

    void work();
}

class ServiceImpl implements Service {

    @Override
    public void work() {
        System.out.println("核心业务");
    }
}

class ServiceProxy implements Service {

    private final Service target;

    ServiceProxy(Service target) {
        this.target = target;
    }

    @Override
    public void work() {

        System.out.println("before");

        target.work();

        System.out.println("after");
    }
}

回答:

  1. 谁是抽象主题?
  2. 谁是真实对象?
  3. 谁是代理对象?
  4. target.work() 为什么必须存在?
  5. 如果客户端直接调用 ServiceImpl.work(),代理逻辑会不会执行?
  6. 控制台输出顺序是什么?

7.3 手写代码

定义:

interface Star

包含:

sing()
dance()

然后完成:

RealStar
↓
负责真正唱歌、跳舞

StarProxy
↓
负责准备场地、签合同、收钱等工作

要求客户端只通过:

Star

类型使用对象。


再实现:

UserService

包含:

login()
deleteUser()

编写静态代理,为两个方法增加:

方法执行前日志
↓
真实业务
↓
方法执行后日志

7.4 Debug

下面代码存在什么设计问题?

UserServiceImpl target =
        new UserServiceImpl();

UserService proxy =
        new UserServiceProxy(target);

target.login();

如果代理负责:

日志
权限
事务

这些逻辑是否会执行?

请修改代码。


再分析:

class UserServiceProxy {

    public void login() {

        System.out.println("before");

        System.out.println("真正执行登录业务");

        System.out.println("after");
    }
}

为什么这种设计并不是一个理想的代理?

应该怎样体现:

代理
↓
真实对象

的关系?

7.5 综合训练

设计一个订单业务:

OrderService

包含:

createOrder()
cancelOrder()
queryOrder()

要求:

  1. OrderServiceImpl 只负责核心业务。
  2. 编写 OrderServiceProxy
  3. 所有方法执行前记录日志。
  4. 所有方法执行后记录耗时。
  5. 不允许把核心业务复制到代理类。
  6. 客户端只能通过 OrderService 接口使用代理对象。

完成以后思考:

如果系统拥有 50 个 Service 接口,每个接口有 10 个方法,静态代理会产生什么问题?

不要立即使用动态代理解决,先总结静态代理的结构性缺陷。

7.6 本章验收

如果能够不看资料画出下面的调用链,即可认为掌握本章:

           接口
            ↑
       ┌────┴────┐
       │         │
     代理对象   真实对象
       │         ↑
       │         │
       └──调用───┘

以及:

客户端
↓
代理对象
↓
before 增强
↓
目标对象核心业务
↓
after 增强
↓
返回客户端

最终能够完整口述:

代理模式是在客户端与真实对象之间增加代理对象。代理和目标对象通常遵循相同的接口,代理负责控制访问、增加日志、权限、事务、性能统计等通用职责,并将真正的核心业务调用转发给目标对象。静态代理需要手写大量代理类,因此进一步引出了运行时自动生成代理类的动态代理技术。