如何用代理模式实现缓存、权限和日志增强?
简化版
代理模式可以在调用目标对象前后加入缓存、权限、日志等增强逻辑:调用前做权限校验或缓存命中判断,调用后记录日志或写入缓存。这样目标类只负责业务,横切逻辑集中在代理层。
详细版
以查询用户为例,目标接口:
interface UserService {
User getUser(Long id);
}
真实对象:
class UserServiceImpl implements UserService {
public User getUser(Long id) {
return queryFromDatabase(id);
}
}
代理对象可以做增强:
class UserServiceProxy implements UserService {
private final UserService target;
private final Map<Long, User> cache = new HashMap<>();
UserServiceProxy(UserService target) {
this.target = target;
}
public User getUser(Long id) {
checkPermission();
System.out.println("查询用户:" + id);
if (cache.containsKey(id)) {
return cache.get(id);
}
User user = target.getUser(id);
cache.put(id, user);
return user;
}
}
代理层可以统一处理:
- 权限:调用前检查当前用户是否有权限;
- 缓存:命中缓存直接返回,未命中再调用目标对象;
- 日志:记录调用参数、结果、耗时和异常;
- 监控:统计成功率、延迟、异常数;
- 限流:调用前判断是否超过阈值。
注意:这些增强逻辑不能破坏目标方法语义。例如缓存要考虑过期、更新一致性、空值缓存和并发击穿;权限校验要避免绕过代理直接访问目标对象。
完整版教学
一、代理增强适合处理什么逻辑
代理适合处理“围绕业务方法发生,但不是业务本身”的逻辑。
比如 getUser(id) 的业务目标是查询用户,但系统还需要:
- 检查当前用户能不能查;
- 记录谁查了谁;
- 缓存热点用户;
- 统计查询耗时;
- 异常时记录告警。
这些逻辑和用户查询相关,但它们不是“查询用户”的核心业务规则。放在代理层,会比散落在业务方法里更清楚。
二、缓存代理的设计要点
缓存代理的基本流程是:
收到请求
↓
生成缓存 key
↓
查缓存
↓
命中则返回
↓
未命中调用目标对象
↓
结果写缓存
↓
返回结果
示例:
public User getUser(Long id) {
String key = "user:" + id;
User cached = cache.get(key);
if (cached != null) {
return cached;
}
User user = target.getUser(id);
cache.put(key, user);
return user;
}
但真实项目不能只写到这里,还要考虑:
- 缓存过期时间;
- 数据更新时如何失效;
- 空值是否缓存,避免缓存穿透;
- 高并发未命中时如何防击穿;
- 缓存 key 是否包含租户、权限、语言等上下文。
缓存代理是很典型的代理模式,但它背后的工程问题不少。
三、权限代理的设计要点
权限代理的基本流程是:
调用目标方法前 -> 判断身份和权限 -> 允许则继续 -> 拒绝则抛异常或返回错误
示例:
public User getUser(Long id) {
if (!securityContext.canReadUser(id)) {
throw new AccessDeniedException("no permission");
}
return target.getUser(id);
}
权限逻辑放代理层的好处是统一。否则每个业务方法都手写权限判断,容易遗漏。
但要注意,权限代理必须成为访问入口。如果调用方能绕过代理直接拿到目标对象,权限就可能失效。
这也是 Spring 中通常通过容器注入代理对象,而不是手动 new 目标对象的原因之一。
四、日志和监控代理的设计要点
日志和监控通常需要处理成功、失败和耗时:
long start = System.currentTimeMillis();
try {
Object result = target.call();
logSuccess(result);
return result;
} catch (Exception e) {
logError(e);
throw e;
} finally {
recordCost(System.currentTimeMillis() - start);
}
这类逻辑非常适合代理,因为它对很多方法都一样。
如果每个业务方法都写一遍:
long start = ...
try { ... } finally { ... }
代码会很重复,也容易记录不一致。
五、代理增强的边界
代理层不要承载复杂业务决策。
比如权限校验可以放代理,但订单状态流转规则不应该塞进通用代理里。缓存读取可以放代理,但数据一致性的核心业务规则仍然应该清楚地设计在业务层或数据层。
代理层更适合通用增强,不适合替代领域模型。
六、多种增强的顺序与职责
一次读请求命中缓存只需 2 ms,未命中则还要访问数据库 40 ms;若权限校验放在缓存之后,未授权用户可能在 2 ms 内拿到不该看到的数据。
request -> auth -> cache lookup -> target on miss -> cache write -> audit
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | 增强顺序本身就是安全与正确性规则:通常先鉴权,再决定是否读取或写入缓存。 |
| 适用边界 | 缓存键、权限粒度、日志脱敏和异常传播必须分别设计,不能因为都在代理层就揉成一个巨型拦截器。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:增强顺序本身就是安全与正确性规则:通常先鉴权,再决定是否读取或写入缓存。
八、常见误区与追问
- 误区:缓存命中后就可以跳过权限校验。 除非缓存按安全主体隔离且有明确证明,否则先返回缓存可能形成越权。
- 误区:代理类存在,就代表所有调用都会经过代理。 只有客户端持有并调用代理引用时增强才生效;绕过代理直接调用目标对象,调用链自然不会出现增强。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:缓存代理怎样避免击穿目标对象? 可使用单飞、互斥或逻辑过期,并同时设计超时和失败降级。
- 追问:代理模式和装饰器模式能只靠类图区分吗? 不能;两者都可能包装同一接口,必须结合“控制访问”还是“叠加职责”的设计意图区分。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
缓存、权限、日志这些逻辑都属于“访问目标对象时顺手要做的事”,所以适合放代理。代理负责守门、记录、加速和观测,目标对象负责真正业务;边界别混,代码才不会变成一锅粥。