什么是静态代理?它有什么优缺点?
简化版
静态代理是在编译期手写代理类,让代理类和目标类实现同一个接口,并在代理类中调用目标对象。它优点是简单直观,缺点是每个接口或方法都可能要写代理类,扩展成本高。
详细版
静态代理通常有三个角色:
- 接口:代理和目标共同实现;
- 目标类:真正执行业务;
- 代理类:持有目标对象,并在调用前后增强。
示例:
interface OrderService {
void createOrder();
}
class OrderServiceImpl implements OrderService {
public void createOrder() {
System.out.println("创建订单");
}
}
class OrderServiceProxy implements OrderService {
private final OrderService target;
OrderServiceProxy(OrderService target) {
this.target = target;
}
public void createOrder() {
System.out.println("开启事务");
target.createOrder();
System.out.println("提交事务");
}
}
优点:
- 代码直观,容易理解;
- 不依赖反射和字节码增强;
- 编译期类型安全;
- 适合少量接口、固定增强逻辑。
缺点:
- 每个被代理类都要写代理类;
- 接口方法变动时,代理类也要跟着改;
- 代理逻辑重复,维护成本高;
- 不适合大规模横切增强。
所以静态代理适合理解代理模式本质,真实项目里大规模增强通常会使用动态代理或 AOP。
完整版教学
一、静态代理为什么叫“静态”
“静态”指代理类在编译前就已经写好了。也就是说,源码里真实存在一个 OrderServiceProxy 类。
它和动态代理的区别是:动态代理的代理类通常在运行期生成,而静态代理是开发者手写的。
静态代理的调用链很清楚:
客户端 -> OrderServiceProxy -> OrderServiceImpl
客户端面向接口:
OrderService service = new OrderServiceProxy(new OrderServiceImpl());
service.createOrder();
这段代码的好处是调用方不需要知道增强细节,只要调用 OrderService 接口即可。
二、静态代理解决了什么问题
假设没有代理,事务逻辑可能写在业务方法里:
public void createOrder() {
beginTransaction();
doCreateOrder();
commit();
}
这样业务方法混入了事务控制。多个方法都要事务时,会出现大量重复代码。
静态代理可以把事务逻辑移到代理类:
public void createOrder() {
beginTransaction();
target.createOrder();
commit();
}
真实对象只做业务:
public void createOrder() {
System.out.println("创建订单");
}
这就是代理模式的典型价值:把横切逻辑从业务类中拆出来。
三、静态代理的结构为什么要求共同接口
代理对象要让客户端“感觉像真实对象”,所以通常要和真实对象实现同一个接口。
class OrderServiceProxy implements OrderService
class OrderServiceImpl implements OrderService
这样客户端可以写:
OrderService service = proxy;
如果没有共同接口,客户端就要知道具体代理类和真实类的差异,代理的透明性就差了。
共同接口也让代理能够替换真实对象,这是面向接口编程的重要体现。
四、静态代理的主要问题
静态代理的问题不是“不能用”,而是“不适合规模化”。
如果有 100 个 Service,每个 Service 都要加日志,你难道手写 100 个 Proxy 类吗?
如果接口新增一个方法,代理类也要补这个方法。否则编译不过,或者增强逻辑缺失。
比如:
interface OrderService {
void createOrder();
void cancelOrder();
}
代理类也必须实现两个方法。接口越多,重复越多。
所以静态代理的核心缺点是代理类数量和维护成本随业务规模增长。
五、静态代理适合什么场景
静态代理适合:
- 代理对象数量少;
- 代理逻辑比较稳定;
- 希望代码显式、易调试;
- 教学或理解代理模式原理;
- 对性能和可控性要求较高的小范围增强。
如果要给大量类统一加事务、日志、权限,动态代理或 AOP 更合适。
六、静态代理的编译期结构
若 8 个服务各有 4 个方法,逐个手写日志代理可能产生 32 个转发方法;接口变化还要同步修改真实类和代理类。
compile -> UserServiceProxy.class -> target.createUser()
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | “静态”指代理类在编译期已经存在,不是指它只能代理 static 方法。 |
| 适用边界 | 静态代理适合接口少、增强规则明确的场景;横切面覆盖几十个接口时,维护成本会快速增长。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:“静态”指代理类在编译期已经存在,不是指它只能代理 static 方法。
八、常见误区与追问
- 误区:静态代理只能增强静态方法。 这里的静态描述生成时机;实例方法同样可以由手写代理转发。
- 误区:代理类存在,就代表所有调用都会经过代理。 只有客户端持有并调用代理引用时增强才生效;绕过代理直接调用目标对象,调用链自然不会出现增强。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:静态代理比动态代理更落后吗? 不是;它类型直观、易调试,在小范围或需要精确控制时反而更合适。
- 追问:代理模式和装饰器模式能只靠类图区分吗? 不能;两者都可能包装同一接口,必须结合“控制访问”还是“叠加职责”的设计意图区分。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
静态代理就是手写一个“中间人”:它和目标类实现同一个接口,调用目标前后加增强。它清楚、稳定、好理解,但类多了会很累,所以是理解代理模式的入口,不是大规模横切增强的最佳方案。