23种设计模式之代理模式(结构型模式二)

什么是代理模式?

代理模式为另一个对象提供了一个 替身(surrogate)占位符(placeholder) ,以控制对这个对象的访问;简单来说,当客户端想要调用一个对象(真实主题)的方法时,它不是直接去调用,而是先调用代理对象的方法。代理对象在将请求转发给真实对象之前或之后,可以添加额外的逻辑;

核心意图: 引入一个间接层,通过这个代理对象来控制、保护、优化或增强对真实对象的访问。

代理模式时序图

核心作用:在不改变原对象代码的前提下控制访问 🤝

  • 代理模式的本质是:控制对真实对象的访问,并为其添加额外功能
  • 代理模式让你在不修改原始类的前提下,控制访问,并为方法自动添加任何你想加的逻辑
  • 它是 Spring AOP 与各种框架增强的核心机制

代理模式主要包括两个能力:

(一) 控制对真实对象的访问(最核心)

不让外部直接访问真实对象,强制只能访问代理对象

限制访问的场景 典型例子 代理具体限制了什么
权限控制(最常见) 后台管理系统、文件读写 根据用户角色(admin/user/guest)决定能不能调用
敏感数据脱敏/字段屏蔽 返回用户个人信息接口 普通用户看不到身份证号、手机号完整号码
频率限制(防刷/限流) 短信验证码、点赞按钮 60秒内只能点一次,超了直接 429 返回
IP 黑白名单 API 网关、内部服务调用 不在白名单的 IP 直接拒绝
付费墙(内容付费) 知乎、付费视频、VIP文章 未付费用户只能看到前 30%,其余返回"请开通会员"
青少年模式/内容分级 抖音、YouTube Kids 未成年用户禁止访问血腥、恐怖、成人内容
企业内部服务只允许内网访问 公司内部 OA、Jenkins、K8s Dashboard 非内网 IP 一律 403
操作审计 + 禁止危险操作 数据库管理工具、Linux 服务器管理后台 普通用户禁止执行 delete from users 之类的语句

(二) 为原对象增加行为(AOP增强)

无需修改真实类即可增强其功能

功能 属于代理的哪种类型 代理在什么时候插入代码 项目中最常见实现方式
方法执行前打印日志 通用/日志代理 前置处理(preHandle) Spring AOP、Java Dynamic Proxy、AspectJ
方法执行后写数据库 审计/日志代理 后置处理(afterReturning / after) 各种框架的 @After 切面
自动加事务 事务代理(最经典!) 前置开启事务 → 正常结束提交 → 异常回滚 Spring @Transactional 就是事务代理!
性能统计耗时 监控/性能代理 记录开始时间 → 结束后计算差值并打印/上报 Micrometer、SkyWalking、自定义AOP
缓存控制 缓存代理(非常常见) 先查缓存 → 没命中才调真实方法 → 结果写回缓存 Spring Cache 中@Cacheable/@CachePut/@CacheEvict
调用次数统计 统计/限流代理 每次调用原子 +1,可结合 Redis/MeterRegistry Prometheus Counter、Guava RateLimiter

实际用途:为什么明明能直接调用对象,却要用代理?

用代理模式的原因有以下四类:

(一) 原始类不能被修改

例如:

  • 第三方 jar 包中的类
  • 核心类禁止业务人员随意改动
  • 多人协作不希望都改同一个类,代理可以做到 不改源码就增强功能

(二) 希望在方法调用前后加逻辑(AOP 做的事)

代理可以自动加:前置增强 → 调用原方法 → 后置增强

例如不想写以下几种重复代码逻辑:

  • log(); ------日志记录
  • check(); ------ 权限/参数校验(前置增强)
  • target.xxx(); ------ 调用真实业务方法(核心业务)
  • audit(); ------ 审计 / 记录调用结果(后置增强)

(三) 需要控制访问权限

例如对某些对象和方法做权限校验:

  • 禁止调用某些方法
  • 只能在特定用户身份下访问真实对象
  • 强制通过网关或统一入口才能访问

(四) 希望统一管理横切逻辑

几乎所有 AOP、拦截器、框架增强都通过代理实现:

  • 事务管理(@Transactional)
  • 日志增强
  • 异常捕捉
  • 监控统计
  • 防重复提交

延伸场景:解决实际业务问题

(一) 延迟加载(Lazy Loading)

真实对象过于"重"(如数据库连接、文件、网络资源)代理可以先占位真正使用时才创建真实对象

(二) 远程调用封装(RPC 实现)

所有 RPC 框架都在用代理,代理让调用远程服务看起来像调用本地方法,包括以下几种:

  • Dubbo 的 ReferenceBean
  • Feign 的 @FeignClient
  • gRPC Stub
  • Spring 的 RestTemplate 动态代理

(三) 安全隔离

当不允许别人直接访问真实对象时,通过代理统一管理

(四) Spring 中的代理设计模式

场景 使用方式 实现代理方式
AOP 切面增强 @Aspect JDK 或 CGLIB
事务管理 @Transactional JDK 或 CGLIB
缓存注解 @Cacheable Spring AOP 代理

代理设计模式的实现方式常见 3 大类 + 1 扩展(常用于框架)

一、静态代理(Static Proxy)

支持代理 final 类

  1. 定义公共接口(抽象主题)
  2. 实现真实业务类(RealSubject)
  3. 编写代理类(Proxy),内部持有 RealSubject 对象,控制访问
csharp 复制代码
// 抽象主题
public interface UserService {
    void doWork();
}

// 真实对象
public class UserServiceImpl implements UserService {
    public void doWork() {
        System.out.println("执行真实业务逻辑");
    }
}

// 代理对象
public class UserServiceProxy implements UserService {
    private final UserServiceImpl userServiceImpl = new UserServiceImpl();

    public void doWork() {
        System.out.println("前置日志记录");
        realService.doWork();
        System.out.println("后置监控统计");
    }
}

// 运行代码
public static void main(String[] args) {
    UserServiceProxy userServiceProxy = new UserServiceProxy();
    userServiceProxy.doWork();
}

二、JDK 动态代理(JDK Dynamic Proxy)

使用 JDK 动态代理的前提:被代理的类必须实现接口

实现方式:

  1. 创建接口和实现类
  2. 使用 InvocationHandler 实现增强逻辑
  3. 通过 Proxy.newProxyInstance() 生成代理对象,并隐藏真实对象防止调用
typescript 复制代码
public interface IPaymentService {
    void pay(String amount);
}

@Service
public class PaymentServiceImpl implements IPaymentService {
    @Override
    public void pay(String amount) {
        System.out.println(">>> 正在执行支付操作,金额:" + amount);
    }
}
typescript 复制代码
public class PaymentInvocationHandler implements InvocationHandler {
    // 原对象
    private final Object target;

    // 定义需要排除的 Object 类方法列表
    private static final List<String> EXCLUDE_METHODS = Arrays.asList("toString", "hashCode", "equals", "clone");

    public PaymentInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        String methodName = method.getName();

        // ⭐️ 关键过滤逻辑:排除 Object 类的方法,这些方法不进行增强(JDK 动态代理的 invoke 方法的机制是:它会拦截 代理对象上所有公开方法的调用,包括那些从 java.lang.Object 继承来的方法)
        if (EXCLUDE_METHODS.contains(methodName)) {
            // 直接调用原方法,不执行增强逻辑
            return method.invoke(target, args);
        }

        // 业务方法的增强逻辑 (仅在此处执行)
        System.out.println("[代理增强] 方法执行前: 正在记录访问日志 -> " + methodName);

        Object result = method.invoke(target, args);

        System.out.println("[代理增强] 方法执行后: 日志记录完成");

        return result;
    }
}    
typescript 复制代码
@Component
public class PaymentProxyHidingBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        // 识别到 PaymentServiceImpl 目标 Bean ,如果注入原对象则替换成代理对象
        if (bean instanceof PaymentServiceImpl) {
            System.out.println("[BPP 机制] 发现目标 Bean [" + beanName + "],准备使用动态代理替换它!");

            // 1. 创建代理对象,将原对象作为参数传入 InvocationHandler
            Object proxyInstance = Proxy.newProxyInstance(
                    bean.getClass().getClassLoader(),
                    bean.getClass().getInterfaces(),
                    new PaymentInvocationHandler(bean)
            );

            // 2. 关键步骤:返回代理对象
            // Spring 容器会将 beanName="userServiceImpl" 映射到这个代理对象。
            // 原对象实例被隐藏在 proxyInstance 内部,外部无法获取。
            return proxyInstance;
        }

        return bean;
    }
}
less 复制代码
@Slf4j
@RestController
@RequestMapping("/portal/test")
public class TestController {
    @Autowired
    private IPaymentService paymentService;
    @GetMapping("/test/proxy")
    public void testProxy() {
        // 1. 验证注入的对象到底是谁
        // 如果是 JDK 代理,类名通常是 $Proxy...
        System.out.println("注入对象的实际类名: " + paymentService.getClass().getName());

        // 2. 调用方法
        paymentService.pay("100元");
    }
}

三、CGLIB 动态代理(CGLIB Enhancer)

CGLIB 动态代理基于继承,代理对象继承于原对象,目标类不能是 final 类,方法也不能是 final修饰

实现方式: 使用第三方库 CGLIB(如 Spring AOP 默认使用) 生成目标类的子类实现代理

typescript 复制代码
public interface IPaymentService {
    void pay(String amount);
}

@Service
public class PaymentServiceImpl implements IPaymentService {
    @Override
    public void pay(String amount) {
        System.out.println(">>> 正在执行支付操作,金额:" + amount);
    }
}
typescript 复制代码
public class PaymentMethodInterceptor implements MethodInterceptor {

    // 维护目标对象(原 Spring Bean)
    private final Object target;

    // 定义需要排除的方法列表
    private static final List<String> EXCLUDE_METHODS = Arrays.asList("toString", "hashCode", "equals", "clone");

    public PaymentMethodInterceptor(Object target) {
        this.target = target;
    }

    /**
     * @param proxy       代理对象本身
     * @param method      被拦截的方法对象
     * @param args        方法参数
     * @param methodProxy CGLIB 提供的机制,用于调用父类方法 (invokeSuper)
     */
    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

        String methodName = method.getName();

        // 1. 过滤不需要增强的方法
        if (EXCLUDE_METHODS.contains(methodName)) {
            // 直接反射调用目标对象的方法
            return method.invoke(target, args);
        }

        // 2. 增强逻辑:前置
        System.out.println("[CGLIB 增强] 方法执行前: 正在记录访问日志 -> " + methodName);

        // 3. 执行目标方法
        // 注意:这里我们调用 target (原对象) 的方法,而不是使用 methodProxy.invokeSuper。
        // 因为我们在 BeanPostProcessor 中拿到的是已经由 Spring 初始化好的 Bean,
        // 我们希望操作那个具体的 Bean 实例。
        Object result = method.invoke(target, args);

        // 4. 增强逻辑:后置
        System.out.println("[CGLIB 增强] 方法执行后: 日志记录完成");

        return result;
    }
}
java 复制代码
@Component
public class PaymentProxyHidingBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        // 识别目标 Bean
        if (bean instanceof PaymentServiceImpl) {
            System.out.println("[BPP 机制] 发现目标 Bean [" + beanName + "],准备使用 CGLIB 代理替换它!");

            // 1. 创建 Enhancer 对象 (类似于 JDK 的 Proxy 类)
            Enhancer enhancer = new Enhancer();

            /*
            2. 设置父类 (Superclass)
            关键区别:JDK 代理是 setInterfaces,CGLIB 是 setSuperclass
            代理类将继承 PaymentServiceImpl
            */
            enhancer.setSuperclass(bean.getClass());

            // 3. 设置回调 (Callback),将我们自定义的拦截器传入,并把原 bean 传进去
            enhancer.setCallback(new PaymentMethodInterceptor(bean));

            // 4. 创建代理对象
            return enhancer.create();
        }

        return bean;
    }
}

四、三种代理模式总结

实现方式 是否要求接口 是否可以增强所有方法 是否支持 final 类
静态代理
JDK 动态代理
CGLIB 动态代理
相关推荐
落枫5941 分钟前
OncePerRequestFilter
后端
程序员西西41 分钟前
详细介绍Spring Boot中用到的JSON序列化技术?
java·后端
课程xingkeit与top41 分钟前
大数据硬核技能进阶:Spark3实战智能物业运营系统(完结)
后端
课程xingkeit与top41 分钟前
基于C++从0到1手写Linux高性能网络编程框架(超清)
后端
语落心生41 分钟前
探秘新一代向量存储格式Lance-format (二十二) 表达式与投影
后端
雨中飘荡的记忆41 分钟前
MySQL 优化实战
java·mysql
豆豆的java之旅44 分钟前
深入浅出Activity工作流:从理论到实践,让业务流转自动化
java·运维·自动化·activity·工作流
码事漫谈1 小时前
音域之舞-基于Rokid CXR-M SDK的AI眼镜沉浸式K歌评分系统开发全解析
后端
一点 内容1 小时前
深度解析OurBMC后端模式:全栈技术架构与运维实践
java·开发语言