代理模式与动态代理思想 | 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 知识问答
- 什么是代理模式?
- 为什么对象会把部分职责交给代理?
- 代理对象、目标对象、接口分别承担什么职责?
- 为什么代理对象通常需要和目标对象拥有相同的抽象接口?
- 什么叫方法增强?
- 什么是横切关注点?
- 日志、权限、事务、耗时统计为什么适合代理处理?
- 静态代理最大的缺点是什么?
- 为什么静态代理的缺点会引出动态代理?
- 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");
}
}
回答:
- 谁是抽象主题?
- 谁是真实对象?
- 谁是代理对象?
target.work()为什么必须存在?- 如果客户端直接调用
ServiceImpl.work(),代理逻辑会不会执行? - 控制台输出顺序是什么?
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()
要求:
OrderServiceImpl只负责核心业务。- 编写
OrderServiceProxy。 - 所有方法执行前记录日志。
- 所有方法执行后记录耗时。
- 不允许把核心业务复制到代理类。
- 客户端只能通过
OrderService接口使用代理对象。
完成以后思考:
如果系统拥有 50 个 Service 接口,每个接口有 10 个方法,静态代理会产生什么问题?
不要立即使用动态代理解决,先总结静态代理的结构性缺陷。
7.6 本章验收
如果能够不看资料画出下面的调用链,即可认为掌握本章:
接口
↑
┌────┴────┐
│ │
代理对象 真实对象
│ ↑
│ │
└──调用───┘
以及:
客户端
↓
代理对象
↓
before 增强
↓
目标对象核心业务
↓
after 增强
↓
返回客户端
最终能够完整口述:
代理模式是在客户端与真实对象之间增加代理对象。代理和目标对象通常遵循相同的接口,代理负责控制访问、增加日志、权限、事务、性能统计等通用职责,并将真正的核心业务调用转发给目标对象。静态代理需要手写大量代理类,因此进一步引出了运行时自动生成代理类的动态代理技术。