📌 PDF :AI人工智能 --- RAG知识库实战项目
大白话说Java设计模式-42-访问者模式(业务实战篇):大白商城营销规则计算的"数据结构与操作分离"
📌 一句话本质:访问者模式就是"把操作从数据结构中分离出来",让数据结构稳定,操作易扩展。
🏷️ 标签:访问者模式 / Java 设计模式 / 数据结构与操作分离 / 营销规则 / 大白商城 🎯 适合:初中级后端 / 经常被"操作扩展"困扰的工程师
目录
- 一、业务场景引入:为什么需要"操作分离"?
- 二、反面教材:硬编码操作的"灾难现场"
- [三、模式原理:访问者的"元素 + 访问者"](#三、模式原理:访问者的"元素 + 访问者")
- 四、实战代码:大白商城营销规则访问者完整实现
- [五、访问者 vs 策略 / 中介者](#五、访问者 vs 策略 / 中介者)
- [六、JDK 编译器 API 怎么用访问者?](#六、JDK 编译器 API 怎么用访问者?)
- [七、ASM / Spring SpEL 怎么借鉴访问者?](#七、ASM / Spring SpEL 怎么借鉴访问者?)
- [八、工程决策 Checklist](#八、工程决策 Checklist)
- 九、与其他模式协作
- [十、本篇小结 + 下篇预告](#十、本篇小结 + 下篇预告)
一、业务场景引入:为什么需要"操作分离"?
大白商城 2024 年营销中心遇到一个老大难:
"商品 / 订单 / 优惠券 3 种对象,要支持 8 种营销计算(满减/折扣/立减/送积分/送券/限时/拼团/秒杀),每加一种计算要改 3 个类!"
我打开代码一看:
java
/**
* ❌ 大白商城老的营销代码
*/
public class Goods {
public BigDecimal calculateDiscount(String rule) {
// ❌ 营销规则耦合在商品类
if ("FULL_REDUCE".equals(rule)) {
// 满减逻辑
} else if ("DISCOUNT".equals(rule)) {
// 折扣逻辑
} else if ("INSTANT_REDUCE".equals(rule)) {
// 立减逻辑
}
// ... 500 行
}
}
public class Order {
public BigDecimal calculateDiscount(String rule) {
// ❌ 又复制一遍
if ("FULL_REDUCE".equals(rule)) { ... }
// ... 500 行
}
}
public class Coupon {
public BigDecimal calculateDiscount(String rule) {
// ❌ 又复制一遍
// ... 500 行
}
}
这种代码的痛点:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 8 种规则 × 3 个类 = 24 处重复 | 改 1 处要改 24 处 |
| ② | 加新规则改 3 个类 | 改老代码 |
| ③ | 业务对象臃肿 | 商品类 500 行营销逻辑 |
| ④ | 难测试 | 业务和营销耦合 |
老板要的"加新规则只加 1 个类",怎么搞?
答案就是------访问者模式。
1.1 大白话讲透访问者
继续打比方:
场景:3 种对象(商品/订单/优惠券)要支持 8 种营销计算。
- 错误做法:把营销逻辑写在每个对象里
- 正确做法 :
- 数据结构(商品/订单/优惠券)保持稳定
- 每种营销规则封装成独立访问者
- 对象接受访问者,让访问者操作自己
- 加新规则?只加 1 个访问者类
访问者 = 把操作从数据结构中分离出来,数据结构稳定,操作易扩展。
1.2 访问者模式的 3 个真实场景
大白商城里,访问者模式用在:
| 场景 | "元素" | "访问者" |
|---|---|---|
| 营销规则 | 商品/订单/优惠券 | 满减/折扣/立减 |
| 报表生成 | 报表节点 | PDF/Excel/HTML |
| 编译器 | AST 节点 | 类型检查/代码生成 |
| 文件处理 | 文件节点 | 压缩/加密/分析 |
任何"数据结构稳定,操作多变"的场景,都用访问者。
二、反面教材:硬编码操作的"灾难现场"
我们看 4 个反面教材,看它们是怎么一步步崩的。
2.1 反面教材 v1:操作写在对象里
java
/**
* ❌ 反面教材 v1:操作写在对象里
*/
public class Goods {
public BigDecimal calculateFullReduce(...) { ... }
public BigDecimal calculateDiscount(...) { ... }
// 8 个方法
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 类臃肿 | 500 行 |
| ② | 加新规则要改类 | 改老代码 |
2.2 反面教材 v2:if-else 堆叠
java
/**
* ❌ 反面教材 v2:if-else 堆叠
*/
public BigDecimal calculate(String rule) {
if ("FULL_REDUCE".equals(rule)) { ... }
else if ("DISCOUNT".equals(rule)) { ... }
// 8 个 else if
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 业务臃肿 | 难维护 |
| ② | 加新规则要改方法 | 改老代码 |
2.3 反面教材 v3:完全分散
java
/**
* ❌ 反面教材 v3:完全分散
*/
public class GoodsDiscount { ... }
public class GoodsFullReduce { ... }
public class OrderDiscount { ... }
// 8 × 3 = 24 个类
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 类爆炸 | 24 个类 |
| ② | 重复逻辑 | 难维护 |
2.4 反面教材 v4:完全没抽象
java
/**
* ❌ 反面教材 v4:完全没抽象
*/
public class GoodsServiceV4 {
public void process() {
// 1000 行所有逻辑
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 无法复用 | 改一处牵动全身 |
| ② | 无法测试 | 单元测试跑不动 |
2.5 4 个反面教材的共同病根
| 痛点 | 反模式方案能不能解决? |
|---|---|
| 加新规则不动老代码 | ❌ 全部要改 |
| 数据结构稳定 | ❌ 全部臃肿 |
| 操作独立测试 | ❌ 全部耦合 |
| 业务清晰 | ❌ 全部混乱 |
必须上访问者模式。
三、模式原理:访问者的"元素 + 访问者"
3.1 访问者的 2 个核心概念
元素(Element):
- 数据结构
- 接受访问者
- 把自己交给访问者处理
访问者(Visitor):
- 操作实现
- 对每个元素实现 visit 方法
- 不修改数据结构
大白商城例子:
| 概念 | 例子 |
|---|---|
| 元素接口 | MarketingElement |
| 具体元素 | Goods / Order / Coupon |
| 访问者接口 | MarketingVisitor |
| 具体访问者 | FullReduceVisitor / DiscountVisitor / ... |
3.2 访问者的 5 个核心角色
| 角色 | 职责 | 例子 |
|---|---|---|
| 元素接口(Element) | 定义 accept | MarketingElement |
| 具体元素 A(Concrete Element A) | 实现 accept | Goods |
| 具体元素 B(Concrete Element B) | 实现 accept | Order |
| 访问者接口(Visitor) | 定义 visit | MarketingVisitor |
| 具体访问者(Concrete Visitor) | 实现 visit | FullReduceVisitor |
3.3 一张图看懂访问者
MarketingElement(元素接口)
│
│ accept(visitor)
│
├── Goods(具体元素)
│ └── accept(visitor) → visitor.visit(this)
│
├── Order(具体元素)
│ └── accept(visitor) → visitor.visit(this)
│
└── Coupon(具体元素)
└── accept(visitor) → visitor.visit(this)
MarketingVisitor(访问者接口)
│
│ visit(Goods) / visit(Order) / visit(Coupon)
│
├── FullReduceVisitor(满减)
├── DiscountVisitor(折扣)
└── InstantReduceVisitor(立减)
3.4 访问者的"灵魂三问"
Q1:访问者 vs 策略,区别是什么?
答:
- 访问者 :数据结构稳定(元素不变)
- 策略 :算法独立(任意替换)
- 访问者作用于多种元素 ,策略作用于单一对象
- 访问者操作随数据结构变化 ,策略操作固定
Q2:访问者 vs 中介者,区别是什么?
答:
- 访问者 :多种操作作用于多种元素
- 中介者 :多个对象通过中介者协调
- 访问者操作外置 ,中介者协调集中
- 访问者强调"操作",中介者强调"协调"
Q3:访问者模式的核心价值是什么?
答:
- 数据结构稳定(元素不变)
- 操作易扩展(加新操作只加访问者)
- 多种操作作用于多种元素(一次遍历完成多操作)
- 集中操作(操作都在访问者里)
3.5 访问者的 3 种实现方式
| 方式 | 特点 | 适用 |
|---|---|---|
| 经典访问者 | 接口 + 双重分派 | 标准 |
| 反射访问者 | 反射调用 | 灵活 |
| Lambda 访问者 | 函数式 | 简单 |
大白商城主推经典访问者方式。
四、实战代码:大白商城营销规则访问者完整实现
下面是大白商城生产环境在用的访问者实现,全套代码可直接复制到 IDEA 跑。
4.1 项目环境与依赖
pom.xml:
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.dabai.mall</groupId>
<artifactId>mall-design-pattern-42</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>mall-design-pattern-42</name>
<description>大白商城 - 设计模式 42 访问者模式</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.2 元素接口:MarketingElement
java
package com.dabai.mall.visitor;
import com.dabai.mall.visitor.visitor.MarketingVisitor;
/**
* ✅ 元素接口:营销元素
* <p>
* 接受访问者
*
* @author 大白商城技术团队
*/
public interface MarketingElement {
/**
* ✅ 接受访问者
*/
void accept(MarketingVisitor visitor);
}
4.3 访问者接口:MarketingVisitor
java
package com.dabai.mall.visitor.visitor;
import com.dabai.mall.visitor.element.Coupon;
import com.dabai.mall.visitor.element.Goods;
import com.dabai.mall.visitor.element.Order;
/**
* ✅ 访问者接口:营销访问者
*
* @author 大白商城技术团队
*/
public interface MarketingVisitor {
/**
* ✅ 访问商品
*/
void visit(Goods goods);
/**
* ✅ 访问订单
*/
void visit(Order order);
/**
* ✅ 访问优惠券
*/
void visit(Coupon coupon);
}
4.4 3 个具体元素
Goods:
java
package com.dabai.mall.visitor.element;
import com.dabai.mall.visitor.MarketingElement;
import com.dabai.mall.visitor.visitor.MarketingVisitor;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.math.BigDecimal;
/**
* ✅ 具体元素:商品
*/
@Data
@AllArgsConstructor
public class Goods implements MarketingElement {
private Long id;
private String name;
private BigDecimal price;
private Integer stock;
@Override
public void accept(MarketingVisitor visitor) {
// ✅ 把自己交给访问者
visitor.visit(this);
}
}
Order:
java
package com.dabai.mall.visitor.element;
import com.dabai.mall.visitor.MarketingElement;
import com.dabai.mall.visitor.visitor.MarketingVisitor;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* ✅ 具体元素:订单
*/
@Data
public class Order implements MarketingElement {
private String orderId;
private BigDecimal total;
private List<Goods> items;
@Override
public void accept(MarketingVisitor visitor) {
visitor.visit(this);
}
}
Coupon:
java
package com.dabai.mall.visitor.element;
import com.dabai.mall.visitor.MarketingElement;
import com.dabai.mall.visitor.visitor.MarketingVisitor;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.math.BigDecimal;
/**
* ✅ 具体元素:优惠券
*/
@Data
@AllArgsConstructor
public class Coupon implements MarketingElement {
private String id;
private String name;
private BigDecimal amount;
private BigDecimal threshold; // 满减门槛
@Override
public void accept(MarketingVisitor visitor) {
visitor.visit(this);
}
}
4.5 3 个具体访问者
FullReduceVisitor(满减):
java
package com.dabai.mall.visitor.visitor;
import com.dabai.mall.visitor.element.Coupon;
import com.dabai.mall.visitor.element.Goods;
import com.dabai.mall.visitor.element.Order;
import lombok.extern.slf4j.Slf4j;
/**
* ✅ 具体访问者:满减
*/
@Slf4j
public class FullReduceVisitor implements MarketingVisitor {
@Override
public void visit(Goods goods) {
// ✅ 满 100 减 10
if (goods.getPrice().compareTo(new java.math.BigDecimal("100")) > 0) {
log.info("【满减】商品 {} 满 100 减 10, 原价: {}",
goods.getName(), goods.getPrice());
}
}
@Override
public void visit(Order order) {
// ✅ 订单满 200 减 20
if (order.getTotal().compareTo(new java.math.BigDecimal("200")) > 0) {
log.info("【满减】订单 {} 满 200 减 20, 原总价: {}",
order.getOrderId(), order.getTotal());
}
}
@Override
public void visit(Coupon coupon) {
log.info("【满减】优惠券 {} 门槛: {}, 优惠: {}",
coupon.getName(), coupon.getThreshold(), coupon.getAmount());
}
}
DiscountVisitor(折扣):
java
package com.dabai.mall.visitor.visitor;
import com.dabai.mall.visitor.element.Coupon;
import com.dabai.mall.visitor.element.Goods;
import com.dabai.mall.visitor.element.Order;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
/**
* ✅ 具体访问者:折扣
*/
@Slf4j
public class DiscountVisitor implements MarketingVisitor {
private final BigDecimal discountRate; // 0.8 = 8 折
public DiscountVisitor(BigDecimal discountRate) {
this.discountRate = discountRate;
}
@Override
public void visit(Goods goods) {
BigDecimal newPrice = goods.getPrice().multiply(discountRate);
log.info("【折扣】商品 {} {} 折, 原价: {}, 现价: {}",
goods.getName(), discountRate, goods.getPrice(), newPrice);
}
@Override
public void visit(Order order) {
BigDecimal newTotal = order.getTotal().multiply(discountRate);
log.info("【折扣】订单 {} {} 折, 原总价: {}, 现总价: {}",
order.getOrderId(), discountRate, order.getTotal(), newTotal);
}
@Override
public void visit(Coupon coupon) {
log.info("【折扣】优惠券 {} 享受 {} 折", coupon.getName(), discountRate);
}
}
InstantReduceVisitor(立减):
java
package com.dabai.mall.visitor.visitor;
import com.dabai.mall.visitor.element.Coupon;
import com.dabai.mall.visitor.element.Goods;
import com.dabai.mall.visitor.element.Order;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
/**
* ✅ 具体访问者:立减
*/
@Slf4j
public class InstantReduceVisitor implements MarketingVisitor {
private final BigDecimal reduceAmount;
public InstantReduceVisitor(BigDecimal reduceAmount) {
this.reduceAmount = reduceAmount;
}
@Override
public void visit(Goods goods) {
BigDecimal newPrice = goods.getPrice().subtract(reduceAmount);
log.info("【立减】商品 {} 立减 {}, 原价: {}, 现价: {}",
goods.getName(), reduceAmount, goods.getPrice(), newPrice);
}
@Override
public void visit(Order order) {
BigDecimal newTotal = order.getTotal().subtract(reduceAmount);
log.info("【立减】订单 {} 立减 {}, 原总价: {}, 现总价: {}",
order.getOrderId(), reduceAmount, order.getTotal(), newTotal);
}
@Override
public void visit(Coupon coupon) {
log.info("【立减】优惠券 {} 立减 {}", coupon.getName(), reduceAmount);
}
}
4.6 对象结构:MarketingObjectStructure
java
package com.dabai.mall.visitor;
import com.dabai.mall.visitor.element.Coupon;
import com.dabai.mall.visitor.element.Goods;
import com.dabai.mall.visitor.element.Order;
import com.dabai.mall.visitor.visitor.MarketingVisitor;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
/**
* ✅ 对象结构:营销对象结构
* <p>
* 持有所有元素,统一接受访问者
*
* @author 大白商城技术团队
*/
@Slf4j
public class MarketingObjectStructure {
private final List<MarketingElement> elements = new ArrayList<>();
public void addElement(MarketingElement element) {
elements.add(element);
}
/**
* ✅ 接受访问者:每个元素都接受
*/
public void accept(MarketingVisitor visitor) {
log.info("【对象结构】开始接受访问者: {}",
visitor.getClass().getSimpleName());
for (MarketingElement element : elements) {
element.accept(visitor);
}
}
}
4.7 业务调用方
java
package com.dabai.mall.service;
import com.dabai.mall.visitor.*;
import com.dabai.mall.visitor.element.*;
import com.dabai.mall.visitor.visitor.*;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
/**
* ✅ 营销服务:访问者模式的调用方
* <p>
* 业务方只调 accept(visitor),不关心访问者细节
*
* @author 大白商城技术团队
*/
@Service
public class MarketingService {
/**
* ✅ 业务方只写一种代码
*/
public void processMarketing() {
// 1️⃣ 构建对象结构
MarketingObjectStructure structure = new MarketingObjectStructure();
Goods goods = new Goods(1L, "iPhone",
new BigDecimal("5999"), 100);
Order order = new Order();
order.setOrderId("ORDER_001");
order.setTotal(new BigDecimal("10000"));
Coupon coupon = new Coupon("CP_001", "满 100 减 10",
new BigDecimal("10"), new BigDecimal("100"));
structure.addElement(goods);
structure.addElement(order);
structure.addElement(coupon);
// 2️⃣ 应用满减规则
structure.accept(new FullReduceVisitor());
// 3️⃣ 应用 8 折规则
structure.accept(new DiscountVisitor(new BigDecimal("0.8")));
// 4️⃣ 应用立减 50 规则
structure.accept(new InstantReduceVisitor(new BigDecimal("50")));
}
}
4.8 单元测试
java
package com.dabai.mall.visitor;
import com.dabai.mall.visitor.element.*;
import com.dabai.mall.visitor.visitor.*;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
/**
* 访问者模式完整单元测试
*/
class MarketingVisitorTest {
@Test
void testFullReduceVisitor() {
MarketingObjectStructure structure = new MarketingObjectStructure();
Goods goods = new Goods(1L, "iPhone",
new BigDecimal("5999"), 100);
structure.addElement(goods);
structure.accept(new FullReduceVisitor());
}
@Test
void testDiscountVisitor() {
MarketingObjectStructure structure = new MarketingObjectStructure();
Goods goods = new Goods(1L, "iPhone",
new BigDecimal("100"), 100);
structure.addElement(goods);
structure.accept(new DiscountVisitor(new BigDecimal("0.8")));
}
@Test
void testInstantReduceVisitor() {
MarketingObjectStructure structure = new MarketingObjectStructure();
Goods goods = new Goods(1L, "iPhone",
new BigDecimal("100"), 100);
structure.addElement(goods);
structure.accept(new InstantReduceVisitor(new BigDecimal("20")));
}
@Test
void testMultipleVisitors() {
// ✅ 一个结构接受多个访问者
MarketingObjectStructure structure = new MarketingObjectStructure();
Goods goods = new Goods(1L, "iPhone",
new BigDecimal("5999"), 100);
Order order = new Order();
order.setOrderId("ORDER_001");
order.setTotal(new BigDecimal("10000"));
structure.addElement(goods);
structure.addElement(order);
// ✅ 应用 3 种规则
structure.accept(new FullReduceVisitor());
structure.accept(new DiscountVisitor(new BigDecimal("0.9")));
structure.accept(new InstantReduceVisitor(new BigDecimal("50")));
}
@Test
void testEmptyStructure() {
// ✅ 空结构接受访问者
MarketingObjectStructure structure = new MarketingObjectStructure();
structure.accept(new FullReduceVisitor());
}
@Test
void testVisitorExtensibility() {
// ✅ 加新规则只加 1 个访问者
MarketingObjectStructure structure = new MarketingObjectStructure();
Goods goods = new Goods(1L, "iPhone",
new BigDecimal("5999"), 100);
structure.addElement(goods);
// 加新规则只加新访问者,老代码零改动
structure.accept(new FullReduceVisitor());
}
}
五、访问者 vs 策略 / 中介者
5.1 一张表看清区别
| 模式 | 关注点 | 数据结构 | 操作 | 适用 |
|---|---|---|---|---|
| 访问者 | 操作分离 | 稳定 | 易扩展 | 数据结构稳定 |
| 策略 | 算法切换 | 任意 | 互相独立 | 多种算法 |
| 中介者 | 多对象协调 | 多对象 | 通信 | 多对象协调 |
5.2 一个具体例子区分 3 个模式
大白商城:
| 模式 | 例子 |
|---|---|
| 访问者 | 营销规则作用于商品/订单/优惠券 |
| 策略 | 支付 4 种方式 |
| 中介者 | 订单/支付/库存/物流协调 |
5.3 决策树
数据结构稳定,操作多变?
├── 是
│ └── ✅ 访问者模式
└── 否
├── 多种算法可切换?
│ ├── 是 → ✅ 策略
│ └── 否
│ ├── 多对象协调?
│ │ ├── 是 → ✅ 中介者
│ │ └── 否 → 普通类
5.4 访问者 vs 策略:易混淆
| 维度 | 访问者 | 策略 |
|---|---|---|
| 关注 | 数据结构 | 算法 |
| 操作数 | 多种操作 | 单一操作 |
| 数据结构 | 稳定 | 任意 |
| 扩展 | 加访问者 | 加策略 |
大白商城选型:
| 场景 | 选哪个 | 原因 |
|---|---|---|
| 营销规则 | 访问者 | 多种规则 × 多种对象 |
| 支付方式 | 策略 | 多种算法 |
| 订单协调 | 中介者 | 多服务协调 |
| 数据导出 | 访问者 | 多种格式 × 多种对象 |
六、JDK 编译器 API 怎么用访问者?
6.1 Java Compiler API:访问者经典应用
核心源码:
java
/**
* ✅ JDK 访问者:JavaFileObject
*/
public interface JavaFileObject extends FileObject {
// 标记接口
}
/**
* ✅ JDK 访问者:SimpleJavaFileObject
*/
public class SimpleJavaFileObject implements JavaFileObject {
// 接受编译器访问
}
6.2 DiagnosticListener:访问者接口
java
/**
* ✅ 访问者接口
*/
@FunctionalInterface
public interface DiagnosticListener<S> {
void report(Diagnostic<? extends S> diagnostic);
}
使用:
java
/**
* ✅ 业务方
*/
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
// ✅ 传入访问者
JavaCompiler.CompilationTask task = compiler.getTask(
null, null, diagnostics, null, null, compilationUnits);
task.call();
// ✅ 访问者被回调
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
log.info("Error on line: {} - {}",
diagnostic.getLineNumber(), diagnostic.getMessage(null));
}
6.3 javax.lang.model.element.ElementVisitor:访问者
核心源码:
java
/**
* ✅ JDK 访问者:ElementVisitor
*/
public interface ElementVisitor<R, P> {
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);
}
6.4 大白商城的"抄作业"
java
/**
* 借鉴 JDK:自定义访问者
*/
public class LoggingVisitor implements MarketingVisitor {
@Override
public void visit(Goods goods) {
log.info("【访问者】商品: id={}, name={}", goods.getId(), goods.getName());
}
@Override
public void visit(Order order) {
log.info("【访问者】订单: id={}, total={}",
order.getOrderId(), order.getTotal());
}
@Override
public void visit(Coupon coupon) {
log.info("【访问者】优惠券: id={}, amount={}",
coupon.getId(), coupon.getAmount());
}
}
七、ASM / Spring SpEL 怎么借鉴访问者?
7.1 ASM:字节码访问者
核心源码:
java
/**
* ✅ ASM 访问者:ClassVisitor
*/
public abstract class ClassVisitor {
public void visit(int version, int access, String name, ...) { ... }
public FieldVisitor visitField(...) { ... }
public MethodVisitor visitMethod(...) { ... }
public void visitEnd() { ... }
}
/**
* ✅ 业务方:继承访问者
*/
public class MyClassVisitor extends ClassVisitor {
@Override
public MethodVisitor visitMethod(int access, String name, ...) {
// ✅ 自定义处理
return super.visitMethod(access, name, ...);
}
}
使用:
java
ClassReader reader = new ClassReader(bytes);
ClassWriter writer = new ClassWriter(reader, 0);
ClassVisitor visitor = new MyClassVisitor(writer);
reader.accept(visitor, 0);
7.2 Spring SpEL:表达式访问者
核心源码(简化):
java
/**
* ✅ SpEL 访问者:内部用访问者遍历 AST
*/
public class SpelNode {
public abstract <T> T accept(SpelNodeVisitor<T> visitor);
}
public interface SpelNodeVisitor<T> {
T visit(SpelNode node);
}
7.3 大白商城的"抄作业"
java
/**
* 借鉴 ASM:自定义业务访问者
*/
public class StatisticsVisitor implements MarketingVisitor {
private Map<String, Integer> stats = new HashMap<>();
@Override
public void visit(Goods goods) {
stats.merge("goods", 1, Integer::sum);
}
@Override
public void visit(Order order) {
stats.merge("order", 1, Integer::sum);
}
@Override
public void visit(Coupon coupon) {
stats.merge("coupon", 1, Integer::sum);
}
public void printStats() {
log.info("【统计】{}", stats);
}
}
八、工程决策 Checklist
8.1 ✅ 这 5 种情况,强烈建议用访问者
| 序号 | 场景 | 原因 |
|---|---|---|
| ① | 数据结构稳定 | 商品/订单/优惠券 |
| ② | 操作多变 | 营销规则 |
| ③ | 多种操作作用于多种对象 | 报表/导出 |
| ④ | 需要遍历复杂对象 | AST / XML |
| ⑤ | 操作需独立 | 满减/折扣/立减 |
8.2 ❌ 这 5 种情况,绝对不要用访问者
| 序号 | 场景 | 原因 |
|---|---|---|
| ① | 数据结构常变 | 加元素改所有访问者 |
| ② | 业务简单 | 过度设计 |
| ③ | 操作少 | 不值得 |
| ④ | 追求简单 | 类多 |
| ⑤ | 对象结构简单 | 不需要 |
8.3 ⚠️ 访问者的 6 大常见坑
| 序号 | 坑 | 表现 | 解决方案 |
|---|---|---|---|
| ① | 数据结构变动 | 改 1 个类动 N 个访问者 | 谨慎 |
| ② | 破坏封装 | 访问者要访问内部 | 提供接口 |
| ③ | 双重分派难理解 | 学习曲线 | 文档 |
| ④ | 性能 | visit 链慢 | 缓存 |
| ⑤ | 循环引用 | 栈溢出 | 检测 |
| ⑥ | 类型硬编码 | 编译期耦合 | 反射 |
8.4 面试官视角:访问者高频追问
Q1:访问者 vs 策略,区别是什么?
答:访问者数据结构稳定 (元素不变),策略算法独立 。访问者作用于多种元素,策略作用于单一对象。
Q2:访问者模式的核心价值是什么?
答:数据结构稳定 + 操作易扩展 。加新操作只加访问者,元素不变 。适用于操作多变、数据结构稳定的场景。
Q3:访问者模式最大的缺点是什么?
答:难增加新元素 。加新元素要改所有访问者。所以访问者适用于"数据结构稳定"的场景。
九、与其他模式协作
9.1 访问者 + 组合 = 树形结构访问
java
/**
* ✅ 访问者 + 组合:树形结构
*/
public class TreeNode implements MarketingElement {
private List<MarketingElement> children = new ArrayList<>();
public void accept(MarketingVisitor visitor) {
visitor.visit(this);
children.forEach(c -> c.accept(visitor));
}
}
9.2 访问者 + 迭代器 = 遍历 + 操作
9.3 访问者 + 策略 = 多种访问者策略
9.4 大白商城模式协作全景图
┌──────────────┐
│ 访问者 │ ← 本篇
└──────┬───────┘
│
┌───────────┬───────┼───────┬───────────┐
│ │ │ │ │
┌───▼───┐ ┌────▼───┐ ┌▼────┐ ┌▼─────┐ ┌───▼────┐
│组合 │ │迭代器 │ │策略 │ │单例 │ │ 工厂 │
│(树) │ │(遍历) │ │(操作)│ │(访问)│ │ 方法 │
└───────┘ └────────┘ └──────┘ └──────┘ └───────┘
20 篇 38 篇 26 篇 02 篇 04 篇
十、本篇小结 + 下篇预告
10.1 本篇小结(5 个核心要点)
- 本质 :访问者 = 数据结构与操作分离 ,加新操作只加访问者。
- 场景 :营销规则 / 报表生成 / 编译器 / 文件处理,操作多变。
- 对比:访问者 vs 策略(多种 vs 单一)/ 访问者 vs 中介者(操作外置 vs 协调集中)。
- JDK Compiler API :
ElementVisitor+ DiagnosticListener 经典应用。 - 避坑:数据结构变动 / 破坏封装 / 双重分派 / 性能问题。
10.2 一句话总结
访问者不是"换种方式写业务",是"用数据结构稳定、操作易扩展的思路" 。大白商城从 24 处重复 → 3 个元素 + 3 个访问者------加新营销规则只加 1 个访问者,商品/订单/优惠券类零改动。
10.3 知识脑图
访问者模式
├── 2 个核心概念
│ ├── 元素(Element)
│ └── 访问者(Visitor)
├── 5 大角色
│ ├── 元素接口(MarketingElement)
│ ├── 具体元素(3 个)
│ ├── 访问者接口(MarketingVisitor)
│ ├── 具体访问者(3 个)
│ └── 对象结构(MarketingObjectStructure)
├── 3 个具体元素
│ ├── Goods
│ ├── Order
│ └── Coupon
├── 3 个具体访问者
│ ├── FullReduceVisitor(满减)
│ ├── DiscountVisitor(折扣)
│ └── InstantReduceVisitor(立减)
├── 模式对比
│ ├── vs 策略(多种 vs 单一)
│ ├── vs 中介者(操作 vs 协调)
│ └── vs 迭代器(操作 vs 遍历)
├── JDK Compiler API
│ ├── JavaFileObject
│ ├── DiagnosticListener
│ └── ElementVisitor
└── 模式协作
├── + 组合(树形访问)
├── + 迭代器(遍历+操作)
└── + 策略(多种访问者)
10.4 下篇预告
第 43 篇【访问者模式 - 源码剖析篇】:JDK / Spring 中的访问者实现
下一篇我们会深入源码,回答三个问题:
- JDK 的
ElementVisitor怎么用访问者遍历 AST? - ASM 的
ClassVisitor怎么访问字节码? - Spring
SpEL怎么用访问者求值?
并附完整的源码解读 + 流程图 + 大白商城的"抄作业"实践。
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯