🔔 本文 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;
}
}
问题:
- 核心逻辑被"淹没"------真正的业务只有 ③,却被 5 坨横切代码包围
- 难以复用------别的 Service 也需要权限/缓存/日志,要重复写一遍
- 违反单一职责------一个方法干了 6 件事
- 改动风险高------想调整缓存策略,要在每个方法里改
- 无法灵活组合------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 方法不满足任何一种,所以代理拦截不到。同理 static、final 方法也不生效。
Q:代理模式和中间件/网关是什么关系?
A:网关(如 Nginx、APISIX、Spring Cloud Gateway)本质就是远程代理的工程化实现------客户端以为在直接访问后端服务,实际请求经过了网关代理,网关在其中做了鉴权、限流、路由、日志等控制。
九、小结
代理模式的核心价值:在不修改目标对象的前提下,透明地控制对它的访问------调用方完全不感知代理的存在。
三个实践要点:
- Spring 项目首选 AOP 代理------声明式注解(@Transactional/@Cacheable/@RateLimit)比手写代理类更优雅
- 警惕自调用失效 ------同类内方法互调不走代理,用
self注入或拆分类解决 - 代理只做"控制和拦截",不做业务逻辑------代理层越薄越好,业务复杂度留在 Service 层
设计模式系列前八篇已覆盖创建型(单例/工厂)、行为型(模板方法/观察者/策略)、结构型(装饰器/适配器/代理),形成了完整的核心模式矩阵。
标签:#设计模式 #代理模式 #Proxy #结构型模式 #Java #Spring #AOP #动态代理 #JDK代理 #CGLIB #MyBatis #Feign #远程调用 #面向对象 #软件工程