代理模式:接口访问的守门人

代理模式,说白了就是给一个对象找个替身。客户端跟替身打交道,替身背后再跟真正的对象沟通。我刚开始接触这个模式时觉得多此一举,直到有一次线上事故让我彻底改观——一个未做访问控制的远程接口被恶意调用,直接把数据库打崩了。嗯,从那以后,代理模式就成了我接口设计里的标配。

代理模式原理

代理模式的核心思想很简单:控制对目标对象的访问。代理对象和目标对象实现同一个接口,客户端只看到代理,看不到背后的真实对象。

核心角色:

  • Subject(主题接口):定义真实对象和代理的共同行为
  • RealSubject(真实主题):真正干活的业务对象
  • Proxy(代理):持有真实对象的引用,在调用前后插入额外逻辑

我习惯把代理理解成一个拦截层。客户端调用代理的方法,代理可以在调用前做权限检查、日志记录、参数校验,调用后做结果缓存、异常处理。这些逻辑完全不影响真实对象的代码。

代理模式核心结构 Client Subject 接口 Proxy RealSubject 额外逻辑 权限/日志/缓存 延迟加载/限流 调用流程 1. Client 调用 Subject 接口 2. 实际调用到 Proxy 3. Proxy 执行额外逻辑 4. Proxy 委托 RealSubject 5. 返回结果给 Client

静态代理与动态代理

静态代理和动态代理的区别,说白了就是一个手写,一个自动生成。

静态代理

静态代理就是手动写一个代理类,实现和目标类相同的接口。每个方法都要手动转发,代码量很大。

// 接口
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 的初始化

代理在接口访问控制中的应用

这是代理模式最实战的场景。我参与过的一个支付网关项目,所有外部接口都通过代理层访问。代理层统一处理了以下控制逻辑:

  1. 身份认证:校验 API Key 和签名
  2. 权限校验:检查接口调用方是否有对应权限
  3. 频率限制:基于令牌桶算法限流
  4. 参数校验:过滤非法参数和 SQL 注入
  5. 数据脱敏:对敏感字段自动脱敏
  6. 审计日志:记录每次调用的完整链路
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 追踪,方便排查问题。

代理模式不是银弹。如果你的接口访问控制逻辑很简单,直接写在业务代码里反而更清晰。但一旦控制逻辑复杂到影响业务代码的可读性,或者控制规则需要动态变更,代理模式就是最好的选择。

我个人习惯在项目初期就用代理层做接口访问控制。虽然前期多写几行代码,但后期加鉴权、加限流、加审计时,你会发现这个决策有多明智。你想想看,改一个代理类就能影响所有接口,比挨个改业务代码省心太多了。