代理模式:接口访问的守门人
代理模式,说白了就是给一个对象找个替身。客户端跟替身打交道,替身背后再跟真正的对象沟通。我刚开始接触这个模式时觉得多此一举,直到有一次线上事故让我彻底改观——一个未做访问控制的远程接口被恶意调用,直接把数据库打崩了。嗯,从那以后,代理模式就成了我接口设计里的标配。
代理模式原理
代理模式的核心思想很简单:控制对目标对象的访问。代理对象和目标对象实现同一个接口,客户端只看到代理,看不到背后的真实对象。
核心角色:
- Subject(主题接口):定义真实对象和代理的共同行为
- RealSubject(真实主题):真正干活的业务对象
- Proxy(代理):持有真实对象的引用,在调用前后插入额外逻辑
我习惯把代理理解成一个拦截层。客户端调用代理的方法,代理可以在调用前做权限检查、日志记录、参数校验,调用后做结果缓存、异常处理。这些逻辑完全不影响真实对象的代码。
静态代理与动态代理
静态代理和动态代理的区别,说白了就是一个手写,一个自动生成。
静态代理
静态代理就是手动写一个代理类,实现和目标类相同的接口。每个方法都要手动转发,代码量很大。
// 接口
public interface ApiService {
String call(String param);
}
// 真实对象
public class RealApiService implements ApiService {
public String call(String param) {
return "Result: " + param;
}
}
// 静态代理
public class ApiServiceProxy implements ApiService {
private RealApiService realService;
public ApiServiceProxy() {
this.realService = new RealApiService();
}
public String call(String param) {
// 前置处理:权限校验
if (!checkAccess(param)) {
throw new SecurityException("Access denied");
}
// 调用真实对象
String result = realService.call(param);
// 后置处理:日志记录
log("call result: " + result);
return result;
}
private boolean checkAccess(String param) { /* ... */ }
private void log(String msg) { /* ... */ }
}
静态代理的痛点:
- 每个接口都要写一个代理类,接口一多就爆炸
- 接口新增方法,代理类必须同步修改
- 我曾经维护过一个项目,30多个接口对应30多个代理类,改一个权限逻辑要改30个文件
动态代理
动态代理用 Java 的 Proxy 类或 CGLib 在运行时生成代理类。你只需要写一个 InvocationHandler,所有方法调用都经过它。
// 动态代理处理器
public class AccessControlHandler implements InvocationHandler {
private Object target;
public AccessControlHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 统一前置处理
if (!hasPermission(method.getName())) {
throw new SecurityException("No permission: " + method.getName());
}
// 调用真实方法
Object result = method.invoke(target, args);
// 统一后置处理
auditLog(method.getName(), args, result);
return result;
}
}
// 使用
ApiService proxy = (ApiService) Proxy.newProxyInstance(
ApiService.class.getClassLoader(),
new Class[]{ApiService.class},
new AccessControlHandler(new RealApiService())
);
我的选择建议:
- 接口稳定、代理逻辑简单 → 静态代理,清晰直观
- 接口多、代理逻辑统一 → 动态代理,减少重复代码
- 需要代理类没有接口的类 → CGLib 动态代理
远程代理与虚拟代理
这两种代理在接口适配中特别实用。远程代理解决跨网络调用的问题,虚拟代理解决资源延迟加载的问题。
远程代理
远程代理让客户端像调用本地方法一样调用远程服务。代理对象封装了网络通信、序列化、异常处理等细节。
| 维度 | 远程代理 | 本地调用 |
|---|---|---|
| 网络通信 | 代理自动处理 HTTP/RPC | 直接方法调用 |
| 序列化 | 代理处理 JSON/Protobuf | 无需序列化 |
| 异常处理 | 代理转换网络异常为业务异常 | 直接抛异常 |
| 超时重试 | 代理内置重试机制 | 无 |
我在做微服务网关时就用远程代理封装了内部 API。客户端只依赖一个接口,代理负责路由、负载均衡、熔断。这样即使后端服务迁移,客户端代码也不用改。
虚拟代理
虚拟代理的核心是延迟创建。对象很重(比如大图片、数据库连接),但客户端不一定马上用。虚拟代理先占个位,真正需要时才创建真实对象。
public class VirtualImageProxy implements Image {
private RealImage realImage;
private String fileName;
public VirtualImageProxy(String fileName) {
this.fileName = fileName;
// 不加载真实图片,只保存文件名
}
@Override
public void display() {
// 第一次调用时才真正加载
if (realImage == null) {
realImage = new RealImage(fileName);
}
realImage.display();
}
}
虚拟代理的适用场景:
- 大对象加载(图片、文档、视频)
- 数据库连接池中的连接
- 配置信息的懒加载
- 第三方 SDK 的初始化
代理在接口访问控制中的应用
这是代理模式最实战的场景。我参与过的一个支付网关项目,所有外部接口都通过代理层访问。代理层统一处理了以下控制逻辑:
- 身份认证:校验 API Key 和签名
- 权限校验:检查接口调用方是否有对应权限
- 频率限制:基于令牌桶算法限流
- 参数校验:过滤非法参数和 SQL 注入
- 数据脱敏:对敏感字段自动脱敏
- 审计日志:记录每次调用的完整链路
public class ApiProxy implements PaymentApi {
private PaymentApi realApi;
private RateLimiter limiter;
private AuditLogger logger;
public ApiProxy(PaymentApi realApi) {
this.realApi = realApi;
this.limiter = new TokenBucketRateLimiter(100, 1); // 每秒100个请求
this.logger = new AuditLogger();
}
@Override
public PaymentResponse pay(PaymentRequest request) {
// 1. 身份认证
if (!authenticate(request.getApiKey(), request.getSign())) {
throw new AuthException("Invalid credentials");
}
// 2. 权限校验
if (!hasPermission(request.getApiKey(), "pay")) {
throw new PermissionException("No permission");
}
// 3. 频率限制
if (!limiter.tryAcquire()) {
throw new RateLimitException("Too many requests");
}
// 4. 参数校验
validateRequest(request);
// 5. 数据脱敏
sanitizeSensitiveData(request);
// 6. 调用真实接口
PaymentResponse response = realApi.pay(request);
// 7. 审计日志
logger.log(request, response);
return response;
}
}
我曾经踩过的坑:
- 代理层异常没处理好,导致真实接口被调用但客户端收到错误。后来我加了异常分类处理:认证异常直接拒绝,限流异常返回重试提示,业务异常透传。
- 代理层性能开销被低估。每个请求多走一层代理,如果代理逻辑太重(比如每次都查数据库做权限校验),QPS 直接掉一半。后来我把权限缓存到 Redis,代理层只做缓存查询。
- 动态代理的调试很痛苦。代理生成的类名是
$Proxy0,堆栈信息不直观。我习惯在代理层加requestId追踪,方便排查问题。
代理模式不是银弹。如果你的接口访问控制逻辑很简单,直接写在业务代码里反而更清晰。但一旦控制逻辑复杂到影响业务代码的可读性,或者控制规则需要动态变更,代理模式就是最好的选择。
我个人习惯在项目初期就用代理层做接口访问控制。虽然前期多写几行代码,但后期加鉴权、加限流、加审计时,你会发现这个决策有多明智。你想想看,改一个代理类就能影响所有接口,比挨个改业务代码省心太多了。