☰
代理模式:给线上接口加日志,你改了多少个类?
2026/10/22 7:24:56 网站建设 项目流程

作者:鱼宵 | 「Java 后端实战精通营」设计模式系列・第 8 篇

一、真实场景:给 10 个 Service 加日志,你打算动几个类?

回忆第 1 篇那份外卖烂代码,总结表里有一条预告:“调用方直接 new 具体类(违反依赖倒置,实现换不了)→ 第 8 课代理”。这篇就是来兑现这个承诺的。

先想个场景:你接手一个老项目,老板说"线上订单接口调用得有点乱,给所有订单方法加个日志,打印入参、返回值、耗时"。你的第一反应是什么?

最朴素的做法:打开OrderService、UserService、PaymentService……每个方法第一行打印入参、最后一行打印耗时。10 个类 50 处改动。两周后老板说"日志加上 traceId"——这 50 处又得全部打开改一遍。

这就是日志、缓存、权限、事务这类横切需求最恶心的地方:它跟业务逻辑半毛钱关系没有,但它逼你把所有业务类都打开一遍。

代理模式就是给这个烂摊子开的药方:业务类一行不改,在外面套个"替身",日志全在替手里打。

代码全在design-patterns仓库的lesson-08工程里,clone 下来跑一遍,你会亲眼看到:OrderServiceImpl一个字没动,createOrder前后却自动多了两行日志。

二、核心原理:给对象找个"替身",老代码一行不动

2.1 代理三要素:接口、真实对象、代理对象

代理模式(Proxy):给一个对象找个替身,外部通过替身访问真实对象——替身负责"能不能访问"和"访问前后干点啥"。

类比房产中介:你买二手房不找房东、找中介。中介带你看房、谈价、签合同,最后过户才见到房东——它控制了你能不能见房东(权限),还帮你干了一堆杂事。

落到代码上就三个角色:

角色干什么本课对应
抽象主题(接口)定义业务方法OrderService
真实主题(真实对象)真正干活的业务类OrderServiceImpl
代理对象持有真实对象引用,在外面加增强LogInvocationHandler生成的代理

为什么"不改老代码"成立?因为调用方只认识接口:你把new OrderServiceImpl()换成代理对象,调用方代码一个字不改,根本不知道背后是真身还是替身。第 1 篇讲的依赖倒置,在这兑现了。

2.2 静态代理 vs JDK 动态代理:手写十个代理类,还是一个就够?

最笨的写法叫静态代理:手写一个OrderServiceProxy implements OrderService,每个方法里"打日志→调真身→打日志"。明天给UserService加日志?再手写一个 Proxy——业务接口有多少个,你就得写多少个代理类,跟开头改 50 处有什么区别?

JDK 动态代理就是来消灭这个重复的:代理类不用你手写,JVM 在运行时用字节码现场生成一个。你只写一个 handler,所有接口、所有方法统一走这一份逻辑。核心就两个 API:

  • InvocationHandler接口:实现它,把"前置增强→转发真实对象→后置增强"写在invoke()里。
  • Proxy.newProxyInstance(类加载器, 接口数组, handler):一行生成代理对象。

2.3 JDK 动态代理 vs CGLIB:面试官必问的取舍

JDK 动态代理CGLIB 代理
代理什么只能代理接口可以代理类(不需要接口)
原理运行时生成一个实现了接口的代理类用 ASM 生成被代理类的子类
final 类可以(final 类不影响接口代理)不行(final 不能被继承,子类生不出来)
final 方法不涉及不行(不能被重写)
性能生成快,调用走反射稍慢生成慢,调用走 FastClass 快
Spring 默认有接口用它没接口用它

面试被问"JDK 为什么只能代理接口"——答案就一句:原理是生成一个实现了目标接口的代理类,类没实现接口,JDK 就没东西可代理;这时才轮到 CGLIB 生成子类上场。

2.4 代理 vs 装饰器:长得几乎一样,区别在意图

装饰器模式的代码结构和代理一模一样:都实现同一接口、都持有真实对象、都在方法前后加东西。那它俩到底什么区别?

代理模式装饰器模式
目的控制能不能访问真实对象给真实对象加功能
谁决定包几层代理自己知道真实对象是谁调用方自己选包什么、包几层
典型场景权限校验、RPC 远程代理、延迟加载加日志、加缓存、加折扣
类比房产中介(它决定你能不能见房东)给手机贴膜加壳(你自己选贴什么膜)
一句话管"能不能进"管"进来了干点啥"

记住这句话:结构是一回事,意图是另一回事——Spring AOP 用动态代理实现,干的却是装饰器的活。

三、关键代码:八个类,一次跑通代理 + 装饰器

本课工程在lesson-08/src/main/java/com/dp/lesson08/,共 8 个类,先看代理主线。

业务接口——就两个方法,纯业务定义:

packagecom.dp.lesson08;/** * 订单服务接口(代理模式的"抽象主题")。 * 面向接口编程:真实实现和代理都实现这个接口, * 调用方拿到的是接口引用,根本不知道背后是真身还是代理。 */publicinterfaceOrderService{/** 创建订单 */StringcreateOrder(StringorderId,doubleamount);/** 查询订单 */StringqueryOrder(StringorderId);}

真实实现——第 1 篇坏代码的"干净版":纯业务,没有日志、没有耗时,干净得不像话:

packagecom.dp.lesson08;/** * 订单服务真实实现(代理模式的"真实主题")。 * 没有日志、没有耗时统计——纯业务逻辑。 * 想加日志?不想改这个类?→ 用代理在外面包一层就行。 */publicclassOrderServiceImplimplementsOrderService{@OverridepublicStringcreateOrder(StringorderId,doubleamount){// 纯业务:创建订单System.out.println("[真实实现] 创建订单:"+orderId+",金额 "+amount);return"订单 "+orderId+" 创建成功";}@OverridepublicStringqueryOrder(StringorderId){// 纯业务:查询订单System.out.println("[真实实现] 查询订单:"+orderId);return"订单 "+orderId+" 状态:已支付";}}

重头戏——JDK 动态代理的核心,所有"代理要干什么"都写在这一个类里:

packagecom.dp.lesson08;importjava.lang.reflect.InvocationHandler;importjava.lang.reflect.Method;/** * 日志动态代理(JDK 动态代理的 InvocationHandler)。 * 不管你调接口的哪个方法,我都在外面包一层: * 执行前打印方法名和参数;执行后打印返回值和耗时;异常时打印真实异常。 * 这就是"不改一行业务代码,统一加日志"的魔法。 */publicclassLogInvocationHandlerimplementsInvocationHandler{/** 被代理的真实对象(调用方法时最终要转发到它) */privatefinalObjecttarget;publicLogInvocationHandler(Objecttarget){this.target=target;}/** * 每次调用代理对象的任何方法,都会进到这里。 * @param proxy 代理对象本身(一般不用) * @param method 被调用的方法对象(反射用) * @param args 调用时传的参数 */@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{// ===== 前置增强:方法执行前打印日志 =====System.out.println("[代理前置] 调用方法:"+method.getName()+",参数:"+java.util.Arrays.toString(args));longstart=System.currentTimeMillis();// 记开始时间,算耗时用Objectresult;try{// ===== 转发给真实对象:真正的业务在这一行执行 =====result=method.invoke(target,args);}catch(Exceptione){// ===== 异常增强:真实方法抛的异常会被包成 InvocationTargetException// 不 getCause(),你打印出来的全是"反射的锅",根本看不到业务异常System.out.println("[代理异常] 方法 "+method.getName()+" 抛异常:"+e.getCause());throwe.getCause();// 真实异常继续往外抛,不吞掉}// ===== 后置增强:打印返回值和耗时 =====longcost=System.currentTimeMillis()-start;System.out.println("[代理后置] 方法 "+method.getName()+" 返回:"+result+",耗时 "+cost+" ms");returnresult;}}

入口类——三行生成代理,然后像普通接口一样调用:

packagecom.dp.lesson08;importjava.lang.reflect.Proxy;/** * 第 8 课入口:演示代理模式和装饰器模式。 * 部分 1:JDK 动态代理——给订单服务统一加日志/耗时 * 部分 2:装饰器模式——给计算器一层一层包功能 */publicclassMain{publicstaticvoidmain(String[]args){System.out.println("========== 第 8 课:代理模式 + 装饰器模式 ==========");System.out.println();// ===== 部分 1:JDK 动态代理 =====System.out.println("---------- 部分 1:JDK 动态代理(给订单服务加日志) ----------");OrderServiceImplrealService=newOrderServiceImpl();// 真实对象,裸的没日志// 三行生成代理:(类加载器, 要代理的接口数组, 代理要干什么)OrderServiceproxyService=(OrderService)Proxy.newProxyInstance(OrderService.class.getClassLoader(),// 类加载器:随便传一个newClass[]{OrderService.class},// 要代理的接口列表newLogInvocationHandler(realService)// 代理要干什么(加日志));// 调用方拿到的是接口引用——它根本不知道背后是真身还是代理System.out.println(">>> 调 createOrder:");Stringresult1=proxyService.createOrder("A1001",150.0);System.out.println("返回值:"+result1);System.out.println();System.out.println(">>> 调 queryOrder:");Stringresult2=proxyService.queryOrder("A1001");System.out.println("返回值:"+result2);System.out.println();// ===== 部分 2:装饰器模式 =====System.out.println("---------- 部分 2:装饰器模式(给计算器一层一层包功能) ----------");System.out.println(">>> 裸计算器:");Calculatorbare=newDefaultCalculator();System.out.println("结果:"+bare.add(100,50));System.out.println();System.out.println(">>> 包日志装饰器:");CalculatorwithLog=newLogDecorator(newDefaultCalculator());System.out.println("结果:"+withLog.add(100,50));System.out.println();// 两层装饰器套在一起:外层折扣包着内层日志,日志包着裸计算器System.out.println(">>> 包日志 + 包折扣(两层装饰器):");CalculatorwithLogAndDiscount=newDiscountDecorator(newLogDecorator(newDefaultCalculator()));System.out.println("最终结果:"+withLogAndDiscount.add(100,50));System.out.println();System.out.println("========== 跑完:代理管「能不能访问」,装饰器管「加什么功能」 ==========");}}

装饰器那半边四个类,结构和代理一模一样——都是接口 + 持有被装饰对象:

packagecom.dp.lesson08;/** 计算器接口(装饰器的抽象组件) */publicinterfaceCalculator{/** 两个数相加 */doubleadd(doublea,doubleb);}
packagecom.dp.lesson08;/** 基础计算器(被装饰的对象):最朴素的实现 */publicclassDefaultCalculatorimplementsCalculator{@Overridepublicdoubleadd(doublea,doubleb){returna+b;// 裸计算,不加任何东西}}
packagecom.dp.lesson08;/** * 日志装饰器(第 1 层外壳)。 * 套路:实现同一接口 → 持有被装饰对象 → 方法里先增强、再转发、再增强。 */publicclassLogDecoratorimplementsCalculator{/** 被装饰的计算器:可以是裸的,也可以是再包了一层装饰器的 */privatefinalCalculatordelegate;publicLogDecorator(Calculatordelegate){this.delegate=delegate;}@Overridepublicdoubleadd(doublea,doubleb){// 增强 1:执行前打印System.out.println("[日志装饰器] 准备计算:"+a+" + "+b);doubleresult=delegate.add(a,b);// 转发:真正的计算在它那做// 增强 2:执行后打印结果System.out.println("[日志装饰器] 计算完成:结果 = "+result);returnresult;}}
packagecom.dp.lesson08;/** * 折扣装饰器(第 2 层外壳):在日志装饰器外面再包一层,算完打 9 折。 * 想包几层包几层:裸计算器 → 包日志 → 包折扣 → 还能再包缓存…… */publicclassDiscountDecoratorimplementsCalculator{/** 被装饰的计算器 */privatefinalCalculatordelegate;/** 折扣率:9 折 */privatestaticfinaldoubleDISCOUNT=0.9;publicDiscountDecorator(Calculatordelegate){this.delegate=delegate;}@Overridepublicdoubleadd(doublea,doubleb){doubleraw=delegate.add(a,b);// 先让被装饰对象算一遍doublediscounted=raw*DISCOUNT;// 增强:打完折再返回System.out.println("[折扣装饰器] 原价 "+raw+",打 9 折后 = "+discounted);returndiscounted;}}

这段代码讲了什么:代理这边,OrderServiceImpl一个字没动,两个方法却自动前后带日志——一个 handler 管所有方法;装饰器这边,同一个DefaultCalculator,裸着算 150、包日志再包折扣算 135——功能是一层一层"包"上去的,老代码一行不改。

四、完整演示 + 实测输出

clone 下来后,在lesson-08目录执行(Windows + PowerShell):

cd C:\Users\haoji\Desktop\课程\design-patterns-journey\lesson-08$env:JAVA_HOME="C:\jdk\jdk-17.0.12"mvn clean install-DskipTests

实测输出(编译段):

[INFO] BUILD SUCCESS [INFO] Total time: 1.523 s

编译通过后运行(PATH 上的 java 可能是 JDK8,报 UnsupportedClassVersionError 就用全路径):

[Console]::OutputEncoding=[System.Text.Encoding]::GetEncoding(936)&"C:\jdk\jdk-17.0.12\bin\java.exe"-cp".\target\classes"com.dp.lesson08.Main

实测输出(本机真实跑出来的):

========== 第 8 课:代理模式 + 装饰器模式 ========== ---------- 部分 1:JDK 动态代理(给订单服务加日志) ---------- >>> 调 createOrder: [代理前置] 调用方法:createOrder,参数:[A1001, 150.0] [真实实现] 创建订单:A1001,金额 150.0 [代理后置] 方法 createOrder 返回:订单 A1001 创建成功,耗时 2 ms 返回值:订单 A1001 创建成功 >>> 调 queryOrder: [代理前置] 调用方法:queryOrder,参数:[A1001] [真实实现] 查询订单:A1001 [代理后置] 方法 queryOrder 返回:订单 A1001 状态:已支付,耗时 0 ms 返回值:订单 A1001 状态:已支付 ---------- 部分 2:装饰器模式(给计算器一层一层包功能) ---------- >>> 裸计算器: 结果:150.0 >>> 包日志装饰器: [日志装饰器] 准备计算:100.0 + 50.0 [日志装饰器] 计算完成:结果 = 150.0 结果:150.0 >>> 包日志 + 包折扣(两层装饰器): [日志装饰器] 准备计算:100.0 + 50.0 [日志装饰器] 计算完成:结果 = 150.0 [折扣装饰器] 原价 150.0,打 9 折后 = 135.0 最终结果:135.0 ========== 跑完:代理管「能不能访问」,装饰器管「加什么功能」 ==========

输出解读,重点看两个细节:

  • 代理部分:不管调createOrder还是queryOrder,前置日志、真实实现、后置日志三段式全自动套上。OrderServiceImpl一行没改——日志全是代理加的。这就是"一个 handler,所有方法统一增强"。
  • 装饰器部分:注意执行顺序——先打印"准备计算"(内层日志),再算 150,最后外层折扣才动手:“原价 150.0,打 9 折 = 135.0”。包在里面的先执行增强,包在外面的后执行,和 new 的嵌套顺序正好相反。

五、挑战题(改参数会怎样?答案在源码里)

  1. ⭐ 把DiscountDecorator的DISCOUNT = 0.9改成0.8,再跑 Main——"两层装饰器"最终结果变成多少?注意日志装饰器打印的"计算完成:结果 ="还是不是 150.0?为什么折扣率改了,内层日志的中间结果却没变?

  2. ⭐⭐ 把Proxy.newProxyInstance第二个参数new Class[]{OrderService.class}改成new Class[]{OrderServiceImpl.class}(传实现类而不是接口),再跑——控制台报什么异常?提示:看 2.3 的取舍表。

  3. ⭐⭐ 把LogInvocationHandlercatch 块里的e.getCause()临时改成e(直接打印反射异常对象本身),再在OrderServiceImpl.createOrder里加一行if (amount > 1000) throw new RuntimeException("金额超限"),传 2000 跑一次——你打印的异常信息里,还看得到"金额超限"这四个字吗?为什么真实业务异常会被包一层?

三题答案都藏在lesson-08源码里,clone 下来改一行就能验证。跑出来和你想的不一样?评论区贴出来一起查。

六、生产环境进阶:手写是为了讲原理,这些才是真实落点

日志、权限、限流这种通用增强,生产里直接用 Spring AOP(@Aspect),别自己写 InvocationHandler。但看懂手写版,下面这些真实落点你就都能秒懂:

  • Spring AOP 的默认策略:有接口用 JDK 动态代理,没接口用 CGLIB;加@EnableAspectJAutoProxy(proxyTargetClass = true)强制 CGLIB。面试问默认策略,背这一句。
  • @Transactional 为什么会失效:同一个类里 A 方法调本类 B 方法(this 调用),B 上的@Transactional不生效——因为事务就是个代理,只有走代理对象的调用才会被拦截,this 调自己等于绕过了代理。这个坑的根就在今天这篇的原理里。
  • MyBatis Mapper 代理:你写的UserMapper是个没有实现类的接口,为什么能直接@Autowired注入?因为 MyBatis 给它现场生成了一个 JDK 动态代理实现——你调userMapper.selectById(1),是代理帮你转发去执行 SQL 的。
  • JDK IO 流的装饰器:new BufferedInputStream(new FileInputStream("test.txt"))——裸文件流外面包一层缓冲区,还能继续包DataInputStream。你天天写的 IO 流,就是教科书级的装饰器。

说白了:手写这 8 个类,是为了让你亲手摸到"代理类运行时生成""一层一层包"这两个动作本身。

七、面试回答模板

Q1:代理模式和装饰器模式有什么区别?

一句话结论:结构几乎一样(都实现同一接口、持有真实对象引用),区别在意图——代理控制"能不能访问",装饰器负责"访问时加什么功能"。展开:代理典型场景是权限校验、远程代理、延迟加载;装饰器典型场景是加日志、加缓存、加折扣。本文章节 2.4 有完整对照表。

Q2:JDK 动态代理为什么只能代理接口?CGLIB 为什么不能代理 final 类?

一句话结论:JDK 动态代理是运行时生成实现了目标接口的代理类,没接口就没东西可代理;CGLIB 是生成被代理类的子类重写方法,final 类/方法不能被继承重写,所以代理不了。展开:JDK 底层走反射(Method.invoke),CGLIB 走 FastClass 调用更快。本文章节 2.3 有取舍表。

Q3:Spring AOP 默认用 JDK 代理还是 CGLIB?怎么切换?

一句话结论:有接口用 JDK 动态代理,没接口自动切 CGLIB;加@EnableAspectJAutoProxy(proxyTargetClass = true)强制 CGLIB。展开:经典副作用是 this 自调用不走代理,导致@Transactional失效。本文章节六有落点说明。

八、总结表

坑原因解法
加日志要动 N 个业务类横切需求硬塞进业务方法外面套代理/装饰器,业务类一行不改(章节三)
JDK 代理传了实现类报错JDK 动态代理只能代理接口有接口传接口数组;没接口换 CGLIB
异常信息全是 InvocationTargetExceptionmethod.invoke 把真实异常包了一层catch 里用e.getCause()拿真实异常再抛
this 自调用 @Transactional 失效自调用绕过了代理对象改走代理 bean 调用,章节六有解释
装饰器 delegate 套自己导致死循环误传成new LogDecorator(new LogDecorator(...))delegate 传"被装饰的对象"(裸计算器)

九、关于这个系列

本文是「Java 后端实战精通营」设计模式系列第 8 篇。第 1 篇那份外卖烂代码一篇篇开药方开到现在:本篇用代理兑现了"调用方直接 new 具体类"的处方。

  • 完整可运行工程:https://gitee.com/j67mk2/design-patterns(本课源码在lesson-08/,主类com.dp.lesson08.Main,代理 + 装饰器八个类全在里面)
  • 下一篇预告:《模式组合实战:策略+模板方法+责任链+观察者,搭一条订单全生命周期链路》。前面 8 课的模式都是单独演的,第 9 课把它们组合起来:创建订单→责任链校验→策略算优惠→模板方法固定流程→观察者发通知,每个模式各司其职,你会看到"组合而不是堆砌"长什么样。

互动提问:你项目里的日志、权限这种横切需求,现在是写在业务方法里,还是已经交给 AOP/代理了?有没有踩过 @Transactional 自调用失效的坑?评论区聊聊,我帮你看看你的代理该怎么包。

本文部分内容由 AI 辅助生成

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询