
我在之前的一个老项目里接手过一段性能很差的报表导出功能。每次导出都要全量查询一次数据库再逐条计算汇总指标用户点一次按钮接口要跑三四十秒。原本想着加个缓存就能解决结果发现业务代码里到处散落着统计逻辑改起来牵一发动全身。后来把统计这块单独抽出来用代理模式在前面挡了一层按条件决定是走缓存还是重算改动量小效果却立竿见影。那是我第一次意识到代理模式不是教科书上一个需要背的UML图而是真正能在不改动业务代码的前提下把横切逻辑从核心逻辑里摘干净的利器。这篇内容我围绕代理模式这个经典设计模式展开不讲空泛理论主要结合我实际写代码、改代码、排查线上问题的经验把它是什么、怎么用、三种实现方式的取舍、生产环境里的高频场景以及特别容易踩的坑一次说清楚。不管你是刚学设计模式的新人还是写了好几年业务代码但没系统梳理过的老手这篇都值得花十分钟看完。1. 代理模式到底解决什么问题从一次不得不改代码的经历说起很多人学代理模式第一眼看到的定义是为其他对象提供一种代理以控制对这个对象的访问。这个说法没错但太抽象了。我先讲一个具体的经历。1.1 没有代理时我们是怎么被横切需求反复折腾的还是上面说的那个报表导出功能。当时的代码长什么样呢简略一下就是这样public class ReportService { public ReportData export(ReportQuery query) { // 1. 记录日志谁在什么时间导出了什么报表 logger.info(export report, query{}, query); long start System.currentTimeMillis(); // 2. 权限校验判断当前用户是否有权查看这些数据 if (!permissionService.check(query)) { throw new PermissionDeniedException(); } // 3. 核心业务查询数据、汇总计算 ListRow rows statisticDao.query(query); ReportData data doCalculate(rows); // 4. 性能监控统计这次导出耗时 long cost System.currentTimeMillis() - start; monitor.record(report.export, cost); return data; } }一开始只有导出报表这一个入口代码虽然挤在一起但也能跑。后来加了定时推送、免登录分享、移动端预览等多个入口每个入口都复制粘贴了一份类似的日志权限监控代码。再后来需求方说部分角色导出的时候不需要实时计算可以接受十分钟前的缓存又有人说某些报表要记录导出次数用于配额管理……每个新需求进来业务方法内部就要多塞一段逻辑。核心的统计计算被埋在层层横切代码下面改一个查询条件都提心吊胆。这就是没有代理时我们的处境跨多个业务对象的通用逻辑被迫重复写在每一个业务方法内部代码膨胀、逻辑耦合、改一处崩一片。1.2 代理模式登场把控制从业务里剥离出来代理模式的核心思想其实特别朴素核心业务类只干核心业务的事其他那些访问前后要做的控制全部丢给一个代理对象去干。客户端调用的不再是真正的业务对象而是代理对象。代理对象在调用真实对象之前和之后可以插入各种逻辑——权限检查、日志记录、参数校验、缓存判断、性能统计、事务控制等等。真实业务对象对此毫不知情它依旧只关注自己的核心职责。用代理模式改造上面的报表逻辑结构就变成public class ReportServiceProxy implements ReportService { private final ReportService target; public ReportServiceProxy(ReportService target) { this.target target; } public ReportData export(ReportQuery query) { logger.info(export report, query{}, query); long start System.currentTimeMillis(); try { if (!permissionService.check(query)) { throw new PermissionDeniedException(); } return target.export(query); } finally { long cost System.currentTimeMillis() - start; monitor.record(report.export, cost); } } }调用方只要拿到的是ReportServiceProxy其他什么都不用改。真正的ReportServiceImpl里只剩核心计算逻辑干净得让人舒服。所以我个人的理解是代理模式真正的价值不在于包装了一个对象而在于把调用方和真实对象之间的访问控制逻辑从两边都拆了出来单独放进一个中间层。它解决的核心痛点是业务代码与控制代码的混杂而不是简单的功能增强。2. 代理模式的四个角色和一份最朴素的代码骨架教科书里讲代理模式一定会提到几个角色抽象主题、真实主题、代理、客户端。我结合自己的理解用大白话拆一遍。2.1 四个角色的实际分工抽象主题Subject定义业务方法的接口。它存在的意义是让客户端只面向接口编程这样代理和真实对象可以随时互换客户端完全无感知。这是代理能不能透明工作的关键。真实主题RealSubject真正干活的对象实现核心业务逻辑。它不需要知道代理的存在也不需要关心日志、权限、缓存这些事情。代理Proxy持有真实主题的引用实现抽象主题的接口。对外看起来和真实主题一模一样但内部会在调用真实主题前后插入控制逻辑。客户端Client面向抽象主题编程持有的是代理对象。客户端完全不感知自己操作的到底是代理还是真实对象。2.2 一段能直接跑起来的静态代理代码理论说多了容易晕我直接贴一段最朴素的静态代理实现。场景是下载文件需求是下载前检查用户是否有VIP权限。首先是抽象主题接口public interface FileDownloader { File download(String fileName); }真实主题真正执行下载逻辑public class LocalFileDownloader implements FileDownloader { Override public File download(String fileName) { System.out.println(正在从本地磁盘读取文件: fileName); return new File(/data/files/ fileName); } }代理类在调用真实下载器之前做了权限校验public class VipProxy implements FileDownloader { private final FileDownloader target; private final User currentUser; public VipProxy(FileDownloader target, User currentUser) { this.target target; this.currentUser currentUser; } Override public File download(String fileName) { if (!vip.equals(currentUser.getLevel())) { throw new SecurityException(只有VIP用户才能下载文件); } System.out.println(权限校验通过开始下载); return target.download(fileName); } }客户端使用时只需要这样FileDownloader downloader new VipProxy(new LocalFileDownloader(), currentUser); File file downloader.download(设计模式笔记.pdf);核心点在于LocalFileDownloader没有任何权限判断的代码新增的VIP校验完全被隔离在了VipProxy里。以后如果想把VIP校验改成付费用户校验只需要改VipProxy或者换一个代理实现业务下载类一行都不用动。静态代理虽然结构简单、逻辑直观但它有一个绕不开的毛病每一个业务接口都要写一个对应的代理类。系统里有二十个接口就得写二十个代理类而且代理类里大量代码是重复的——都是调用前后插入逻辑的模板代码。这也是为什么实际生产环境里我们更多使用动态代理。3. 静态代理之外JDK动态代理和CGLIB的选型真相静态代理的问题在于一个接口一个代理类代码量膨胀得让人崩溃。动态代理则是在运行时动态生成代理类一个代理逻辑可以复用到任意多个接口上。Java生态里最常见的就是 JDK 动态代理和 CGLIBSpring AOP 的底层就是这两者的组合。我分别说一下。3.1 JDK动态代理只认接口JDK动态代理基于接口实现核心是java.lang.reflect.Proxy和InvocationHandler。代理对象在运行时生成实现了指定接口并且把方法调用统一转发到InvocationHandler.invoke()方法里。还是拿文件下载举例用JDK动态代理改造public class LogHandler implements InvocationHandler { private final Object target; public LogHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(方法执行前: method.getName()); Object result method.invoke(target, args); System.out.println(方法执行后: method.getName()); return result; } }创建代理对象的逻辑可以封装成一个工具方法public class ProxyFactory { public static Object createProxy(Object target, InvocationHandler handler) { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler ); } }用的时候FileDownloader downloader new LocalFileDownloader(); FileDownloader proxy (FileDownloader) ProxyFactory.createProxy( downloader, new LogHandler(downloader)); proxy.download(设计模式笔记.pdf);JDK动态代理这里有个硬性条件目标对象必须有接口。因为动态生成的代理类是通过java.lang.reflect.Proxy创建的它在 Java 的类继承体系里已经继承了Proxy类Java 是单继承所以代理类只能通过实现接口的方式来扩展目标类型。如果目标类没有实现任何接口JDK动态代理直接歇菜。3.2 CGLIB没有接口时的救星CGLIB 走的是另一条路它直接在运行时生成目标类的子类通过覆写方法来实现代理逻辑。因为用的是继承所以不需要接口目标类只要不是final的方法也不是final的就可以被代理。public class CglibProxyFactory { public static Object createProxy(Class? targetClass, MethodInterceptor interceptor) { Enhancer enhancer new Enhancer(); enhancer.setSuperclass(targetClass); enhancer.setCallback(interceptor); return enhancer.create(); } }对应的拦截器逻辑MethodInterceptor interceptor (obj, method, args, proxy) - { System.out.println(CGLIB方法执行前: method.getName()); Object result proxy.invokeSuper(obj, args); System.out.println(CGLIB方法执行后: method.getName()); return result; };注意这里调用目标方法用的是proxy.invokeSuper(obj, args)不是method.invoke(obj, args)。因为直接反射 invoke 到目标对象会绕过 CGLIB 生成的增强逻辑而且可能引发递归调用问题。这个细节我见过挺多人踩坑的。3.3 三种方式怎么选一张表说清楚我把静态代理、JDK动态代理、CGLIB的差异整理成一张表方便你对照着选对比维度静态代理JDK动态代理CGLIB代理类生成时机编译期手动编写运行期动态生成运行期动态生成目标对象要求需要接口必须实现接口无需接口类不能是final是否需要额外依赖不需要JDK自带需要引入CGLIB库性能调用开销最快反射调用有一定开销生成子类方法拦截略慢代码维护成本高接口多了类爆炸低一套Handler通用低一套Interceptor通用使用场景接口少、逻辑简单以接口设计为主的Spring项目没有接口的遗留系统或第三方类一个比较实用的判断逻辑是如果是新写的代码优先面向接口设计用JDK动态代理如果是老系统类没有接口或者要代理的是第三方库里的具体类那就用CGLIB。4. 生产环境里代理模式用得最多的四个场景理论讲完我重点说说生产环境里代理模式到底在哪些地方扛大梁。你会发现很多你天天在用但没意识到的东西底层都是代理模式。4.1 事务管理Spring Transactional 的幕后功臣这是代理模式在Java后端最广为人知的应用。Spring 容器里你写的那个 Service 类实际放进容器里的并不是它本身而是一个代理对象。当你调用带有Transactional注解的方法时代理对象会先开启事务再调用真实方法。如果方法抛了 RuntimeException代理会执行回滚如果正常返回代理会提交事务。Service public class OrderService { Transactional public void createOrder(OrderDTO dto) { orderDao.insert(dto); inventoryDao.deduct(dto.getSkuId(), dto.getCount()); } }Spring 容器里的 orderService 实际上是OrderService的一个代理。如果该类实现了接口Spring 默认用 JDK 动态代理如果没有实现接口Spring 自动切换为 CGLIB。调 createOrder 方法的整体执行路径是进入代理 invoke → 开启数据库事务 → 调用真实 createOrder 方法 → 方法正常返回 → 提交事务方法抛异常 → 回滚事务。这就是为什么你在一个 Service 内部自己调自己另一个Transactional方法时事务经常不生效因为这次调用发生在真实对象内部根本没有经过代理对象。4.2 缓存与懒加载别让业务代码自己判断该不该查库缓存是代理模式特别适合的场景。把是否命中缓存的判断放在代理层业务方法永远只写从数据库查数据这一件事。public class CacheProxyT implements InvocationHandler { private final T target; private final Cache cache; private final String prefix; Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String cacheKey buildKey(method, args); Object cached cache.get(cacheKey); if (cached ! null) { return cached; } Object result method.invoke(target, args); cache.put(cacheKey, result); return result; } }这样做的好处是缓存策略是集中管理的。今天用本地 Caffeine明天想换成 Redis改代理层一个类就行所有业务方法都不用动。懒加载的原理也类似代理对象被创建时并不真正初始化内部的重资源对象只有当你第一次调用某个方法时代理才去创建真实对象并转发请求。很多ORM框架的延迟加载就是这样做的。4.3 访问控制与审计日志把安全逻辑收敛到一处我在文章开头提到的报表导出后来就是通过代理模式统一做了权限校验和操作审计。所有涉及导出的入口不管是从 Web 页面来的还是从定时任务来的统一经过同一个代理做什么校验、记什么日志都在一个地方维护。这个思路对安全审计特别有意义。审计要求谁在什么时候做了什么操作如果你在每个业务方法里手动写一遍日志代码漏记是必然的。把审计逻辑放到代理层所有被代理的方法天然就有了操作记录能力完全靠机制保障而不是靠人肉自觉。4.4 远程调用代理像调本地方法一样调远程服务RPC 框架比如 Dubbo、Feign的调用方拿到的其实就是一个远程代理对象。你在代码里调用userService.getUserById(id)这个 userService 并不是本地实现而是一个代理。代理内部把方法名和参数序列化通过网络发送到远程服务器再反序列化拿到返回结果。这个过程完美符合代理模式的定义客户端完全感知不到远程通信的存在就像在调用一个本地对象。网络传输、序列化、负载均衡、超时重试这些复杂度全被代理对象隐藏了。5. 线上项目里代理最容易出问题的三个坑代理模式好用但用不好会带来很隐蔽的问题。这些问题不看源码基本发现不了我遇到过好几次每次排查都要花不少时间这里直接分享给你。5.1 Spring 代理失效this 自调用是最经典的坑这是我在代码评审里见过最多的问题没有之一。很多人写了这样的代码Service public class OrderService { public void handleOrder(OrderDTO dto) { updateStatus(dto); sendNotify(dto); } Transactional public void updateStatus(OrderDTO dto) { // 更新订单状态 } }调handleOrder方法时从 Spring 容器里拿到的是代理对象代理会正常开启。但handleOrder方法体内部调updateStatus时这个this指向的是真实对象而不是代理对象所以Transactional根本不会生效。解决办法有三种把需要代理的方法拆分到另一个 Spring Bean 中注入后通过代理对象调用注入ApplicationContext每次从容器里获取代理对象再调用在类内部注入自身代理Spring Cloud 之前的版本可以用Lazy注入自己或者用AopContext.currentProxy()但AopContext默认不开启。我个人最推荐第一种因为拆分之后类的职责也更清楚了updateStatus和sendNotify各管各的不容易再踩别的坑。5.2 动态代理的类加载器问题JDK 动态代理创建代理对象时需要传入类加载器Proxy.newProxyInstance(target.getClass().getClassLoader(), ...);如果传入的类加载器和目标接口的类加载器不一致运行时会报ClassCastException或IllegalArgumentException。这种情况在 Web 应用里尤其常见因为 Tomcat 等容器会对不同应用使用不同的类加载器。经验是和接口、目标对象一样使用它们自己的类加载器而不是随便用ProxyFactory.class.getClassLoader()。我见过有人图省事写死成某个工具类的类加载器结果在测试环境没事、部署到线上就翻车。5.3 CGLIB 代理目标类被 final 修饰导致启动失败CGLIB 通过生成子类来代理所以目标类不能被final修饰目标方法也不能是final。如果 Spring 启动时发现某个需要代理的 Bean 是 final 类会直接抛出异常。这个问题常见于把第三方 JAR 里的 final 类交给 Spring 管理然后又想在它上面加事务或切面逻辑。遇到这种情形老老实实做一层封装写一个非 final 的门面类去组合这个第三方 final 类再对门面类做代理。硬着头皮用 CGLIB 去代理 final 类路是走不通的。6. 代理模式与易混模式的边界一句话就能分清楚设计模式学多了之后容易把长得像的混在一起。代理、装饰器、适配器这仨是最容易让人犯迷糊的因为它们代码结构上都是内部持有一个对象外部看起来像另一个对象。我分享一个我的判别方法。6.1 代理模式 vs 装饰器模式目的不同姿态不同装饰器模式的核心是增强它加入的是新功能而且通常是递归叠加的。比如给文件流套缓冲、套加密、套压缩一层套一层每一层都给原始对象增加新的能力而且新增的能力是给原对象锦上添花。代理模式的核心是控制它不一定给原对象增加什么新功能更多时候是在控制能不能调用什么时候调用调用前后还要做什么。代理通常只做一层的隔离不会像装饰器那样层层嵌套。一个比较直白的判断方式装饰器对外暴露的能力比原对象更丰富代理对外暴露的能力往往和原对象一样但在访问过程上做了管控。比如上面文件下载的例子代理和原对象的方法列表一模一样但代理加了权限控制这就是代理。如果你在 IO 流上套一层 BufferedReader让它多了按行读的能力那就是装饰器。6.2 代理模式 vs 适配器模式接口形态变没变适配器模式是为了解决接口形态不兼容的问题。老接口和新接口长得不一样适配器在中间做翻译转换让它俩能对接上。代理模式里代理对象和真实对象实现的是同一个接口接口形态没有变化。所以判断标准也很简单代理模式中代理和原对象接口是同一个客户端无感知适配器模式中适配器和被适配对象接口不同客户端通过适配器获得了不同的接口能力。一个是同接口改行为一个是异接口做转换方向完全不同。6.3 再送你一个综合判断清单以后写代码犹豫不决的时候可以按这个清单过一遍是想在调用前后加控制逻辑但不改变对外接口→ 代理模式是想给对象动态增加新能力可能一层层叠加→ 装饰器模式是因为接口不兼容需要做转换让两边接上→ 适配器模式是想屏蔽一堆子系统对外只暴露一个简单门面→ 门面模式这个清单帮我厘清过很多次思路尤其是在做代码重构的时候目标不明确很容易把模式用串。7. 把代理模式真正用好的几条实战建议最后这部分分享几个我在实际落地中沉淀下来的习惯。这些不属于教科书内容但真的能帮你少走弯路。7.1 不要把代理做成上帝类代理层很容易写失控什么都往里面塞日志也要、权限也要、缓存也要、监控也要、限流也要。最后代理类比真实业务类还大横切逻辑再一次混乱。我的习惯是一个代理只负责一件事比如CacheProxy只管缓存AuditProxy只管审计PermissionProxy只管权限。如果确实多个关注点都需要那就让多个代理嵌套叠加或者直接用 AOP 切面的多切面编排也不要写成一个万能代理。7.2 动态代理的类命名要能看出痕迹动态代理在运行期生成的类打印全类名时往往是类似com.sun.proxy.$Proxy0这种很难从名字上看出代理了什么。排查线上问题时如果日志里出现了这种类名你应该立刻意识到这里是动态代理在生效。7.3 优先想清楚是不是真的需要代理代理模式是个好工具但不是万金油。如果你的业务方法只有一个入口、横切逻辑只有一种、也不太可能再加新入口那直接在方法里写几行日志和权限校验问题也不大。过度设计同样是技术债。我见过不少新人为了展示自己懂设计模式非要把简单的业务代码套上代理、套上工厂、套上策略最后维护的人骂娘。代理模式用得最合理的地方永远是一套逻辑要服务多个目标、且这些目标在验证环境里各自需要不同控制的时候。以我个人经验来说真正理解代理模式靠的不是背定义而是多问自己几次哪些逻辑其实不属于核心业务它有没有可能被抽离抽离之后用什么机制塞回去带着这些问题去看 Spring AOP、看 RPC 框架的调用链、看 ORM 的延迟加载你会发现代理模式早已无处不在。希望这篇内容能帮你把这条线彻底打通。