设计模式实战解读(八):代理模式——控制访问的隐形中间层

🔔 本文 5000+ 字深度原创,含完整代码示例和生产级落地方案。创作不易,如果对你有帮助,请点赞 👍 收藏 ⭐ 关注 🔥 三连支持,你的认可是我持续输出的最大动力!
本文是「设计模式实战解读」系列第八篇。系列文章统一按照 定义 → 痛点场景 → 模式结构 → 核心实现 → 真实应用 → 常见变种 → 优缺点 → 避坑指南 → FAQ 的结构展开,每篇聚焦一个模式讲透。


一句话定义

代理模式(Proxy):为目标对象提供一个替身(代理),通过代理控制对目标对象的访问,在不改变目标对象的前提下,在访问前后插入额外的处理逻辑。

归属:结构型模式。


一、没有代理时的痛点

假设你有一个"用户信息查询"接口,随着系统发展,逐渐需要加上权限校验、缓存、日志、限流等功能:

java 复制代码
public class UserServiceImpl implements UserService {

    public UserDTO getUser(Long userId) {
        // ① 权限校验
        if (!SecurityContext.hasPermission("user:read")) {
            throw new ForbiddenException("无权限");
        }

        // ② 查缓存
        String cacheKey = "user:" + userId;
        UserDTO cached = redis.get(cacheKey, UserDTO.class);
        if (cached != null) {
            return cached;
        }

        // ③ 核心逻辑:查数据库
        UserDO userDO = userMapper.selectById(userId);
        UserDTO dto = UserConverter.toDTO(userDO);

        // ④ 写缓存
        redis.set(cacheKey, dto, 30, TimeUnit.MINUTES);

        // ⑤ 记日志
        log.info("查询用户: userId={}, result={}", userId, dto);

        // ⑥ 限流统计
        rateLimiter.acquire("user:getUser");

        return dto;
    }
}

问题:

  1. 核心逻辑被"淹没"------真正的业务只有 ③,却被 5 坨横切代码包围
  2. 难以复用------别的 Service 也需要权限/缓存/日志,要重复写一遍
  3. 违反单一职责------一个方法干了 6 件事
  4. 改动风险高------想调整缓存策略,要在每个方法里改
  5. 无法灵活组合------A 方法要缓存+日志,B 方法只要权限+限流,写不灵活

核心诉求:让 UserServiceImpl 只写核心业务逻辑,其他横切关注点由"另一个人"代为处理------这个人就是代理。


二、模式结构

复制代码
┌─────────────────────────────────┐
│      Subject(主题接口)          │
│  + getUser(userId): UserDTO     │ ← 代理和真实对象的公共接口
└──────────────┬──────────────────┘
               │
    ┌──────────┴──────────┐
    │                     │
    ↓                     ↓
┌──────────────┐  ┌──────────────────────────┐
│ RealSubject  │  │    Proxy(代理)           │
│ (真实对象)   │  ├──────────────────────────┤
│              │  │ - target: Subject        │ ← 持有真实对象
│UserServiceImpl│ │ + getUser(userId)        │ ← 前后插入控制逻辑
└──────────────┘  └──────────────────────────┘

三个角色:

  • Subject(主题接口):代理和真实对象的公共接口
  • RealSubject(真实主题):实际干活的对象
  • Proxy(代理):持有真实对象的引用,对外暴露相同接口,在调用真实对象前后插入控制逻辑

调用方以为在调用真实对象,实际调用的是代理------代理对调用方透明


三、核心实现

3.1 静态代理

手写代理类:

java 复制代码
// 主题接口
public interface UserService {
    UserDTO getUser(Long userId);
    List<UserDTO> listUsers(UserQuery query);
}

// 真实实现(只写核心逻辑)
@Service("userServiceImpl")
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDTO getUser(Long userId) {
        UserDO userDO = userMapper.selectById(userId);
        return UserConverter.toDTO(userDO);
    }

    @Override
    public List<UserDTO> listUsers(UserQuery query) {
        return userMapper.selectList(query).stream()
            .map(UserConverter::toDTO)
            .collect(Collectors.toList());
    }
}

// 静态代理:加缓存
public class CachingUserServiceProxy implements UserService {
    private final UserService target;
    private final RedisTemplate<String, Object> redis;

    public CachingUserServiceProxy(UserService target, RedisTemplate<String, Object> redis) {
        this.target = target;
        this.redis = redis;
    }

    @Override
    public UserDTO getUser(Long userId) {
        String cacheKey = "user:" + userId;
        // 前置:查缓存
        UserDTO cached = (UserDTO) redis.opsForValue().get(cacheKey);
        if (cached != null) {
            return cached;
        }
        // 调用真实对象
        UserDTO result = target.getUser(userId);
        // 后置:写缓存
        redis.opsForValue().set(cacheKey, result, 30, TimeUnit.MINUTES);
        return result;
    }

    @Override
    public List<UserDTO> listUsers(UserQuery query) {
        // 列表查询不缓存,直接透传
        return target.listUsers(query);
    }
}

静态代理的问题:每个接口的每个方法都要手写一遍代理逻辑,当接口有 20 个方法时代理类就爆炸了。

3.2 JDK 动态代理

java.lang.reflect.Proxy 在运行时生成代理:

java 复制代码
// 通用的缓存代理------适用于任何接口
public class CachingInvocationHandler implements InvocationHandler {
    private final Object target;
    private final RedisTemplate<String, Object> redis;

    public CachingInvocationHandler(Object target, RedisTemplate<String, Object> redis) {
        this.target = target;
        this.redis = redis;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 只对带 @Cacheable 注解的方法加缓存
        Cacheable cacheable = method.getAnnotation(Cacheable.class);
        if (cacheable == null) {
            return method.invoke(target, args); // 直接调用原方法
        }

        // 构建缓存 Key
        String cacheKey = cacheable.prefix() + ":" + Arrays.toString(args);
        Object cached = redis.opsForValue().get(cacheKey);
        if (cached != null) {
            return cached;
        }

        // 调用真实方法
        Object result = method.invoke(target, args);

        // 写缓存
        redis.opsForValue().set(cacheKey, result, cacheable.ttl(), TimeUnit.SECONDS);
        return result;
    }

    // 工厂方法
    @SuppressWarnings("unchecked")
    public static <T> T createProxy(T target, RedisTemplate<String, Object> redis) {
        return (T) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            new CachingInvocationHandler(target, redis)
        );
    }
}

// 使用
UserService userService = CachingInvocationHandler.createProxy(userServiceImpl, redis);
userService.getUser(123L); // 自动走缓存逻辑

3.3 CGLIB 动态代理

当目标类没有实现接口时,用 CGLIB 基于子类代理:

java 复制代码
public class LoggingMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args,
                            MethodProxy proxy) throws Throwable {
        log.info("调用方法: {}.{}(), 参数: {}", 
            method.getDeclaringClass().getSimpleName(), method.getName(), Arrays.toString(args));
        long start = System.currentTimeMillis();
        try {
            Object result = proxy.invokeSuper(obj, args);
            log.info("方法返回: {}, 耗时: {}ms", result, System.currentTimeMillis() - start);
            return result;
        } catch (Exception e) {
            log.error("方法异常: {}", e.getMessage(), e);
            throw e;
        }
    }

    // 工厂方法
    @SuppressWarnings("unchecked")
    public static <T> T createProxy(Class<T> targetClass) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(targetClass);
        enhancer.setCallback(new LoggingMethodInterceptor());
        return (T) enhancer.create();
    }
}

3.4 Spring AOP------代理模式的工业级实现

Spring 的 @Transactional@Cacheable@Async 底层全是代理模式:

java 复制代码
@Service
public class OrderServiceImpl implements OrderService {

    @Override
    @Transactional(rollbackFor = Exception.class)  // Spring 自动生成事务代理
    @Cacheable(value = "order", key = "#orderId")  // Spring 自动生成缓存代理
    public OrderDTO getOrder(Long orderId) {
        // 只写纯业务逻辑,事务和缓存由代理透明处理
        return orderMapper.selectById(orderId);
    }
}

Spring 会自动选择代理方式:

  • 目标类实现了接口 → JDK 动态代理
  • 目标类没有接口 → CGLIB 子类代理
  • 可通过 @EnableAspectJAutoProxy(proxyTargetClass = true) 强制 CGLIB

四、真实应用场景

4.1 代理的类型分类

代理类型 核心目的 典型场景
保护代理 权限控制 Spring Security、接口鉴权
缓存代理 避免重复计算 @Cacheable、MyBatis 二级缓存
虚拟代理 延迟加载(懒加载) Hibernate 懒加载、图片占位符
远程代理 隐藏网络通信 Dubbo/Feign 远程调用、RMI
日志代理 记录调用链 AOP 日志、MyBatis SQL 日志
智能引用代理 引用计数 / 资源管理 连接池、对象池

4.2 框架级应用

框架 代理实现 代理目的
Spring AOP JDK/CGLIB 事务、缓存、日志、权限
MyBatis JDK Proxy 把 Mapper 接口代理为 SQL 执行
Dubbo JDK/Javassist 远程调用透明化(像本地方法一样调用远程服务)
Spring Cloud OpenFeign JDK Proxy HTTP 远程调用像本地接口
Hibernate CGLIB/ByteBuddy 实体懒加载
Retrofit JDK Proxy 接口声明式 HTTP 调用

4.3 MyBatis Mapper------最巧妙的代理应用

java 复制代码
// 你只定义接口,从不写实现类
@Mapper
public interface UserMapper {
    @Select("SELECT * FROM user WHERE id = #{id}")
    UserDO selectById(Long id);
}

// MyBatis 用 JDK 动态代理自动生成实现
// 调用 userMapper.selectById(1L) 时实际执行的是代理逻辑:
// 1. 解析方法上的 @Select 注解获取 SQL
// 2. 绑定参数
// 3. 执行 SQL
// 4. 结果集映射为 Java 对象

你以为在调用一个普通方法,实际上代理帮你完成了 SQL 解析 → 参数绑定 → JDBC 执行 → 结果映射的全过程。

4.4 iPaaS API 网关代理

在 iPaaS 平台中,API 网关就是一个典型的代理层------调用方以为在直接调用后端服务,实际经过了网关代理的层层处理:

java 复制代码
// iPaaS API 网关代理
public class ApiGatewayProxy implements ConnectorInvoker {

    private final ConnectorInvoker realInvoker;
    private final RateLimiter rateLimiter;
    private final AuthService authService;
    private final MetricsService metricsService;
    private final CircuitBreaker circuitBreaker;

    @Override
    public InvokeResult invoke(InvokeRequest request) {
        // 1. 保护代理:鉴权
        authService.authenticate(request.getToken());
        authService.authorize(request.getAppId(), request.getConnectorId());

        // 2. 限流代理
        if (!rateLimiter.tryAcquire(request.getAppId(), request.getConnectorId())) {
            throw new RateLimitException("API调用超出频率限制");
        }

        // 3. 熔断代理
        if (circuitBreaker.isOpen(request.getConnectorId())) {
            throw new CircuitBreakerException("连接器暂时不可用,请稍后重试");
        }

        // 4. 调用真实服务
        long start = System.currentTimeMillis();
        try {
            InvokeResult result = realInvoker.invoke(request);
            circuitBreaker.recordSuccess(request.getConnectorId());
            return result;
        } catch (Exception e) {
            circuitBreaker.recordFailure(request.getConnectorId());
            throw e;
        } finally {
            // 5. 监控代理:记录调用指标
            long cost = System.currentTimeMillis() - start;
            metricsService.record(request.getConnectorId(), cost);
        }
    }
}

调用方只需要:

java 复制代码
invoker.invoke(request); // 完全不感知背后的鉴权/限流/熔断/监控

五、常见变种

5.1 远程代理(Feign/Dubbo)

java 复制代码
// Feign:声明接口,框架自动生成远程调用代理
@FeignClient(name = "user-service", url = "${user.service.url}")
public interface UserServiceClient {

    @GetMapping("/api/users/{id}")
    UserDTO getUser(@PathVariable("id") Long userId);

    @PostMapping("/api/users")
    UserDTO createUser(@RequestBody CreateUserRequest request);
}

// 使用时像本地方法一样调用
@Autowired
private UserServiceClient userServiceClient;

UserDTO user = userServiceClient.getUser(123L);
// 实际发生了 HTTP GET 请求 → 序列化 → 网络传输 → 反序列化
// 但调用方完全不感知

5.2 虚拟代理(懒加载)

java 复制代码
// 虚拟代理:真正使用时才加载重量级对象
public class LazyConnectionProxy implements Connection {
    private Connection realConnection;
    private final DataSource dataSource;

    @Override
    public PreparedStatement prepareStatement(String sql) throws SQLException {
        // 第一次真正使用时才获取连接
        if (realConnection == null) {
            realConnection = dataSource.getConnection();
        }
        return realConnection.prepareStatement(sql);
    }

    @Override
    public void close() throws SQLException {
        if (realConnection != null) {
            realConnection.close();
        }
    }
}

Spring 的 @Lazy 注入和 MyBatis 的懒加载都是这个思路。

5.3 保护代理(权限控制)

java 复制代码
// 保护代理:控制对敏感资源的访问
public class ProtectedFileServiceProxy implements FileService {
    private final FileService realService;
    private final PermissionChecker permissionChecker;

    @Override
    public byte[] readFile(String path) {
        // 检查读权限
        permissionChecker.checkRead(SecurityContext.getCurrentUser(), path);
        return realService.readFile(path);
    }

    @Override
    public void writeFile(String path, byte[] data) {
        // 检查写权限
        permissionChecker.checkWrite(SecurityContext.getCurrentUser(), path);
        realService.writeFile(path, data);
    }

    @Override
    public void deleteFile(String path) {
        // 检查删除权限(更严格)
        permissionChecker.checkAdmin(SecurityContext.getCurrentUser());
        realService.deleteFile(path);
    }
}

六、优缺点

优点 缺点
调用方无感知(透明代理) 增加了间接层,调试复杂
控制对真实对象的访问 动态代理有反射性能开销(通常可忽略)
符合开闭原则(不改原始类) 代理类激增时难以管理
支持延迟加载、缓存等优化 Spring 代理有"自调用失效"问题
解耦横切关注点 增加系统复杂度

七、避坑指南

坑 1:Spring 代理的"自调用失效"(最常见!)

java 复制代码
@Service
public class OrderService {

    @Transactional
    public void createOrder(OrderRequest request) {
        orderMapper.insert(request);
        // 同类内调用另一个带 @Cacheable 的方法------代理不会生效!
        this.getOrder(request.getId()); // ❌ 缓存注解无效
    }

    @Cacheable(value = "order", key = "#orderId")
    public OrderDTO getOrder(Long orderId) {
        return orderMapper.selectById(orderId);
    }
}

原因 :Spring 代理是通过外部引用触发的。this.getOrder() 是内部调用,绕过了代理对象。

解法

java 复制代码
// 方案1:注入自己(Spring 4.3+ 支持)
@Autowired
@Lazy
private OrderService self;

public void createOrder(OrderRequest request) {
    orderMapper.insert(request);
    self.getOrder(request.getId()); // ✓ 通过代理调用
}

// 方案2:拆分为两个类
@Service
public class OrderCommandService {
    @Autowired private OrderQueryService queryService;

    @Transactional
    public void createOrder(OrderRequest request) {
        orderMapper.insert(request);
        queryService.getOrder(request.getId()); // ✓ 跨类调用走代理
    }
}

坑 2:代理后 getClass() 返回的不是原始类

java 复制代码
@Autowired
private UserService userService;

// 如果 Spring 用了 CGLIB 代理
userService.getClass(); // → UserServiceImpl$$EnhancerBySpringCGLIB$$xxxx

// 这会导致某些反射逻辑出问题
// 解法:用 AopUtils 获取真实类
Class<?> targetClass = AopUtils.getTargetClass(userService);

坑 3:代理对象序列化问题

代理对象通常不能直接序列化(放入 Redis、传输到远程),因为代理类是运行时生成的。

解法

  • 缓存时存储 DTO 而非代理 Bean
  • 序列化前调用 AopUtils.getTargetSource() 获取原始对象

坑 4:过度使用代理导致调试噩梦

复制代码
调用链路:Controller → AOP日志代理 → 事务代理 → 缓存代理 → 权限代理 → 真实Service

一个方法上 4 层代理,出了 bug 断点打在 Service 里,发现调用栈有 20 层反射...

缓解方案

  • 代理层数控制在 ≤ 3 层
  • 使用 @Order 明确代理(AOP 切面)的执行顺序
  • 日志中输出代理链信息,方便排查

坑 5:final 方法/类无法被代理

java 复制代码
// ❌ CGLIB 无法代理 final 方法
public class UserServiceImpl {
    public final UserDTO getUser(Long id) { ... } // CGLIB 代理不到
}

// ❌ JDK 动态代理要求有接口
public class UserServiceImpl { // 没实现接口 → JDK Proxy 无法代理
    public UserDTO getUser(Long id) { ... }
}

规范 :被 Spring 管理的 Bean 不要用 final 修饰类或方法。


八、常见问题(FAQ)

Q:代理模式和装饰器模式的区别?

A:结构几乎一样(都是包装同接口对象),区别在于意图和透明度

维度 代理模式 装饰器模式
意图 控制访问(权限/限流/懒加载) 增强功能(加日志/加重试)
透明度 调用方不知道用了代理 调用方主动选择包哪些装饰
创建方式 通常由框架自动创建(Spring AOP) 通常由调用方手动组装
典型场景 @Transactional / Feign / MyBatis Mapper Java I/O 流 / 手写增强链

Q:JDK 动态代理和 CGLIB 怎么选?

A:

  • 有接口 → JDK 动态代理(性能更好、Spring 默认优先)
  • 无接口 → CGLIB(基于子类继承,更通用)
  • Spring Boot 2.x 默认 CGLIB(proxyTargetClass=true),因为实际项目中很多 Bean 没有接口

Q:Dubbo/Feign 的远程调用为什么用代理模式?

A:让远程调用的使用方式和本地方法调用完全一致------调用方不需要知道对面是 HTTP/TCP/gRPC,不需要手动序列化/反序列化,不需要处理网络异常重试。代理把这些复杂性全部隐藏。这就是代理的"透明性"。

Q:Spring 的 @Transactional 为什么在 private 方法上不生效?

A:@Transactional 依赖代理实现。JDK 动态代理只能代理接口方法;CGLIB 只能代理可以被子类覆写的方法------private 方法不满足任何一种,所以代理拦截不到。同理 staticfinal 方法也不生效。

Q:代理模式和中间件/网关是什么关系?

A:网关(如 Nginx、APISIX、Spring Cloud Gateway)本质就是远程代理的工程化实现------客户端以为在直接访问后端服务,实际请求经过了网关代理,网关在其中做了鉴权、限流、路由、日志等控制。


九、小结

代理模式的核心价值:在不修改目标对象的前提下,透明地控制对它的访问------调用方完全不感知代理的存在。

三个实践要点:

  1. Spring 项目首选 AOP 代理------声明式注解(@Transactional/@Cacheable/@RateLimit)比手写代理类更优雅
  2. 警惕自调用失效 ------同类内方法互调不走代理,用 self 注入或拆分类解决
  3. 代理只做"控制和拦截",不做业务逻辑------代理层越薄越好,业务复杂度留在 Service 层

设计模式系列前八篇已覆盖创建型(单例/工厂)、行为型(模板方法/观察者/策略)、结构型(装饰器/适配器/代理),形成了完整的核心模式矩阵。


标签:#设计模式 #代理模式 #Proxy #结构型模式 #Java #Spring #AOP #动态代理 #JDK代理 #CGLIB #MyBatis #Feign #远程调用 #面向对象 #软件工程

相关推荐
我爱cope2 小时前
【Agent智能体12 | 反思设计模式-使用外部反馈】
人工智能·设计模式·语言模型·职场和发展
geovindu2 小时前
python: Bounded Parallelism Pattern
开发语言·python·设计模式·有界并行模式
我爱cope3 小时前
【Agent智能体11 | 反思设计模式-评估反射的影响的方法】
人工智能·设计模式·语言模型·职场和发展
nnsix3 小时前
设计模式 - 迭代器模式 笔记
笔记·设计模式·迭代器模式
IT策士3 小时前
第 23篇 k8s之Pod:多容器 Pod 与设计模式(Sidecar 等)
设计模式·容器·kubernetes
青山师18 小时前
动态规划算法深度解析:从状态转移方程到工业级优化
数据结构·算法·面试·动态规划·代理模式·java面试
qq_2975746720 小时前
设计模式系列文章(基础篇第 11 篇):模板方法模式——定义算法骨架,实现代码复用与流程统一
算法·设计模式·模板方法模式
狂人开飞机1 天前
01. 工厂模式(Factory Pattern)
设计模式·c#
阿狸猿1 天前
论软件设计模式及其应用
设计模式