大白话说Java设计模式-43-访问者模式(源码剖析篇)

📌 PDF :AI人工智能 --- 大模型微调与部署实战项目

大白话说Java设计模式-43-访问者模式(源码剖析篇):JDK / Spring 中的访问者实现

📌 一句话本质:访问者在源码里不是"操作分离"的形象化,而是"元素 + 访问者 + accept/visit"三件套的工业级架构。

🏷️ 标签:访问者模式 / ElementVisitor / ClassVisitor / SpEL 🎯 适合:中高级后端 / 想读懂主流框架源码的工程师


目录

  • 一、为什么读访问者源码?一张地图先看清
  • [二、JDK 源码剖析:ElementVisitor 元素访问者](#二、JDK 源码剖析:ElementVisitor 元素访问者)
  • [三、ASM 源码剖析:ClassVisitor 字节码访问者](#三、ASM 源码剖析:ClassVisitor 字节码访问者)
  • [四、Spring 源码剖析:SpEL 表达式访问者](#四、Spring 源码剖析:SpEL 表达式访问者)
  • 五、为什么框架作者这么设计?
  • [六、借鉴到我们项目:3 个"抄作业"实践](#六、借鉴到我们项目: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.1 ElementVisitor:访问者接口

源码位置

  • OpenJDK 17
  • src/java.compiler/share/classes/javax/lang/model/element/ElementVisitor.java

核心源码

java 复制代码
/**
 * ✅ 访问者接口:元素访问者
 */
public interface ElementVisitor<R, P> {
    /**
     * ✅ 默认 visit(兜底)
     */
    R visit(Element e, P p);

    /**
     * ✅ 访问包
     */
    R visitPackage(PackageElement e, P p);

    /**
     * ✅ 访问类型
     */
    R visitType(TypeElement e, P p);

    /**
     * 访问变量
     */
    R visitVariable(VariableElement e, P p);

    /**
     * 访问可执行元素(方法/构造器)
     */
    R visitExecutable(ExecutableElement e, P p);

    /**
     * 访问类型参数
     */
    R visitTypeParameter(TypeParameterElement e, P p);

    /**
     * 访问未知元素
     */
    R visitUnknown(Element e, P p);
}

关键点

关键点 解释
R visit 返回类型
P visit 参数类型
多种 visit 每个 Element 子类对应
泛型 灵活

2.2 AbstractElementVisitor6:默认实现

核心源码

java 复制代码
/**
 * ✅ 默认访问者
 */
public abstract class AbstractElementVisitor6<R, P> 
        implements ElementVisitor<R, P> {

    @Override
    public R visit(Element e, P p) {
        // ✅ 默认调 visitUnknown
        return e.accept(this, p);
    }

    @Override
    public R visitPackage(PackageElement e, P p) {
        return visitUnknown(e, p);
    }

    @Override
    public R visitType(TypeElement e, P p) {
        return visitUnknown(e, p);
    }

    // ... 其他默认实现都调 visitUnknown
}

关键点

关键点 解释
默认调 visitUnknown 兜底
业务方可覆盖 自定义
模板方法 父类定骨架

2.3 Element.accept():双重分派

核心源码

java 复制代码
/**
 * ✅ 元素接口
 */
public interface Element {
    /**
     * ✅ 接受访问者(双重分派)
     */
    <R, P> R accept(ElementVisitor<R, P> v, P p);
}

具体实现(TypeElement):

java 复制代码
/**
 * ✅ TypeElement.accept()
 */
public interface TypeElement extends Element, ... {
    @Override
    <R, P> R accept(ElementVisitor<R, P> v, P p) {
        // ✅ 调 visitor.visitType(this, p)
        return v.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.5 ElementVisitor 的"作者选择"

设计点 选择 原因
泛型 R, P 灵活
兜底 visitUnknown 默认
双重分派 accept/visit 编译期决定
默认实现 AbstractElementVisitor6 简化

三、ASM 源码剖析:ClassVisitor 字节码访问者

3.1 ClassVisitor:字节码访问者

源码位置

  • ASM 9.x
  • org.objectweb.asm.ClassVisitor

核心源码

java 复制代码
/**
 * ✅ 访问者:Class 字节码
 */
public abstract class ClassVisitor {

    protected final int api;
    protected ClassVisitor cv;

    public ClassVisitor(int api) {
        this(api, null);
    }

    public ClassVisitor(int api, ClassVisitor classVisitor) {
        this.api = api;
        this.cv = classVisitor;
    }

    /**
     * ✅ 访问类
     */
    public void visit(int version, int access, String name, 
                       String signature, String superName, String[] interfaces) {
        if (cv != null) {
            cv.visit(version, access, name, signature, superName, interfaces);
        }
    }

    /**
     * ✅ 访问字段
     */
    public FieldVisitor visitField(int access, String name, String desc, 
                                    String signature, Object value) {
        if (cv != null) {
            return cv.visitField(access, name, desc, signature, value);
        }
        return null;
    }

    /**
     * ✅ 访问方法
     */
    public MethodVisitor visitMethod(int access, String name, String desc, 
                                      String signature, String[] exceptions) {
        if (cv != null) {
            return cv.visitMethod(access, name, desc, signature, exceptions);
        }
        return null;
    }

    /**
     * 访问结束
     */
    public void visitEnd() {
        if (cv != null) {
            cv.visitEnd();
        }
    }
}

关键点

关键点 解释
链式 cv 多访问者串联
兜底 null 子访问者可选
多种 visit 字段/方法/类

3.2 ClassReader:元素

核心源码

java 复制代码
/**
 * ✅ 元素:ClassReader
 */
public class ClassReader {
    public void accept(ClassVisitor classVisitor, int parsingOptions) {
        // 1️⃣ 读 class 文件
        // 2️⃣ 调 classVisitor.visit()
        // 3️⃣ 遍历字段,调 visitField()
        // 4️⃣ 遍历方法,调 visitMethod()
    }
}

3.3 ClassWriter:访问者 + 写

核心源码

java 复制代码
/**
 * ✅ 访问者:ClassWriter(可写)
 */
public class ClassWriter extends ClassVisitor {

    public byte[] toByteArray() {
        // ✅ 生成字节码
    }
}

3.4 业务方:自定义 ClassVisitor

java 复制代码
/**
 * ✅ 业务方:自定义 ClassVisitor
 */
public class LogClassVisitor extends ClassVisitor {

    public LogClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM9, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, 
                                      String signature, String[] exceptions) {
        log.info("Method: {} {}", name, desc);
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        return new LogMethodVisitor(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.6 ClassVisitor 的"作者选择"

设计点 选择 原因
链式 cv 多访问者 装饰器风格
多种 visit 字段/方法/类 全覆盖
兜底 null 简化 灵活
字节码 API 低级 性能

四、Spring 源码剖析:SpEL 表达式访问者

4.1 SpelNode:元素

源码位置

  • Spring Expression 6.1.x
  • spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNode.java

核心源码

java 复制代码
/**
 * ✅ 元素:SpelNode
 */
public abstract class SpelNode implements ... {

    /**
     * ✅ 接受访问者
     */
    public abstract <T> T accept(SpelNodeVisitor<T> visitor);
}

4.2 SpelNodeVisitor:访问者接口

核心源码

java 复制代码
/**
 * ✅ 访问者接口
 */
public interface SpelNodeVisitor<T> {

    /**
     * ✅ 默认 visit
     */
    T visit(SpelNode node);

    /**
     * 复合
     */
    T visitComp(Comp node);

    /**
     * 字面量
     */
    T visitLiteral(Literal node);

    /**
     * 方法/构造器引用
     */
    T visitMethodRef(MethodReference node);

    /**
     * 属性引用
     */
    T visitPropertyRef(PropertyReference node);

    // ... 其他
}

4.3 SpelNodeImpl:元素实现

核心源码

java 复制代码
/**
 * ✅ 元素实现:Literal
 */
public class Literal extends SpelNodeImpl {
    
    private final Object value;

    @Override
    public <T> T accept(SpelNodeVisitor<T> visitor) {
        // ✅ 调 visitor.visitLiteral
        return visitor.visitLiteral(this);
    }
}

4.4 业务方:自定义 SpelNodeVisitor

java 复制代码
/**
 * ✅ 业务方:自定义 SpelNodeVisitor
 */
public class LoggingSpelNodeVisitor implements SpelNodeVisitor<Void> {

    @Override
    public Void visit(SpelNode node) {
        log.info("Node: {}", node.getClass().getSimpleName());
        return null;
    }

    @Override
    public Void visitLiteral(Literal node) {
        log.info("Literal: {}", node.getLiteralValue());
        return null;
    }
}

4.5 SpelNodeVisitor 的"作者选择"

设计点 选择 原因
多种 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 业务分析

场景:大白商城自定义规则表达式分析。

完整实现

java 复制代码
/**
 * 借鉴 SpelNodeVisitor:自定义业务 AST 访问者
 */
public class RuleAstVisitor implements RuleNodeVisitor<Void> {

    @Override
    public Void visitExpression(RuleExpression node) {
        log.info("Rule: {}", node.getExpression());
        return null;
    }

    @Override
    public Void visitCondition(RuleCondition node) {
        log.info("Condition: field={}, op={}, value={}", 
                node.getField(), node.getOperator(), node.getValue());
        return null;
    }

    @Override
    public Void visitAction(RuleAction node) {
        log.info("Action: type={}, params={}", 
                node.getActionType(), node.getParams());
        return null;
    }
}

6.2 实践 2:业务对象数据采集

场景:大白商城业务对象多维数据采集(统计/导出/分析)。

完整实现

java 复制代码
/**
 * 借鉴 ClassVisitor:业务对象访问者
 */
public class BusinessDataCollector implements BusinessVisitor {

    private final Map<String, Object> data = new HashMap<>();

    @Override
    public void visit(Goods goods) {
        data.put("goodsCount", 1);
        data.put("goodsTotal", 
                data.getOrDefault("goodsTotal", 0.0) 
                + goods.getPrice().doubleValue());
    }

    @Override
    public void visit(Order order) {
        data.put("orderCount", 1);
        data.put("orderTotal", 
                data.getOrDefault("orderTotal", 0.0) 
                + order.getTotal().doubleValue());
    }

    @Override
    public void visit(Coupon coupon) {
        data.put("couponCount", 1);
        data.put("couponTotal", 
                data.getOrDefault("couponTotal", 0.0) 
                + coupon.getAmount().doubleValue());
    }

    public Map<String, Object> getData() {
        return data;
    }
}

6.3 实践 3:多维度分析

场景:大白商城订单多维度分析(销售/利润/用户)。

完整实现

java 复制代码
/**
 * 借鉴 ElementVisitor:多维度分析
 */
public class MultiDimensionAnalyzer implements MarketingVisitor {

    private double sales = 0;
    private double profit = 0;
    private Map<String, Integer> categoryCount = new HashMap<>();

    @Override
    public void visit(Goods goods) {
        sales += goods.getPrice().doubleValue();
        profit += goods.getPrice().doubleValue() * 0.2;
    }

    @Override
    public void visit(Order order) {
        sales += order.getTotal().doubleValue();
    }

    @Override
    public void visit(Coupon coupon) {
        // 优惠券对利润影响
        profit -= coupon.getAmount().doubleValue() * 0.1;
    }

    public void printReport() {
        log.info("【分析】销售额: {}", sales);
        log.info("【分析】利润: {}", profit);
        log.info("【分析】分类: {}", categoryCount);
    }
}

收益

优化点 效果
AST 业务分析 规则灵活扩展
业务对象采集 多维数据
多维度分析 一遍采集多维数据

七、本篇小结 + 下一模式预告

7.1 本篇小结(5 个核心要点)

  1. JDK ElementVisitorElement.accept() / ElementVisitor.visitXxx 双重分派。
  2. ASM ClassVisitor链式 cv + visitField/visitMethod/visitEnd
  3. Spring SpELSpelNode.accept() / SpelNodeVisitor.visitXxx 表达式访问。
  4. 设计哲学元素 + 访问者 + 双重分派
  5. 借鉴价值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 个问题:

  1. 优惠规则表达式(满 100 减 10 / 8 折 / 立减 50)怎么解析执行?
  2. 解释器 vs 策略 / 状态模式怎么区分?
  3. Spring SpEL 怎么用解释器?
  4. Regex / JavaCC 怎么借鉴解释器?

并附完整的优惠规则解释器实战代码(含表达式 + 终结符 + 非终结符 + 单元测试),可直接复制到 IDEA 跑


觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯

相关推荐
丘山一郎9 小时前
Spring 中的IOC控制反转 和DI依赖注入
java·后端·spring
码视野9 小时前
基于 Spring Boot + Vue3 的【大学英语四六级 (CET-4/6) 作文智能评分与句式润色系统】设计与实现(含PRD/三端高保真源码/大屏)
java·前端·人工智能·spring boot·后端·vue3
MetaLite9 小时前
Spring-AOP自调用为什么失效-AopContext真能解决吗
java·后端·spring
大模型码小白10 小时前
AI 对话流性能调优:万级消息的虚拟滚动落地
java·大数据·前端·javascript·人工智能·算法·机器学习
许彰午10 小时前
14-字段级SM4加密与SM2签名
java·低代码·架构
许彰午12 小时前
13-describe元数据输出
java·低代码·架构·状态模式
用户37215742613513 小时前
Java 设置 Word 文档编辑限制并允许指定区域编辑
java
宠友信息13 小时前
社区类源码开发实践中的仿小红书系统技术要点分析
java·spring boot·redis·mysql·uni-app·vue·内容运营
用户37215742613513 小时前
Java 设置 PDF 表单域只读或扁平化
java