📌大厂规范:Java项目工具类 — 01_字符串类型转换工具类(Java企业级代码)
大白话说Java设计模式-43-访问者模式(源码剖析篇):JDK / Spring 中的访问者实现
📌一句话本质:访问者在源码里不是"操作分离"的形象化,而是"元素 + 访问者 + accept/visit"三件套的工业级架构。
🏷️ 标签:访问者模式 / ElementVisitor / ClassVisitor / SpEL 🎯 适合:中高级后端 / 想读懂主流框架源码的工程师
目录
- 一、为什么读访问者源码?一张地图先看清
- 二、JDK 源码剖析:ElementVisitor 元素访问者
- 三、ASM 源码剖析:ClassVisitor 字节码访问者
- 四、Spring 源码剖析:SpEL 表达式访问者
- 五、为什么框架作者这么设计?
- 六、借鉴到我们项目:3 个"抄作业"实践
- 七、本篇小结 + 下一模式预告
一、为什么读访问者源码?一张地图先看清
访问者模式在 JDK 和 Spring 里出现得很经典——ElementVisitor/ClassVisitor/ SpEL 都是访问者思想。
源码地图:
| 类别 | 框架 / 中间件 | 访问者接口 | 元素 |
|---|---|---|---|
| JDK | javax.lang.model | ElementVisitor | Element |
| JDK | javax.tools | DiagnosticListener | Diagnostic |
| ASM | org.objectweb.asm | ClassVisitor | Class 字节码 |
| ASM | org.objectweb.asm | MethodVisitor | Method 字节码 |
| Spring | spring-expression | SpelNodeVisitor | SpelNode |
| JavaParser | com.github.javaparser | VoidVisitor | AST |
我们按"ElementVisitor → ClassVisitor → SpEL"的顺序来剖析。
二、JDK 源码剖析:ElementVisitor 元素访问者
2.1ElementVisitor:访问者接口
源码位置:
- OpenJDK 17
src/java.compiler/share/classes/javax/lang/model/element/ElementVisitor.java
核心源码:
/** * ✅ 访问者接口:元素访问者 */publicinterfaceElementVisitor<R,P>{/** * ✅ 默认 visit(兜底) */Rvisit(Elemente,Pp);/** * ✅ 访问包 */RvisitPackage(PackageElemente,Pp);/** * ✅ 访问类型 */RvisitType(TypeElemente,Pp);/** * 访问变量 */RvisitVariable(VariableElemente,Pp);/** * 访问可执行元素(方法/构造器) */RvisitExecutable(ExecutableElemente,Pp);/** * 访问类型参数 */RvisitTypeParameter(TypeParameterElemente,Pp);/** * 访问未知元素 */RvisitUnknown(Elemente,Pp);}关键点:
| 关键点 | 解释 |
|---|---|
R | visit 返回类型 |
P | visit 参数类型 |
| 多种 visit | 每个 Element 子类对应 |
| 泛型 | 灵活 |
2.2AbstractElementVisitor6:默认实现
核心源码:
/** * ✅ 默认访问者 */publicabstractclassAbstractElementVisitor6<R,P>implementsElementVisitor<R,P>{@OverridepublicRvisit(Elemente,Pp){// ✅ 默认调 visitUnknownreturne.accept(this,p);}@OverridepublicRvisitPackage(PackageElemente,Pp){returnvisitUnknown(e,p);}@OverridepublicRvisitType(TypeElemente,Pp){returnvisitUnknown(e,p);}// ... 其他默认实现都调 visitUnknown}关键点:
| 关键点 | 解释 |
|---|---|
| 默认调 visitUnknown | 兜底 |
| 业务方可覆盖 | 自定义 |
| 模板方法 | 父类定骨架 |
2.3Element.accept():双重分派
核心源码:
/** * ✅ 元素接口 */publicinterfaceElement{/** * ✅ 接受访问者(双重分派) */<R,P>Raccept(ElementVisitor<R,P>v,Pp);}具体实现(TypeElement):
/** * ✅ TypeElement.accept() */publicinterfaceTypeElementextendsElement,...{@Override<R,P>Raccept(ElementVisitor<R,P>v,Pp){// ✅ 调 visitor.visitType(this, p)returnv.visitType(this,p);}}关键点:
| 关键点 | 解释 |
|---|---|
| 元素.accept(visitor) | 接受访问者 |
| 元素调 visitor.visitXxx | 双重分派 |
| 编译期决定 | 类型检查 |
2.4 完整访问者调用链
Element element = ...; ElementVisitor<R, P> visitor = new MyVisitor(); element.accept(visitor, p); ↓ element.accept() // 调 visitor.visitXxx ↓ visitor.visitType(this, p) // 编译期决定 ↓ MyVisitor.visitType(typeElement, p) // 业务方实现2.5ElementVisitor的"作者选择"
| 设计点 | 选择 | 原因 |
|---|---|---|
| 泛型 | R, P | 灵活 |
| 兜底 | visitUnknown | 默认 |
| 双重分派 | accept/visit | 编译期决定 |
| 默认实现 | AbstractElementVisitor6 | 简化 |
三、ASM 源码剖析:ClassVisitor 字节码访问者
3.1ClassVisitor:字节码访问者
源码位置:
- ASM 9.x
org.objectweb.asm.ClassVisitor
核心源码:
/** * ✅ 访问者:Class 字节码 */publicabstractclassClassVisitor{protectedfinalintapi;protectedClassVisitorcv;publicClassVisitor(intapi){this(api,null);}publicClassVisitor(intapi,ClassVisitorclassVisitor){this.api=api;this.cv=classVisitor;}/** * ✅ 访问类 */publicvoidvisit(intversion,intaccess,Stringname,Stringsignature,StringsuperName,String[]interfaces){if(cv!=null){cv.visit(version,access,name,signature,superName,interfaces);}}/** * ✅ 访问字段 */publicFieldVisitorvisitField(intaccess,Stringname,Stringdesc,Stringsignature,Objectvalue){if(cv!=null){returncv.visitField(access,name,desc,signature,value);}returnnull;}/** * ✅ 访问方法 */publicMethodVisitorvisitMethod(intaccess,Stringname,Stringdesc,Stringsignature,String[]exceptions){if(cv!=null){returncv.visitMethod(access,name,desc,signature,exceptions);}returnnull;}/** * 访问结束 */publicvoidvisitEnd(){if(cv!=null){cv.visitEnd();}}}关键点:
| 关键点 | 解释 |
|---|---|
| 链式 cv | 多访问者串联 |
| 兜底 null | 子访问者可选 |
| 多种 visit | 字段/方法/类 |
3.2ClassReader:元素
核心源码:
/** * ✅ 元素:ClassReader */publicclassClassReader{publicvoidaccept(ClassVisitorclassVisitor,intparsingOptions){// 1️⃣ 读 class 文件// 2️⃣ 调 classVisitor.visit()// 3️⃣ 遍历字段,调 visitField()// 4️⃣ 遍历方法,调 visitMethod()}}3.3ClassWriter:访问者 + 写
核心源码:
/** * ✅ 访问者:ClassWriter(可写) */publicclassClassWriterextendsClassVisitor{publicbyte[]toByteArray(){// ✅ 生成字节码}}3.4 业务方:自定义 ClassVisitor
/** * ✅ 业务方:自定义 ClassVisitor */publicclassLogClassVisitorextendsClassVisitor{publicLogClassVisitor(ClassVisitorcv){super(Opcodes.ASM9,cv);}@OverridepublicMethodVisitorvisitMethod(intaccess,Stringname,Stringdesc,Stringsignature,String[]exceptions){log.info("Method: {} {}",name,desc);MethodVisitormv=super.visitMethod(access,name,desc,signature,exceptions);returnnewLogMethodVisitor(mv);}}3.5 完整 ASM 调用链
ClassReader reader = new ClassReader(bytes); ClassWriter writer = new ClassWriter(reader, 0); LogClassVisitor visitor = new LogClassVisitor(writer); reader.accept(visitor, 0); ↓ reader.accept() ↓ visitor.visit() // 访问类 visitor.visitField() // 访问字段 visitor.visitMethod() // 访问方法 visitor.visitEnd() // 访问结束 ↓ writer.toByteArray() // 生成字节码3.6ClassVisitor的"作者选择"
| 设计点 | 选择 | 原因 |
|---|---|---|
| 链式 cv | 多访问者 | 装饰器风格 |
| 多种 visit | 字段/方法/类 | 全覆盖 |
| 兜底 null | 简化 | 灵活 |
| 字节码 API | 低级 | 性能 |
四、Spring 源码剖析:SpEL 表达式访问者
4.1SpelNode:元素
源码位置:
- Spring Expression 6.1.x
spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNode.java
核心源码:
/** * ✅ 元素:SpelNode */publicabstractclassSpelNodeimplements...{/** * ✅ 接受访问者 */publicabstract<T>Taccept(SpelNodeVisitor<T>visitor);}4.2SpelNodeVisitor:访问者接口
核心源码:
/** * ✅ 访问者接口 */publicinterfaceSpelNodeVisitor<T>{/** * ✅ 默认 visit */Tvisit(SpelNodenode);/** * 复合 */TvisitComp(Compnode);/** * 字面量 */TvisitLiteral(Literalnode);/** * 方法/构造器引用 */TvisitMethodRef(MethodReferencenode);/** * 属性引用 */TvisitPropertyRef(PropertyReferencenode);// ... 其他}4.3SpelNodeImpl:元素实现
核心源码:
/** * ✅ 元素实现:Literal */publicclassLiteralextendsSpelNodeImpl{privatefinalObjectvalue;@Overridepublic<T>Taccept(SpelNodeVisitor<T>visitor){// ✅ 调 visitor.visitLiteralreturnvisitor.visitLiteral(this);}}4.4 业务方:自定义 SpelNodeVisitor
/** * ✅ 业务方:自定义 SpelNodeVisitor */publicclassLoggingSpelNodeVisitorimplementsSpelNodeVisitor<Void>{@OverridepublicVoidvisit(SpelNodenode){log.info("Node: {}",node.getClass().getSimpleName());returnnull;}@OverridepublicVoidvisitLiteral(Literalnode){log.info("Literal: {}",node.getLiteralValue());returnnull;}}4.5SpelNodeVisitor的"作者选择"
| 设计点 | 选择 | 原因 |
|---|---|---|
| 多种 visit | AST 节点全覆盖 | 灵活 |
| 默认 visit | 兜底 | 简化 |
| 泛型 T | 返回值灵活 | 易用 |
五、为什么框架作者这么设计?
读源码看的是"实现",悟的是"设计哲学"。
5.1 框架作者选访问者的 4 个核心考虑
| 序号 | 考虑 | 举例 |
|---|---|---|
| ① | 数据结构稳定 | AST / 字节码 |
| ② | 操作多变 | 编译/字节码增强 |
| ③ | 多种操作作用于多种节点 | 类型检查/代码生成 |
| ④ | 可扩展 | 加新操作只加访问者 |
5.2 访问者 vs 策略:源码里怎么选?
| 场景 | 选哪个 | 原因 |
|---|---|---|
| 多种操作作用于多种节点 | 访问者 | 数据结构稳定 |
| 单一算法切换 | 策略 | 算法独立 |
| AST 遍历 | 访问者 | 多种分析 |
| 算法组合 | 策略 | 简单 |
5.3 框架作者不选访问者的 3 种情况
| 序号 | 场景 | 原因 |
|---|---|---|
| ① | 数据结构常变 | 加元素改 N 个访问者 |
| ② | 操作少 | 没必要 |
| ③ | 对象简单 | 用不上 |
5.4 访问者在企业级应用的 3 种典型用法
用法 1:AST 遍历 ElementVisitor / SpelNodeVisitor → 多种分析 用法 2:字节码增强 ClassVisitor / MethodVisitor → AOP / Lombok 用法 3:业务规则 营销规则 / 报表导出 → 数据结构稳定六、借鉴到我们项目:3 个"抄作业"实践
6.1 实践 1:AST 业务分析
场景:大白商城自定义规则表达式分析。
完整实现:
/** * 借鉴 SpelNodeVisitor:自定义业务 AST 访问者 */publicclassRuleAstVisitorimplementsRuleNodeVisitor<Void>{@OverridepublicVoidvisitExpression(RuleExpressionnode){log.info("Rule: {}",node.getExpression());returnnull;}@OverridepublicVoidvisitCondition(RuleConditionnode){log.info("Condition: field={}, op={}, value={}",node.getField(),node.getOperator(),node.getValue());returnnull;}@OverridepublicVoidvisitAction(RuleActionnode){log.info("Action: type={}, params={}",node.getActionType(),node.getParams());returnnull;}}6.2 实践 2:业务对象数据采集
场景:大白商城业务对象多维数据采集(统计/导出/分析)。
完整实现:
/** * 借鉴 ClassVisitor:业务对象访问者 */publicclassBusinessDataCollectorimplementsBusinessVisitor{privatefinalMap<String,Object>data=newHashMap<>();@Overridepublicvoidvisit(Goodsgoods){data.put("goodsCount",1);data.put("goodsTotal",data.getOrDefault("goodsTotal",0.0)+goods.getPrice().doubleValue());}@Overridepublicvoidvisit(Orderorder){data.put("orderCount",1);data.put("orderTotal",data.getOrDefault("orderTotal",0.0)+order.getTotal().doubleValue());}@Overridepublicvoidvisit(Couponcoupon){data.put("couponCount",1);data.put("couponTotal",data.getOrDefault("couponTotal",0.0)+coupon.getAmount().doubleValue());}publicMap<String,Object>getData(){returndata;}}6.3 实践 3:多维度分析
场景:大白商城订单多维度分析(销售/利润/用户)。
完整实现:
/** * 借鉴 ElementVisitor:多维度分析 */publicclassMultiDimensionAnalyzerimplementsMarketingVisitor{privatedoublesales=0;privatedoubleprofit=0;privateMap<String,Integer>categoryCount=newHashMap<>();@Overridepublicvoidvisit(Goodsgoods){sales+=goods.getPrice().doubleValue();profit+=goods.getPrice().doubleValue()*0.2;}@Overridepublicvoidvisit(Orderorder){sales+=order.getTotal().doubleValue();}@Overridepublicvoidvisit(Couponcoupon){// 优惠券对利润影响profit-=coupon.getAmount().doubleValue()*0.1;}publicvoidprintReport(){log.info("【分析】销售额: {}",sales);log.info("【分析】利润: {}",profit);log.info("【分析】分类: {}",categoryCount);}}收益:
| 优化点 | 效果 |
|---|---|
| AST 业务分析 | 规则灵活扩展 |
| 业务对象采集 | 多维数据 |
| 多维度分析 | 一遍采集多维数据 |
七、本篇小结 + 下一模式预告
7.1 本篇小结(5 个核心要点)
- JDK ElementVisitor:
Element.accept() / ElementVisitor.visitXxx双重分派。 - ASM ClassVisitor:链式 cv + visitField/visitMethod/visitEnd。
- Spring SpEL:
SpelNode.accept() / SpelNodeVisitor.visitXxx表达式访问。 - 设计哲学:元素 + 访问者 + 双重分派。
- 借鉴价值:AST 业务分析 / 数据采集 / 多维度分析,任何"操作多变"都用访问者。
7.2 一句话总结
访问者在源码里不是"操作分离"的形象化,而是"元素 + 访问者 + accept/visit"三件套的工业级架构。ElementVisitor 遍历 AST,ClassVisitor 访问字节码,SpelNodeVisitor 遍历表达式。学访问者,先学 ElementVisitor 和 ClassVisitor。
7.3 知识脑图
访问者源码剖析 ├── JDK │ ├── ElementVisitor │ │ ├── visit / visitPackage / visitType / visitVariable │ │ └── visitExecutable / visitTypeParameter / visitUnknown │ ├── AbstractElementVisitor6(默认) │ └── Element.accept()(双重分派) ├── ASM │ ├── ClassVisitor │ │ ├── visit() / visitField() / visitMethod() / visitEnd() │ │ └── 链式 cv │ ├── ClassReader(元素) │ ├── ClassWriter(写) │ └── MethodVisitor ├── Spring │ ├── SpelNode(元素) │ ├── SpelNodeVisitor(访问者) │ └── SpelNodeImpl └── 借鉴价值 ├── AST 业务分析 ├── 业务对象采集 └── 多维度分析7.4 下一模式预告
第 44 篇【解释器模式 - 业务实战篇】:大白商城优惠规则表达式的"自定义语言"
下一篇我们会实战解释器模式,回答 4 个问题:
- 优惠规则表达式(满 100 减 10 / 8 折 / 立减 50)怎么解析执行?
- 解释器 vs 策略 / 状态模式怎么区分?
- Spring SpEL 怎么用解释器?
- Regex / JavaCC 怎么借鉴解释器?
并附完整的优惠规则解释器实战代码(含表达式 + 终结符 + 非终结符 + 单元测试),可直接复制到 IDEA 跑。
觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯