概述
访问者模式(Visitor Pattern)是GoF设计模式中较为复杂但功能强大的行为型模式之一。其核心定义是:表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。这一特性直接回应了面向对象设计中一个经典困境——当数据结构相对稳定,而作用于其上的操作却频繁变化时,若直接在元素类上新增方法,将严重违反开闭原则。
访问者模式的核心价值在于实现数据结构与数据操作的分离。在实际工程中,我们经常遇到这样的场景:底层数据模型(如AST语法树、文件系统节点、JSON结构)已经定型,但上层业务却需要不断追加新的处理逻辑(如语义分析、格式转换、权限校验)。访问者模式通过引入“访问者”这一角色,将操作从元素类中抽离出来,使得新增操作无需修改元素类,完美突破了开闭原则对“新增操作”方向的约束。
本文将带领读者从最原始的类型判断代码出发,逐步演进到经典的访问者模式实现,并深入剖析双重分派(Double Dispatch)机制如何在Java单分派语言的限制下实现操作与元素的动态绑定。随后,我们将进入JDK、Spring、MyBatis、ASM等主流框架的源码深处,探寻访问者模式在真实工业级场景中的精妙应用。特别地,本文专设“分布式环境下的访问者模式”章节,探讨在ShardingSphere SQL解析、配置中心遍历、API网关路由处理等分布式场景中,访问者模式如何成为解析与遍历复杂结构的首选方案。此外,本文还将通过五个典型场景的完整可运行Demo,手把手展示访问者模式在购物车计算、编译器AST遍历、文件系统操作、JSON处理及报表导出中的实践,并附赠10道专家级面试题与深度解答。
一、模式定义与结构
1.1 GoF标准定义
访问者模式(Visitor):表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
1.2 UML类图
classDiagram
class Client {
}
class ObjectStructure {
-elements: List~Element~
+attach(Element)
+detach(Element)
+accept(Visitor)
}
class Visitor {
<<interface>>
+visitConcreteElementA(ConcreteElementA)
+visitConcreteElementB(ConcreteElementB)
}
class ConcreteVisitor1 {
+visitConcreteElementA(ConcreteElementA)
+visitConcreteElementB(ConcreteElementB)
}
class ConcreteVisitor2 {
+visitConcreteElementA(ConcreteElementA)
+visitConcreteElementB(ConcreteElementB)
}
class Element {
<<interface>>
+accept(Visitor)
}
class ConcreteElementA {
+accept(Visitor)
+operationA()
}
class ConcreteElementB {
+accept(Visitor)
+operationB()
}
Client --> ObjectStructure
Client --> Visitor
Client --> Element
ObjectStructure o-- Element
Visitor <|.. ConcreteVisitor1
Visitor <|.. ConcreteVisitor2
Element <|.. ConcreteElementA
Element <|.. ConcreteElementB
ConcreteElementA ..> Visitor : "visitor.visitConcreteElementA(this)"
ConcreteElementB ..> Visitor : "visitor.visitConcreteElementB(this)"
1.3 角色职责详解与双重分派机制
上图展示了访问者模式的五大核心角色:
- Visitor(抽象访问者):为对象结构中的每一个具体元素类声明一个
visit操作。方法名称与参数类型标识了被访问元素的具体类型,使访问者能够针对不同元素执行不同操作。 - ConcreteVisitor(具体访问者):实现
Visitor声明的每一个visit操作,定义对特定元素类的具体行为。访问者可以在遍历过程中累积内部状态(如总价格、类型错误列表)。 - Element(抽象元素):定义一个
accept方法,接收一个访问者对象作为参数。该方法是双重分派的第一重分派入口。 - ConcreteElement(具体元素):实现
accept方法,其典型实现为visitor.visitConcreteElement(this)。注意此处this的静态类型为具体元素类,编译器可精确匹配到访问者中对应的重载visit方法。 - ObjectStructure(对象结构):维护元素集合,并提供高层接口让访问者能够遍历所有元素。该角色可以是一个组合结构(如目录树),也可以是简单列表。
双重分派(Double Dispatch)机制解读:
Java是一门单分派(Single Dispatch)语言。所谓分派,是指根据对象的类型决定调用哪个方法。Java在方法调用时,首先根据接收者(receiver)的实际类型进行动态绑定(即虚方法调用),但方法的参数类型是在编译期静态确定的。这意味着仅一次方法调用无法同时根据两个对象的实际类型来动态决定执行逻辑。
访问者模式巧妙地通过两次方法调用模拟了双重分派:
- 第一重分派:客户端调用
element.accept(visitor)。此处的分派依据是element的实际类型,JVM动态绑定到具体元素类的accept方法。 - 第二重分派:在具体元素的
accept方法内部,执行visitor.visit(this)。此时this的静态类型为当前具体元素类,编译器会将其精准绑定到Visitor接口中对应的重载方法(例如visit(ConcreteElementA))。这一调用仍然是虚方法调用,JVM会根据visitor的实际类型,调用具体访问者中实现的对应visit方法。
经过这两次分派,最终执行的操作既取决于元素的具体类型,也取决于访问者的具体类型,从而实现了操作与元素的动态解耦。
二、代码演进与实现
2.1 不使用模式的原始代码:instanceof地狱
假设我们有一个购物系统,包含书籍、水果和电子产品三种商品。需要计算总价和计算税费。
// 商品基类(不含计算操作)
abstract class Item {
protected String name;
protected double price;
public Item(String name, double price) { this.name = name; this.price = price; }
public String getName() { return name; }
public double getPrice() { return price; }
}
class Book extends Item {
private String isbn;
public Book(String name, double price, String isbn) { super(name, price); this.isbn = isbn; }
public String getIsbn() { return isbn; }
}
class Fruit extends Item {
private double weight;
public Fruit(String name, double price, double weight) { super(name, price); this.weight = weight; }
public double getWeight() { return weight; }
}
class Electronics extends Item {
private String brand;
public Electronics(String name, double price, String brand) { super(name, price); this.brand = brand; }
public String getBrand() { return brand; }
}
// 客户端:通过instanceof判断类型执行不同操作
public class WithoutVisitorDemo {
public static void main(String[] args) {
List<Item> items = Arrays.asList(
new Book("Design Patterns", 59.0, "978-0201633610"),
new Fruit("Apple", 3.5, 1.2),
new Electronics("Phone", 5999.0, "BrandX")
);
// 计算总价(无类型区分,直接累加价格即可)
double totalPrice = 0;
for (Item item : items) {
totalPrice += item.getPrice();
}
System.out.println("Total Price: " + totalPrice);
// 计算税费:书籍免税,水果税率5%,电子产品税率15%
double totalTax = 0;
for (Item item : items) {
if (item instanceof Book) {
// 书籍免税
totalTax += 0;
} else if (item instanceof Fruit) {
totalTax += item.getPrice() * 0.05;
} else if (item instanceof Electronics) {
totalTax += item.getPrice() * 0.15;
} else {
throw new IllegalArgumentException("Unknown item type");
}
}
System.out.println("Total Tax: " + totalTax);
}
}
问题分析:
- 类型判断臃肿:每次新增操作(如折扣计算、积分计算)都需要在客户端写一遍
instanceof链,极易遗漏或出错。 - 违反开闭原则:当新增商品类型(如
Clothing)时,所有包含instanceof判断的代码都必须修改,难以维护。 - 操作逻辑散落:与某一类型相关的操作分散在不同方法中,内聚性差。
2.2 经典访问者模式重构
接下来我们引入访问者模式,将操作封装到独立的访问者类中。
2.2.1 定义Visitor接口与Element接口
// 抽象访问者:为每一种具体元素声明visit方法
interface ShoppingCartVisitor {
void visit(Book book); // 重载方法1:针对Book
void visit(Fruit fruit); // 重载方法2:针对Fruit
void visit(Electronics elec); // 重载方法3:针对Electronics
}
// 抽象元素:声明accept方法接收访问者
interface ItemElement {
void accept(ShoppingCartVisitor visitor);
}
2.2.2 具体元素类实现accept
每个具体元素类实现accept方法,其内部回调访问者的对应visit方法,并将自身(this)作为参数传入。
class Book implements ItemElement {
private String name;
private double price;
private String isbn;
public Book(String name, double price, String isbn) {
this.name = name; this.price = price; this.isbn = isbn;
}
// getters省略 ...
@Override
public void accept(ShoppingCartVisitor visitor) {
visitor.visit(this); // 第二重分派:静态类型为Book,调用visit(Book)
}
}
class Fruit implements ItemElement {
private String name;
private double price;
private double weight;
public Fruit(String name, double price, double weight) {
this.name = name; this.price = price; this.weight = weight;
}
// getters省略 ...
@Override
public void accept(ShoppingCartVisitor visitor) {
visitor.visit(this); // 静态类型为Fruit,调用visit(Fruit)
}
}
class Electronics implements ItemElement {
private String name;
private double price;
private String brand;
public Electronics(String name, double price, String brand) {
this.name = name; this.price = price; this.brand = brand;
}
// getters省略 ...
@Override
public void accept(ShoppingCartVisitor visitor) {
visitor.visit(this); // 静态类型为Electronics,调用visit(Electronics)
}
}
2.2.3 实现具体访问者类
价格计算访问者:累加所有商品价格。
class PriceCalculatorVisitor implements ShoppingCartVisitor {
private double totalPrice = 0; // 内部状态累积
@Override
public void visit(Book book) {
totalPrice += book.getPrice();
}
@Override
public void visit(Fruit fruit) {
// 水果按重量计价(本例price为单价)
totalPrice += fruit.getPrice() * fruit.getWeight();
}
@Override
public void visit(Electronics electronics) {
totalPrice += electronics.getPrice();
}
public double getTotalPrice() { return totalPrice; }
}
税费计算访问者:根据商品类型执行不同税率计算。
class TaxCalculatorVisitor implements ShoppingCartVisitor {
private double totalTax = 0;
@Override
public void visit(Book book) {
// 书籍免税
}
@Override
public void visit(Fruit fruit) {
totalTax += fruit.getPrice() * fruit.getWeight() * 0.05;
}
@Override
public void visit(Electronics electronics) {
totalTax += electronics.getPrice() * 0.15;
}
public double getTotalTax() { return totalTax; }
}
2.2.4 客户端与对象结构
public class VisitorPatternDemo {
public static void main(String[] args) {
List<ItemElement> items = Arrays.asList(
new Book("Design Patterns", 59.0, "978-0201633610"),
new Fruit("Apple", 3.5, 1.2),
new Electronics("Phone", 5999.0, "BrandX")
);
// 价格计算
PriceCalculatorVisitor priceVisitor = new PriceCalculatorVisitor();
for (ItemElement item : items) {
item.accept(priceVisitor); // 第一重分派:根据item实际类型调用对应accept
}
System.out.println("Total Price: " + priceVisitor.getTotalPrice());
// 税费计算
TaxCalculatorVisitor taxVisitor = new TaxCalculatorVisitor();
for (ItemElement item : items) {
item.accept(taxVisitor);
}
System.out.println("Total Tax: " + taxVisitor.getTotalTax());
}
}
输出结果:
Total Price: 6063.2
Total Tax: 900.06
2.3 访问者模式的进阶特性
a. 访问者的状态管理
如上例所示,PriceCalculatorVisitor内部维护了totalPrice状态,并在各visit方法中更新。这种状态累积能力使得访问者模式非常适合归约操作(reduce),例如计算总和、收集错误信息等。
b. 遍历策略分离
将遍历逻辑从客户端移至ObjectStructure:
class ShoppingCart {
private List<ItemElement> items = new ArrayList<>();
public void addItem(ItemElement item) { items.add(item); }
// 驱动访问者遍历所有元素
public void accept(ShoppingCartVisitor visitor) {
for (ItemElement item : items) {
item.accept(visitor);
}
}
}
// 客户端简化
ShoppingCart cart = new ShoppingCart();
cart.addItem(new Book(...));
cart.addItem(new Fruit(...));
cart.accept(new PriceCalculatorVisitor());
c. 反射简化accept(高级技巧)
当元素种类较多时,每个元素类的accept方法都是样板代码visitor.visit(this)。可以通过反射在抽象基类中统一实现:
abstract class ReflectiveItem implements ItemElement {
@Override
public void accept(ShoppingCartVisitor visitor) {
try {
Method method = visitor.getClass().getMethod("visit", this.getClass());
method.invoke(visitor, this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
注意:反射会带来性能损耗,且丢失编译期类型检查,仅在元素类型极多且相对稳定时考虑使用。
2.4 双重分派时序图
sequenceDiagram
participant Client
participant ObjectStructure
participant ConcreteElement as book:Book
participant Visitor as taxVisitor:TaxCalculatorVisitor
Client->>ObjectStructure: accept(visitor)
loop 遍历所有元素
ObjectStructure->>ConcreteElement: accept(visitor)
Note over ConcreteElement: 第一重分派:动态绑定到Book.accept
ConcreteElement->>Visitor: visit(this)
Note over Visitor: 第二重分派:静态类型Book→调用visit(Book)<br>动态绑定到TaxCalculatorVisitor.visit(Book)
Visitor-->>ConcreteElement: 执行针对Book的税费计算
end
ObjectStructure-->>Client: 遍历完成
时序图解读:
上图精确描绘了双重分派在访问者模式中的执行时序:
- 发起遍历:客户端创建具体访问者对象
taxVisitor,并将其传递给对象结构(或直接迭代元素列表)。对象结构持有所有元素引用。 - 第一重分派(元素选择):对象结构依次对每个元素调用
accept(visitor)方法。JVM根据当前元素的实际类型(例如Book)动态分派到Book.accept(visitor)。这一步骤决定了“哪一个元素”将接受访问。 - 第二重分派(操作选择):在
Book.accept方法内部,执行visitor.visit(this)。注意,此处的this具有明确的编译时类型Book,因此编译器能够精确匹配到ShoppingCartVisitor接口中的visit(Book)方法。随后,JVM再次根据visitor的实际类型(TaxCalculatorVisitor)进行虚方法调用,最终执行TaxCalculatorVisitor.visit(Book)内的具体逻辑。 - 操作执行:访问者的
visit方法获取到具体的元素对象,可读取元素属性(如价格、重量),执行特定计算并更新内部状态。
整个过程通过两次方法调用巧妙地绕开了Java单分派的限制,实现了“由元素类型和访问者类型共同决定最终执行逻辑”的效果。这也是访问者模式成为处理稳定数据结构上多变操作的首选方案的根本原因。
三、源码级应用分析
3.1 JDK中的访问者模式
3.1.1 java.nio.file.FileVisitor与Files.walkFileTree
FileVisitor接口是访问者模式在文件系统遍历中的经典应用。Files.walkFileTree方法接受一个起始路径和一个FileVisitor实现,内部递归遍历目录树,并针对每个文件和目录回调访问者方法。
public interface FileVisitor<T> {
FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs);
FileVisitResult visitFile(T file, BasicFileAttributes attrs);
FileVisitResult visitFileFailed(T file, IOException exc);
FileVisitResult postVisitDirectory(T dir, IOException exc);
}
- Element:
Path对象代表文件系统节点(文件/目录)。 - Visitor:
FileVisitor定义了对目录进入、文件访问、访问失败、目录退出四种情况的处理。 - 遍历驱动:
Files.walkFileTree内部使用FileTreeWalker递归遍历,起到ObjectStructure的作用。
通过实现FileVisitor,我们可以轻松实现文件搜索、批量删除、权限修改等操作,而无需修改Path或Files类。
3.1.2 javax.lang.model.element.ElementVisitor
Java编译时注解处理API(javax.annotation.processing)中大量使用访问者模式遍历程序元素。Element代表包、类、方法、变量等编译单元,ElementVisitor则定义了对各类型元素的visit方法。
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表示返回值类型,P表示附加参数类型,提高了灵活性。 - 抽象类
AbstractElementVisitor8提供了默认实现,简化了具体访问者的编写。 - 注解处理器(如Lombok)通过实现
ElementVisitor来分析类结构并生成代码。
3.1.3 java.beans.BeanInfo中的属性访问者
java.beans包下的BeanInfo类用于描述JavaBean的元信息。虽然未直接命名为Visitor,但其内部类SimpleBeanInfo与属性遍历机制隐含访问者思想。PropertyDescriptor可通过getReadMethod和getWriteMethod进行统一访问,实现类似visitProperty的效果。
3.2 Spring框架深度剖析
3.2.1 BeanDefinitionVisitor
Spring IoC容器中,BeanDefinitionVisitor用于遍历并修改BeanDefinition对象。它提供了visitBeanDefinition、visitPropertyValues等方法,允许对Bean定义的属性值进行占位符解析或类型转换。
public class BeanDefinitionVisitor {
public void visitBeanDefinition(BeanDefinition beanDefinition) {
visitBeanClassName(beanDefinition);
visitPropertyValues(beanDefinition.getPropertyValues());
// ...
}
}
这种设计使得Spring可以在不修改BeanDefinition类的前提下,灵活增加对定义的后置处理逻辑(如@Value解析)。
3.2.2 PropertyPlaceholderHelper中的PlaceholderResolver
PropertyPlaceholderHelper负责解析字符串中的占位符${...}。虽然它不是标准的访问者模式,但其内部类PlaceholderResolver作为一个函数式接口,可以被看作是一种简化的访问者:针对字符串中不同位置的占位符“元素”执行解析操作。
3.2.3 AspectJ的AbstractPointcutAdvisor
Spring AOP与AspectJ集成中,AbstractPointcutAdvisor及其子类在处理切点表达式匹配时,对不同的Joinpoint类型(方法执行、构造器调用等)采用访问者模式进行匹配判断。
3.3 MyBatis框架:SqlNode体系
MyBatis的动态SQL解析是访问者模式的绝佳案例。SqlNode接口代表动态SQL片段(如if、trim、foreach),DynamicContext作为上下文对象。每个SqlNode都实现了apply(DynamicContext)方法,内部会调用context.appendSql(),同时根据逻辑条件决定是否应用子节点。
public interface SqlNode {
boolean apply(DynamicContext context);
}
// IfSqlNode 内部持有条件判断和子节点
public class IfSqlNode implements SqlNode {
private final ExpressionEvaluator evaluator;
private final String test;
private final SqlNode contents;
@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context); // 递归遍历子节点
return true;
}
return false;
}
}
虽然这里的apply方法并非典型的accept+visit结构,但其思想完全一致:稳定的SqlNode树结构 + 多变的上下文操作(SQL生成、参数绑定)。不同的SqlNode类型(元素)通过统一的apply接口接受DynamicContext(访问者)的访问。
3.4 ASM字节码框架:ClassVisitor
ASM是Java字节码操作的事实标准库,其核心设计完全遵循访问者模式。ClassVisitor作为抽象访问者,定义了visit、visitMethod、visitField等方法,分别对应类、方法、字段等字节码元素。ClassReader负责读取字节码并驱动访问者遍历。
public abstract class ClassVisitor {
public void visit(int version, int access, String name, ...) { }
public FieldVisitor visitField(int access, String name, ...) { }
public MethodVisitor visitMethod(int access, String name, ...) { }
// ...
}
ASM为何选择访问者模式?
- 元素类型固定:字节码结构(类、方法、字段、注解)由JVM规范严格定义,极少变化。
- 操作多变:字节码增强、性能分析、代码覆盖率、反编译等场景需求层出不穷。
- 性能考量:访问者模式避免了为每个操作创建新的中间数据结构,直接在遍历过程中完成修改,内存占用极小。
3.5 其他框架
- ANTLR ParseTreeVisitor:ANTLR 4生成的语法分析器支持访问者模式遍历语法树。用户实现
ParseTreeVisitor<T>接口,对不同类型的语法节点执行语义动作。 - Jackson JsonNode:
JsonNode的traverse()方法返回JsonParser,配合JsonToken可模拟访问者遍历。Jackson 2.9后引入JsonNodeVisitor(内部类),用于高效树遍历。
四、分布式环境下的访问者模式
4.1 分布式AST分析与SQL审计(ShardingSphere)
在Apache ShardingSphere中,SQL解析器生成AST后,通过访问者模式实现分片键提取、SQL改写、权限校验等功能。SQLASTVisitor接口定义了针对各种SQL语法节点的visit方法。
public interface SQLASTVisitor {
void visit(SelectStatement select);
void visit(InsertStatement insert);
void visit(WhereSegment where);
void visit(TableSegment table);
// ...
}
- 分片键提取访问者:遍历AST,识别分片表并提取分片值,生成路由信息。
- SQL改写访问者:根据路由结果,修改逻辑表名为物理表名,添加派生列等。
- 权限校验访问者:检查用户对表的操作权限。
这种设计使得ShardingSphere能够轻松支持MySQL、PostgreSQL、Oracle等多种方言的SQL,只需为每种方言实现特定的visit方法即可。
4.2 分布式配置中心的配置项遍历
Apollo配置中心将配置项组织为树形结构(Namespace → Properties)。通过访问者模式可以实现:
- 配置加密访问者:遍历所有敏感配置项(如密码)并加密存储。
- 格式校验访问者:验证配置值的格式是否符合规范(如URL、正则)。
- 迁移访问者:将旧版本配置结构迁移至新版本。
4.3 微服务API网关的路由规则遍历
Spring Cloud Gateway中,RouteDefinition包含谓词(Predicate)和过滤器(Filter)列表。RouteDefinitionVisitor可遍历所有路由规则,实现:
- 路由规则校验:检查谓词组合是否合理,避免冲突。
- 监控埋点访问者:为每个路由自动注入监控过滤器。
- 日志记录访问者:为每个路由添加请求/响应日志过滤器。
4.4 分布式图数据库的图遍历
Neo4j的遍历框架(Traversal Framework)本质上是访问者模式在图结构上的应用。PathEvaluator和PathExpander定义了对路径中各节点的访问行为,实现最短路径、子图匹配等算法。
4.5 跨服务的领域事件处理
在事件驱动架构(EDA)中,不同类型的事件(OrderCreatedEvent、UserRegisteredEvent、InventoryUpdatedEvent)可视为元素,事件处理器(Handler)可视为访问者。事件总线充当ObjectStructure,将事件分发给对应的处理器。
interface DomainEventVisitor {
void visit(OrderCreatedEvent event);
void visit(UserRegisteredEvent event);
// ...
}
4.6 分布式SQL解析访问者代码示例
// AST节点接口
interface ASTNode {
void accept(SQLVisitor visitor);
}
class TableNode implements ASTNode {
private String tableName;
public TableNode(String name) { this.tableName = name; }
public String getTableName() { return tableName; }
@Override
public void accept(SQLVisitor visitor) { visitor.visit(this); }
}
class JoinNode implements ASTNode {
private ASTNode left, right;
private String joinType;
// 构造器、getter省略...
@Override
public void accept(SQLVisitor visitor) { visitor.visit(this); }
}
class WhereNode implements ASTNode {
private String condition;
// ...
@Override
public void accept(SQLVisitor visitor) { visitor.visit(this); }
}
// 访问者接口
interface SQLVisitor {
void visit(TableNode table);
void visit(JoinNode join);
void visit(WhereNode where);
}
// 具体访问者:SQL生成
class SQLGenerator implements SQLVisitor {
private StringBuilder sql = new StringBuilder();
@Override public void visit(TableNode table) { sql.append(table.getTableName()); }
@Override public void visit(JoinNode join) {
join.getLeft().accept(this);
sql.append(" ").append(join.getJoinType()).append(" JOIN ");
join.getRight().accept(this);
}
@Override public void visit(WhereNode where) { sql.append(" WHERE ").append(where.getCondition()); }
public String getSQL() { return sql.toString(); }
}
// 具体访问者:权限校验
class PermissionChecker implements SQLVisitor {
private Set<String> allowedTables;
@Override public void visit(TableNode table) {
if (!allowedTables.contains(table.getTableName())) throw new SecurityException("Access denied");
}
// 其他visit方法递归检查...
}
4.7 分布式SQL解析架构流程图
flowchart TD
A[SQL字符串] --> B[SQL解析器 Parser]
B --> C[抽象语法树 AST]
C --> D{遍历器驱动}
D --> E[TableNode]
D --> F[JoinNode]
D --> G[WhereNode]
E --> H[accept SQLVisitor]
F --> H
G --> H
H --> I{访问者类型}
I --> J[SQLGenerator 生成改写SQL]
I --> K[PermissionChecker 权限校验]
I --> L[ShardingExtractor 分片提取]
J --> M[改写后SQL]
K --> N[校验结果]
L --> O[分片路由信息]
架构图解读:
上图完整呈现了分布式数据库中间件(如ShardingSphere)利用访问者模式处理SQL解析的典型流程:
- 输入与解析:用户提交的原始SQL字符串首先经过SQL解析器(Parser),解析器根据词法和语法规则将SQL文本转换为内存中的抽象语法树(AST)。AST由多种节点对象构成,如
TableNode代表表引用、JoinNode代表连接操作、WhereNode代表条件表达式。 - 遍历驱动:解析完成后,系统获取AST根节点,并启动遍历驱动(通常是一个递归遍历器)。遍历器按照AST的树形结构依次访问每个节点,对每个节点调用
accept(visitor)方法。 - 双重分派执行:以
TableNode为例,其accept方法内部执行visitor.visit(this)。根据传入的具体访问者类型(如SQLGenerator或PermissionChecker),JVM动态分派到对应的visit实现。 - 并行操作产出:不同的访问者遍历同一棵AST,可产生完全不同的输出结果。
SQLGenerator递归拼接各节点片段,输出改写后的SQL字符串;PermissionChecker校验用户对每个表的操作权限,无权限则抛出异常;ShardingExtractor提取分片键值,为后续路由提供依据。
这种设计将不变的SQL语法结构与多变的上层业务逻辑彻底解耦,使得中间件能够轻松扩展新的SQL方言、新的改写规则或新的审计策略,而无需修改核心AST节点类。这正是访问者模式在分布式基础设施中发挥关键作用的缩影。
五、对比辨析
5.1 访问者模式 vs 策略模式
| 维度 | 访问者模式 | 策略模式 |
|---|---|---|
| 作用对象 | 一组不同类的元素集合 | 单一上下文对象 |
| 核心机制 | 双重分派,操作由元素类型和访问者类型共同决定 | 直接替换算法,客户端持有策略接口引用 |
| 扩展方向 | 易于增加新操作(新增访问者),难以增加新元素 | 易于增加新策略,上下文稳定 |
| 典型场景 | AST遍历、文件系统操作、多种报表导出 | 支付方式选择、压缩算法切换、排序策略 |
选择依据:当需要对多个不同类的对象执行一组相关操作时,使用访问者;当仅需为单一上下文替换不同算法实现时,使用策略。
5.2 访问者模式 vs 迭代器模式
- 迭代器模式关注如何遍历(顺序、深度优先/广度优先),隐藏底层集合结构。
- 访问者模式关注遍历时做什么,将操作逻辑外置。
- 二者常结合使用:迭代器负责遍历元素集合,每取出一个元素便调用其
accept方法,传入访问者。
5.3 访问者模式 vs 命令模式
- 命令模式将请求封装为对象,支持撤销、排队、日志等。
- 访问者模式封装的是对一类元素结构的操作集,强调的是元素与操作的解耦。
- 命令模式的操作对象通常是单一的Receiver;访问者模式的操作对象是元素结构中的多种类型。
5.4 访问者模式 vs 解释器模式
- 解释器模式定义了一种语言文法及其解释器,通过递归解析AST执行计算。
- 访问者模式常作为解释器模式中遍历AST并执行语义动作的实现手段。例如,表达式求值解释器可以通过
EvaluateVisitor遍历表达式树来计算结果。
5.5 单分派 vs 双重分派
- 单分派:方法调用仅根据一个对象的实际类型(接收者)动态决定执行哪个方法。Java、C++(默认)、C#(默认)均属单分派语言。
- 双重分派:方法调用同时根据两个对象的实际类型(接收者和参数)动态决定。访问者模式通过
accept+visit两次调用模拟双重分派,弥补了Java语言层面的不足。
六、适用场景分析(重点强化)
场景一:购物车商品价格计算
完整Demo代码
以下Demo展示一个购物车系统,包含书籍、电子产品、水果三种商品类型,实现价格计算、税费计算、折扣计算三个访问者。
// ---------- 元素接口与具体元素 ----------
interface CartItem {
void accept(CartVisitor visitor);
String getName();
double getBasePrice();
}
class BookItem implements CartItem {
private String name; private double price; private String author;
public BookItem(String name, double price, String author) {
this.name = name; this.price = price; this.author = author;
}
@Override public void accept(CartVisitor visitor) { visitor.visit(this); }
@Override public String getName() { return name; }
@Override public double getBasePrice() { return price; }
public String getAuthor() { return author; }
}
class ElectronicItem implements CartItem {
private String name; private double price; private String brand;
public ElectronicItem(String name, double price, String brand) {
this.name = name; this.price = price; this.brand = brand;
}
@Override public void accept(CartVisitor visitor) { visitor.visit(this); }
@Override public String getName() { return name; }
@Override public double getBasePrice() { return price; }
public String getBrand() { return brand; }
}
class FruitItem implements CartItem {
private String name; private double pricePerKg; private double weight;
public FruitItem(String name, double pricePerKg, double weight) {
this.name = name; this.pricePerKg = pricePerKg; this.weight = weight;
}
@Override public void accept(CartVisitor visitor) { visitor.visit(this); }
@Override public String getName() { return name; }
@Override public double getBasePrice() { return pricePerKg * weight; }
public double getWeight() { return weight; }
public double getPricePerKg() { return pricePerKg; }
}
// ---------- 访问者接口 ----------
interface CartVisitor {
void visit(BookItem book);
void visit(ElectronicItem electronic);
void visit(FruitItem fruit);
}
// ---------- 具体访问者:价格计算 ----------
class PriceVisitor implements CartVisitor {
private double total = 0;
@Override public void visit(BookItem book) { total += book.getBasePrice(); }
@Override public void visit(ElectronicItem elec) { total += elec.getBasePrice(); }
@Override public void visit(FruitItem fruit) { total += fruit.getBasePrice(); }
public double getTotal() { return total; }
}
// ---------- 具体访问者:税费计算 ----------
class TaxVisitor implements CartVisitor {
private double totalTax = 0;
@Override public void visit(BookItem book) { /* 书籍免税 */ }
@Override public void visit(ElectronicItem elec) { totalTax += elec.getBasePrice() * 0.15; }
@Override public void visit(FruitItem fruit) { totalTax += fruit.getBasePrice() * 0.05; }
public double getTotalTax() { return totalTax; }
}
// ---------- 具体访问者:折扣计算(满减、会员折扣等) ----------
class DiscountVisitor implements CartVisitor {
private double discount = 0;
@Override public void visit(BookItem book) { discount += book.getBasePrice() * 0.1; } // 图书9折
@Override public void visit(ElectronicItem elec) { discount += elec.getBasePrice() * 0.05; } // 电子95折
@Override public void visit(FruitItem fruit) { discount += 2.0; } // 水果立减2元
public double getTotalDiscount() { return discount; }
}
// ---------- 客户端 ----------
public class ShoppingCartDemo {
public static void main(String[] args) {
List<CartItem> items = Arrays.asList(
new BookItem("Effective Java", 60.0, "Joshua Bloch"),
new ElectronicItem("Headphones", 200.0, "Sony"),
new FruitItem("Apple", 8.0, 1.5) // 单价8元/kg,1.5kg
);
PriceVisitor priceVisitor = new PriceVisitor();
TaxVisitor taxVisitor = new TaxVisitor();
DiscountVisitor discountVisitor = new DiscountVisitor();
for (CartItem item : items) {
item.accept(priceVisitor);
item.accept(taxVisitor);
item.accept(discountVisitor);
}
double total = priceVisitor.getTotal();
double tax = taxVisitor.getTotalTax();
double discount = discountVisitor.getTotalDiscount();
System.out.printf("原价总计: %.2f\n", total);
System.out.printf("税费: %.2f\n", tax);
System.out.printf("折扣: %.2f\n", discount);
System.out.printf("实付: %.2f\n", total + tax - discount);
}
}
类图
classDiagram
class CartItem {
<<interface>>
+accept(CartVisitor)
+getName() String
+getBasePrice() double
}
class BookItem {
-name: String
-price: double
-author: String
+accept(CartVisitor)
+getAuthor() String
}
class ElectronicItem {
-name: String
-price: double
-brand: String
+accept(CartVisitor)
+getBrand() String
}
class FruitItem {
-name: String
-pricePerKg: double
-weight: double
+accept(CartVisitor)
+getWeight() double
}
class CartVisitor {
<<interface>>
+visit(BookItem)
+visit(ElectronicItem)
+visit(FruitItem)
}
class PriceVisitor {
-total: double
+visit(BookItem)
+visit(ElectronicItem)
+visit(FruitItem)
+getTotal() double
}
class TaxVisitor {
-totalTax: double
+visit(BookItem)
+visit(ElectronicItem)
+visit(FruitItem)
+getTotalTax() double
}
class DiscountVisitor {
-discount: double
+visit(BookItem)
+visit(ElectronicItem)
+visit(FruitItem)
+getTotalDiscount() double
}
CartItem <|.. BookItem
CartItem <|.. ElectronicItem
CartItem <|.. FruitItem
CartVisitor <|.. PriceVisitor
CartVisitor <|.. TaxVisitor
CartVisitor <|.. DiscountVisitor
BookItem ..> CartVisitor : visitor.visit(this)
ElectronicItem ..> CartVisitor : visitor.visit(this)
FruitItem ..> CartVisitor : visitor.visit(this)
文字说明
购物车场景是展示访问者模式优势的典型范例。在电商系统中,商品类型(书籍、电子产品、水果)是相对稳定的核心领域模型,而促销活动、税费政策、积分计算等业务规则却频繁变更。若采用传统方案,每次新增业务规则都需在CartItem子类中添加新方法,这将导致核心领域模型不断膨胀且与业务逻辑紧密耦合。
访问者模式通过将操作外置到CartVisitor接口,完美解决了这一矛盾。从类图可见,BookItem、ElectronicItem、FruitItem仅保留最基本的属性和accept方法,而PriceVisitor、TaxVisitor、DiscountVisitor各自封装了独立的计算逻辑。当需要新增“积分计算”功能时,只需创建一个新的PointsVisitor类并实现三个visit方法,无需修改任何现有商品类。此外,访问者内部状态(如total)的自然累积能力使得归约计算变得极为简洁。客户端仅需循环调用accept即可完成复杂聚合,代码清晰且符合开闭原则。
场景二:编译器AST遍历与语义分析
完整Demo代码
模拟一个极简编程语言的AST,支持变量声明、赋值语句和整数表达式。实现类型检查、代码生成和格式化打印三个访问者。
// ---------- AST节点接口 ----------
interface ASTNode {
void accept(ASTVisitor visitor);
}
// 变量声明节点: int x;
class VarDeclNode implements ASTNode {
private String type; // "int" 或 "float"
private String name;
public VarDeclNode(String type, String name) { this.type = type; this.name = name; }
public String getType() { return type; }
public String getName() { return name; }
@Override public void accept(ASTVisitor visitor) { visitor.visit(this); }
}
// 赋值语句节点: x = 5 + 3;
class AssignNode implements ASTNode {
private String varName;
private ASTNode expression;
public AssignNode(String varName, ASTNode expr) { this.varName = varName; this.expression = expr; }
public String getVarName() { return varName; }
public ASTNode getExpression() { return expression; }
@Override public void accept(ASTVisitor visitor) { visitor.visit(this); }
}
// 整数常量节点
class IntLiteralNode implements ASTNode {
private int value;
public IntLiteralNode(int value) { this.value = value; }
public int getValue() { return value; }
@Override public void accept(ASTVisitor visitor) { visitor.visit(this); }
}
// 二元表达式节点
class BinaryExprNode implements ASTNode {
private ASTNode left, right;
private char operator;
public BinaryExprNode(ASTNode left, char op, ASTNode right) {
this.left = left; this.right = right; this.operator = op;
}
public ASTNode getLeft() { return left; }
public ASTNode getRight() { return right; }
public char getOperator() { return operator; }
@Override public void accept(ASTVisitor visitor) { visitor.visit(this); }
}
// ---------- 访问者接口 ----------
interface ASTVisitor {
void visit(VarDeclNode node);
void visit(AssignNode node);
void visit(IntLiteralNode node);
void visit(BinaryExprNode node);
}
// ---------- 类型检查访问者 ----------
class TypeCheckVisitor implements ASTVisitor {
private Map<String, String> symbolTable = new HashMap<>();
private List<String> errors = new ArrayList<>();
@Override public void visit(VarDeclNode node) {
if (symbolTable.containsKey(node.getName())) {
errors.add("Variable " + node.getName() + " already declared");
} else {
symbolTable.put(node.getName(), node.getType());
}
}
@Override public void visit(AssignNode node) {
if (!symbolTable.containsKey(node.getVarName())) {
errors.add("Variable " + node.getVarName() + " not declared");
}
node.getExpression().accept(this); // 递归检查表达式
}
@Override public void visit(IntLiteralNode node) { /* 字面量总是类型正确 */ }
@Override public void visit(BinaryExprNode node) {
node.getLeft().accept(this);
node.getRight().accept(this);
// 此处可添加类型兼容性检查,略...
}
public boolean hasErrors() { return !errors.isEmpty(); }
public void printErrors() { errors.forEach(System.err::println); }
}
// ---------- 代码生成访问者(输出伪汇编) ----------
class CodeGenVisitor implements ASTVisitor {
private StringBuilder code = new StringBuilder();
private int tempCount = 1;
@Override public void visit(VarDeclNode node) {
code.append("ALLOC ").append(node.getName()).append("\n");
}
@Override public void visit(AssignNode node) {
node.getExpression().accept(this);
code.append("STORE ").append(node.getVarName()).append("\n");
}
@Override public void visit(IntLiteralNode node) {
code.append("LOADI ").append(node.getValue()).append("\n");
}
@Override public void visit(BinaryExprNode node) {
node.getLeft().accept(this);
node.getRight().accept(this);
String op = node.getOperator() == '+' ? "ADD" : "SUB";
code.append(op).append("\n");
}
public String getCode() { return code.toString(); }
}
// ---------- 格式化打印访问者(Pretty Printer) ----------
class PrettyPrintVisitor implements ASTVisitor {
private int indent = 0;
private StringBuilder out = new StringBuilder();
private void printIndent() { for(int i=0; i<indent; i++) out.append(" "); }
@Override public void visit(VarDeclNode node) {
printIndent(); out.append(node.getType()).append(" ").append(node.getName()).append(";\n");
}
@Override public void visit(AssignNode node) {
printIndent(); out.append(node.getVarName()).append(" = ");
node.getExpression().accept(this);
out.append(";\n");
}
@Override public void visit(IntLiteralNode node) { out.append(node.getValue()); }
@Override public void visit(BinaryExprNode node) {
out.append("(");
node.getLeft().accept(this);
out.append(" ").append(node.getOperator()).append(" ");
node.getRight().accept(this);
out.append(")");
}
public String getOutput() { return out.toString(); }
}
// ---------- 客户端模拟编译流程 ----------
public class CompilerDemo {
public static void main(String[] args) {
// 构建AST: int x; x = (5 + 3) - 2;
ASTNode program = new AssignNode("x",
new BinaryExprNode(
new BinaryExprNode(new IntLiteralNode(5), '+', new IntLiteralNode(3)),
'-',
new IntLiteralNode(2)
));
// 注意:实际会有多个语句列表,此处简化
// 多遍扫描:类型检查 -> 代码生成 -> 格式化输出
TypeCheckVisitor typeChecker = new TypeCheckVisitor();
program.accept(typeChecker);
if (typeChecker.hasErrors()) {
typeChecker.printErrors();
return;
}
CodeGenVisitor codeGen = new CodeGenVisitor();
program.accept(codeGen);
System.out.println("Generated Code:\n" + codeGen.getCode());
PrettyPrintVisitor printer = new PrettyPrintVisitor();
program.accept(printer);
System.out.println("Formatted AST:\n" + printer.getOutput());
}
}
时序图:多遍扫描流程
sequenceDiagram
participant Compiler as 编译器驱动
participant AST as 根节点(AssignNode)
participant Visitor1 as TypeCheckVisitor
participant Visitor2 as CodeGenVisitor
participant Visitor3 as PrettyPrintVisitor
Compiler->>AST: accept(TypeCheckVisitor)
activate AST
AST->>Visitor1: visit(this) [AssignNode]
Visitor1->>AST: 递归访问表达式子节点
AST-->>Compiler: 类型检查完成
deactivate AST
Compiler->>AST: accept(CodeGenVisitor)
activate AST
AST->>Visitor2: visit(this) [AssignNode]
Visitor2->>AST: 递归生成子节点代码
AST-->>Compiler: 代码生成完成
deactivate AST
Compiler->>AST: accept(PrettyPrintVisitor)
activate AST
AST->>Visitor3: visit(this) [AssignNode]
Visitor3->>AST: 递归格式化子节点
AST-->>Compiler: 格式化输出完成
deactivate AST
文字说明
编译器前端处理流程是访问者模式最为经典的应用领域之一。从时序图可见,编译过程通常包含多遍扫描:语法分析器首先构建出抽象语法树(AST),随后类型检查、语义分析、中间代码生成、目标代码生成等多个阶段依次遍历同一棵AST。每个阶段由一个独立的访问者实现,例如TypeCheckVisitor负责构建符号表并检查类型一致性,CodeGenVisitor生成目标机器的指令序列,PrettyPrintVisitor则将AST还原为格式化源码。
这种设计带来的巨大优势在于关注点分离与可扩展性。真实的Java编译器(如javac)内部正是采用了类似的架构:com.sun.tools.javac.tree.TreeScanner就是一个抽象访问者基类,其子类Enter(符号录入)、Attr(属性标注)、Flow(数据流分析)均通过重载visitXxx方法实现各自功能。当需要增加新的编译阶段(如新增代码优化遍),只需派生新的访问者子类,而无需修改AST节点定义。访问者模式使得编译器的演进与维护变得极为清晰高效。
场景三:文件系统批量操作
完整Demo代码
模拟文件系统节点,实现图片、文档、视频三种文件类型,通过访问者完成压缩、加密、缩略图生成操作。
// ---------- 文件元素接口 ----------
interface FileElement {
void accept(FileVisitor visitor);
String getName();
long getSize();
}
class ImageFile implements FileElement {
private String name; private long size; private String format;
public ImageFile(String name, long size, String format) {
this.name = name; this.size = size; this.format = format;
}
@Override public void accept(FileVisitor visitor) { visitor.visit(this); }
@Override public String getName() { return name; }
@Override public long getSize() { return size; }
public String getFormat() { return format; }
}
class DocumentFile implements FileElement {
private String name; private long size; private String docType;
public DocumentFile(String name, long size, String docType) {
this.name = name; this.size = size; this.docType = docType;
}
@Override public void accept(FileVisitor visitor) { visitor.visit(this); }
@Override public String getName() { return name; }
@Override public long getSize() { return size; }
public String getDocType() { return docType; }
}
class VideoFile implements FileElement {
private String name; private long size; private int duration;
public VideoFile(String name, long size, int duration) {
this.name = name; this.size = size; this.duration = duration;
}
@Override public void accept(FileVisitor visitor) { visitor.visit(this); }
@Override public String getName() { return name; }
@Override public long getSize() { return size; }
public int getDuration() { return duration; }
}
// 目录节点(对象结构的一部分)
class Directory {
private String name;
private List<FileElement> files = new ArrayList<>();
private List<Directory> subDirs = new ArrayList<>();
public Directory(String name) { this.name = name; }
public void addFile(FileElement file) { files.add(file); }
public void addSubDir(Directory dir) { subDirs.add(dir); }
public String getName() { return name; }
public List<FileElement> getFiles() { return files; }
public List<Directory> getSubDirs() { return subDirs; }
// 递归遍历访问者
public void accept(FileVisitor visitor) {
for (FileElement file : files) {
file.accept(visitor);
}
for (Directory sub : subDirs) {
sub.accept(visitor); // 递归进入子目录
}
}
}
// ---------- 访问者接口 ----------
interface FileVisitor {
void visit(ImageFile image);
void visit(DocumentFile doc);
void visit(VideoFile video);
}
// ---------- 压缩访问者 ----------
class CompressVisitor implements FileVisitor {
@Override public void visit(ImageFile image) {
System.out.println("Compressing image: " + image.getName() + " (format: " + image.getFormat() + ")");
}
@Override public void visit(DocumentFile doc) {
System.out.println("Compressing document: " + doc.getName() + " (type: " + doc.getDocType() + ")");
}
@Override public void visit(VideoFile video) {
System.out.println("Compressing video: " + video.getName() + " (duration: " + video.getDuration() + "s)");
}
}
// ---------- 加密访问者 ----------
class EncryptVisitor implements FileVisitor {
@Override public void visit(ImageFile image) {
System.out.println("Encrypting image: " + image.getName() + " with AES-256");
}
@Override public void visit(DocumentFile doc) {
System.out.println("Encrypting document: " + doc.getName() + " with AES-256");
}
@Override public void visit(VideoFile video) {
System.out.println("Encrypting video: " + video.getName() + " with AES-256");
}
}
// ---------- 缩略图生成访问者(仅对图片、视频有效) ----------
class ThumbnailVisitor implements FileVisitor {
@Override public void visit(ImageFile image) {
System.out.println("Generating thumbnail for image: " + image.getName());
}
@Override public void visit(DocumentFile doc) {
// 文档不生成缩略图
}
@Override public void visit(VideoFile video) {
System.out.println("Generating thumbnail for video: " + video.getName());
}
}
// ---------- 客户端 ----------
public class FileSystemDemo {
public static void main(String[] args) {
Directory root = new Directory("root");
root.addFile(new ImageFile("photo.jpg", 2048000, "JPEG"));
root.addFile(new DocumentFile("report.pdf", 512000, "PDF"));
Directory sub = new Directory("videos");
sub.addFile(new VideoFile("movie.mp4", 1024000000, 3600));
root.addSubDir(sub);
CompressVisitor compress = new CompressVisitor();
EncryptVisitor encrypt = new EncryptVisitor();
ThumbnailVisitor thumbnail = new ThumbnailVisitor();
System.out.println("=== 执行压缩 ===");
root.accept(compress);
System.out.println("\n=== 执行加密 ===");
root.accept(encrypt);
System.out.println("\n=== 生成缩略图 ===");
root.accept(thumbnail);
}
}
流程图:文件遍历与操作分发
flowchart TD
A[开始遍历根目录] --> B{遍历当前目录文件列表}
B -->|每个文件| C[文件类型判断: Image/Document/Video]
C -->|ImageFile| D[调用 ImageFile.accept]
C -->|DocumentFile| E[调用 DocumentFile.accept]
C -->|VideoFile| F[调用 VideoFile.accept]
D --> G[visitor.visit ImageFile]
E --> H[visitor.visit DocumentFile]
F --> I[visitor.visit VideoFile]
G --> J[执行图片特定操作 压缩/加密/缩略图]
H --> K[执行文档特定操作]
I --> L[执行视频特定操作]
B -->|文件处理完毕| M{存在子目录?}
M -->|是| N[递归进入子目录]
N --> B
M -->|否| O[遍历结束]
文字说明
文件系统批量操作是访问者模式另一个典型场景。流程图清晰展示了遍历器(Directory.accept)如何递归驱动整个文件树的访问过程。对于目录下的每个文件,遍历器调用其accept方法;文件对象根据自身具体类型(ImageFile、DocumentFile、VideoFile)分派到不同的visit重载方法中。
Java NIO的Files.walkFileTree正是这一模式的标准实现。其内部采用深度优先遍历,对每个文件和目录回调FileVisitor的对应方法。开发人员只需实现visitFile、preVisitDirectory等方法,即可在不修改文件系统API的前提下,实现自定义的批量处理逻辑。例如,本例中的CompressVisitor、EncryptVisitor和ThumbnailVisitor代表了三种完全不同的操作策略。若未来需要新增“病毒扫描”功能,只需添加VirusScanVisitor类,而无需改动任何文件元素类。这种可插拔的处理机制在大型内容管理系统中极大地降低了维护成本,使得文件处理功能可以像插件一样自由组合与扩展。
场景四:JSON/XML文档处理
完整Demo代码
模拟JSON节点结构,支持对象、数组、字符串值、数值四种节点类型。实现Schema校验、值脱敏两个访问者。
// ---------- JSON节点接口 ----------
interface JsonNode {
void accept(JsonVisitor visitor);
}
class JsonObjectNode implements JsonNode {
private Map<String, JsonNode> fields = new LinkedHashMap<>();
public void put(String key, JsonNode value) { fields.put(key, value); }
public Map<String, JsonNode> getFields() { return fields; }
@Override public void accept(JsonVisitor visitor) { visitor.visit(this); }
}
class JsonArrayNode implements JsonNode {
private List<JsonNode> items = new ArrayList<>();
public void add(JsonNode item) { items.add(item); }
public List<JsonNode> getItems() { return items; }
@Override public void accept(JsonVisitor visitor) { visitor.visit(this); }
}
class JsonStringNode implements JsonNode {
private String value;
public JsonStringNode(String value) { this.value = value; }
public String getValue() { return value; }
@Override public void accept(JsonVisitor visitor) { visitor.visit(this); }
}
class JsonNumberNode implements JsonNode {
private double value;
public JsonNumberNode(double value) { this.value = value; }
public double getValue() { return value; }
@Override public void accept(JsonVisitor visitor) { visitor.visit(this); }
}
// ---------- 访问者接口 ----------
interface JsonVisitor {
void visit(JsonObjectNode obj);
void visit(JsonArrayNode arr);
void visit(JsonStringNode str);
void visit(JsonNumberNode num);
}
// ---------- Schema校验访问者 ----------
class SchemaValidator implements JsonVisitor {
private List<String> errors = new ArrayList<>();
@Override public void visit(JsonObjectNode obj) {
// 校验必填字段等,此处略
obj.getFields().values().forEach(n -> n.accept(this));
}
@Override public void visit(JsonArrayNode arr) {
arr.getItems().forEach(n -> n.accept(this));
}
@Override public void visit(JsonStringNode str) {
if (str.getValue().length() > 100) errors.add("String too long");
}
@Override public void visit(JsonNumberNode num) {
if (num.getValue() < 0) errors.add("Negative number not allowed");
}
public boolean isValid() { return errors.isEmpty(); }
public List<String> getErrors() { return errors; }
}
// ---------- 脱敏访问者(手机号、邮箱打码) ----------
class DesensitizeVisitor implements JsonVisitor {
private static final Pattern PHONE = Pattern.compile("\\d{11}");
private static final Pattern EMAIL = Pattern.compile("\\w+@\\w+\\.\\w+");
@Override public void visit(JsonObjectNode obj) {
obj.getFields().values().forEach(n -> n.accept(this));
}
@Override public void visit(JsonArrayNode arr) {
arr.getItems().forEach(n -> n.accept(this));
}
@Override public void visit(JsonStringNode str) {
String val = str.getValue();
if (PHONE.matcher(val).matches()) {
// 实际应修改节点内容,此处仅演示
System.out.println("Desensitized phone: " + val.substring(0,3) + "****" + val.substring(7));
} else if (EMAIL.matcher(val).matches()) {
System.out.println("Desensitized email: " + val.replaceAll("(.).*(@.*)", "$1***$2"));
}
}
@Override public void visit(JsonNumberNode num) { /* 数字不处理 */ }
}
// ---------- 客户端 ----------
public class JsonProcessorDemo {
public static void main(String[] args) {
JsonObjectNode user = new JsonObjectNode();
user.put("name", new JsonStringNode("John Doe"));
user.put("phone", new JsonStringNode("13800138000"));
user.put("age", new JsonNumberNode(30));
SchemaValidator validator = new SchemaValidator();
user.accept(validator);
System.out.println("Schema valid: " + validator.isValid());
DesensitizeVisitor desensitize = new DesensitizeVisitor();
user.accept(desensitize);
}
}
类图
classDiagram
class JsonNode {
<<interface>>
+accept(JsonVisitor)
}
class JsonObjectNode {
-fields: Map
+put(String JsonNode)
+accept(JsonVisitor)
}
class JsonArrayNode {
-items: List~JsonNode~
+add(JsonNode)
+accept(JsonVisitor)
}
class JsonStringNode {
-value: String
+accept(JsonVisitor)
}
class JsonNumberNode {
-value: double
+accept(JsonVisitor)
}
class JsonVisitor {
<<interface>>
+visit(JsonObjectNode)
+visit(JsonArrayNode)
+visit(JsonStringNode)
+visit(JsonNumberNode)
}
class SchemaValidator {
-errors: List~String~
+visit(...)
}
class DesensitizeVisitor {
+visit(...)
}
JsonNode <|.. JsonObjectNode
JsonNode <|.. JsonArrayNode
JsonNode <|.. JsonStringNode
JsonNode <|.. JsonNumberNode
JsonVisitor <|.. SchemaValidator
JsonVisitor <|.. DesensitizeVisitor
JsonObjectNode ..> JsonVisitor
JsonArrayNode ..> JsonVisitor
JsonStringNode ..> JsonVisitor
JsonNumberNode ..> JsonVisitor
文字说明
JSON处理是访问者模式在数据交换领域的典型应用。类图中,JsonNode作为抽象元素,其四种具体子类覆盖了JSON规范的核心数据类型。SchemaValidator和DesensitizeVisitor分别代表了两种不同的处理视角:前者关注数据结构的合规性,后者关注敏感信息的保护。
与Jackson库中的JsonNode遍历机制对比:Jackson提供了JsonNodeVisitor接口(内部使用),但更常用的是基于JsonParser的流式遍历或JsonPointer的路径访问。然而,当需要对整个JSON树执行复杂的状态相关操作(如Schema校验需跨节点上下文)时,访问者模式的优势便凸显出来。例如,SchemaValidator可以在遍历过程中维护路径栈,精准定位错误字段位置。相较于JsonPath的声明式查询,访问者模式更适合全量遍历与归约计算,且扩展新的遍历逻辑(如统计节点数量、转换字段命名风格)极为方便,无需引入额外的查询语言学习成本。
场景五:报表导出引擎
完整Demo代码
报表包含文本、图表、表格三种元素,通过访问者导出为PDF、Excel、HTML格式。
// ---------- 报表元素接口 ----------
interface ReportElement {
void accept(ReportVisitor visitor);
}
class TextElement implements ReportElement {
private String content;
public TextElement(String content) { this.content = content; }
public String getContent() { return content; }
@Override public void accept(ReportVisitor visitor) { visitor.visit(this); }
}
class ChartElement implements ReportElement {
private String type; // "bar", "pie", "line"
private int[][] data;
public ChartElement(String type, int[][] data) { this.type = type; this.data = data; }
public String getType() { return type; }
public int[][] getData() { return data; }
@Override public void accept(ReportVisitor visitor) { visitor.visit(this); }
}
class TableElement implements ReportElement {
private String[] headers;
private String[][] rows;
public TableElement(String[] headers, String[][] rows) {
this.headers = headers; this.rows = rows;
}
public String[] getHeaders() { return headers; }
public String[][] getRows() { return rows; }
@Override public void accept(ReportVisitor visitor) { visitor.visit(this); }
}
// ---------- 访问者接口 ----------
interface ReportVisitor {
void visit(TextElement text);
void visit(ChartElement chart);
void visit(TableElement table);
}
// ---------- PDF导出访问者 ----------
class PDFExportVisitor implements ReportVisitor {
private StringBuilder pdf = new StringBuilder();
@Override public void visit(TextElement text) {
pdf.append("[PDF Text] ").append(text.getContent()).append("\n");
}
@Override public void visit(ChartElement chart) {
pdf.append("[PDF Chart: ").append(chart.getType()).append("] (rendered)\n");
}
@Override public void visit(TableElement table) {
pdf.append("[PDF Table]\n");
for (String h : table.getHeaders()) pdf.append(h).append("\t");
pdf.append("\n");
for (String[] row : table.getRows()) {
for (String cell : row) pdf.append(cell).append("\t");
pdf.append("\n");
}
}
public String getPDF() { return pdf.toString(); }
}
// ---------- Excel导出访问者 ----------
class ExcelExportVisitor implements ReportVisitor {
private StringBuilder excel = new StringBuilder();
@Override public void visit(TextElement text) {
excel.append("Cell A1: ").append(text.getContent()).append("\n");
}
@Override public void visit(ChartElement chart) {
excel.append("[Excel Chart Object: ").append(chart.getType()).append("]\n");
}
@Override public void visit(TableElement table) {
excel.append("[Excel Sheet]\n");
// 生成行列...
}
public String getExcel() { return excel.toString(); }
}
// ---------- 客户端 ----------
public class ReportExportDemo {
public static void main(String[] args) {
List<ReportElement> report = Arrays.asList(
new TextElement("Annual Sales Report 2025"),
new ChartElement("bar", new int[][]{{100,200},{150,250}}),
new TableElement(new String[]{"Q1","Q2"}, new String[][]{{"$10K","$12K"},{"$8K","$9K"}})
);
PDFExportVisitor pdfExporter = new PDFExportVisitor();
ExcelExportVisitor excelExporter = new ExcelExportVisitor();
for (ReportElement elem : report) {
elem.accept(pdfExporter);
elem.accept(excelExporter);
}
System.out.println("=== PDF Output ===\n" + pdfExporter.getPDF());
System.out.println("=== Excel Output ===\n" + excelExporter.getExcel());
}
}
流程图:报表导出流程
flowchart LR
A[报表元素集合] --> B{遍历元素}
B --> C[TextElement]
B --> D[ChartElement]
B --> E[TableElement]
C --> F[accept PDFVisitor]
D --> F
E --> F
C --> G[accept ExcelVisitor]
D --> G
E --> G
F --> H[生成PDF格式内容]
G --> I[生成Excel格式内容]
H --> J[合并为PDF文档]
I --> K[合并为Excel工作簿]
文字说明
报表导出引擎是展示访问者模式格式与数据分离优势的绝佳场景。流程图表明,同一份报表元素集合(TextElement、ChartElement、TableElement)可以同时传递给多个不同的导出访问者,每个访问者根据自身的目标格式生成对应的输出。
在实际企业级报表系统中(如JasperReports、Apache POI报表),数据模型(如报表带、字段、图像)是高度稳定的,而输出格式(PDF、HTML、XLSX、CSV、DOCX)却随需求不断增长。若采用在元素类中硬编码导出逻辑的方式,每新增一种格式都需修改所有元素类,导致核心领域模型臃肿不堪。访问者模式通过将导出逻辑外置到ReportVisitor实现类中,实现了格式与数据的彻底解耦。例如,当需要支持Markdown格式导出时,只需新增MarkdownExportVisitor,并逐一实现visit方法,无需触碰任何ReportElement子类。此外,访问者内部状态(如pdf字符串构建器)可以自然地累积各元素的输出片段,最终合并为完整的文档,这种归约过程与报表生成的业务语义高度契合。
七、面试题精选与专家级解答
Q1: 什么是双重分派?Java是单分派还是双分派语言?访问者模式如何模拟双重分派?
解答:
- 双重分派(Double Dispatch):指方法调用时,根据两个对象的实际类型来动态决定执行哪段代码。
- Java是单分派语言:方法调用时,JVM仅根据接收者(receiver)的实际类型进行动态绑定(虚方法调用),而参数的类型在编译期已静态确定(重载方法选择基于参数的声明类型)。例如
void foo(A a)和void foo(B b),调用foo(x)时选择哪个重载版本完全由x的声明类型决定。 - 访问者模式模拟双重分派:
element.accept(visitor)—— 第一重分派:根据element的实际类型调用对应的accept方法。visitor.visit(this)—— 第二重分派:在accept内部,this具有明确的具体类型,编译器据此选择正确的重载visit方法;随后JVM根据visitor的实际类型调用具体实现。
Q2: 访问者模式违背了哪些设计原则?它的主要缺点是什么?在什么情况下应避免使用?
解答:
- 违背原则:
- 依赖倒置原则:访问者接口依赖于具体元素类,导致访问者与元素的具体类型耦合。
- 迪米特法则:访问者需要了解元素内部细节(如属性getter),可能破坏封装。
- 主要缺点:
- 新增元素困难:若对象结构频繁增加新的元素类,需在所有访问者接口及实现中添加新的
visit方法。 - 破坏封装:访问者可能需要暴露元素的内部状态(通过getter),可能导致元素类封装性下降。
- 新增元素困难:若对象结构频繁增加新的元素类,需在所有访问者接口及实现中添加新的
- 避免使用场景:当元素类型经常变化(如插件系统)、元素间存在复杂继承关系、或操作相对稳定时,访问者模式反而会增加维护成本。
Q3: JDK中哪些地方使用了访问者模式?请至少列举三个并分析其实现细节
解答:
- java.nio.file.FileVisitor:遍历文件树。
FileVisitor定义了preVisitDirectory、visitFile等方法,Files.walkFileTree负责驱动遍历。开发者实现该接口即可自定义文件处理逻辑。 - javax.lang.model.element.ElementVisitor:注解处理API中用于遍历程序元素(包、类、方法、变量)。
AbstractElementVisitor8提供默认实现,便于子类仅覆写关心的方法。支持泛型返回值R和附加参数P。 - javax.lang.model.element.AnnotationValueVisitor:用于处理注解值元素,支持对不同类型注解值(如数组、枚举常量、基本类型)执行不同操作。
Q4: ASM字节码框架为什么选择访问者模式?ClassVisitor的设计有什么优势?
解答:
- 原因:字节码结构(类、方法、字段、注解)由JVM规范固定,极难变化;而字节码操作场景(增强、分析、统计)多种多样。访问者模式完美契合“稳定数据结构+多变操作”的特点。
- ClassVisitor优势:
- 高性能:直接在一次遍历中完成字节码解析与修改,无需构造中间DOM树,内存占用极低。
- 灵活组合:可通过
ClassVisitor链实现多遍处理(类似Servlet Filter链),每个访问者专注一个功能(如添加字段、修改方法)。 - 可扩展性:新增字节码操作功能仅需派生新的
ClassVisitor子类,无需修改ASM核心库。
Q5: 访问者模式和策略模式在结构上完全不同,但有时可以解决相似问题,如何选择?
解答:
- 策略模式:封装算法,使它们可以互相替换。适用于单一上下文,算法的变化独立于使用算法的客户端。例如支付策略、压缩策略。
- 访问者模式:适用于稳定的对象结构,需要对结构中多种不同类型的元素执行相关但不同的操作。例如AST遍历、报表导出。
- 选择依据:若操作的目标是单一类型的对象,用策略;若目标是一组不同类的对象集合,且操作需随元素类型而变化,用访问者。
Q6: 如何解决访问者模式中元素类型固定导致新增元素困难的问题?请简述几种方案
解答:
- 默认适配器:提供抽象访问者基类,对所有
visit方法提供默认空实现。新增元素时,仅需在基类中添加新方法并默认实现,已有具体访问者不受影响(但需重新编译)。 - 反射分发:在元素基类的
accept中使用反射调用visit方法,根据元素实际类型动态查找。优点是无需为每种元素声明visit重载,但丧失编译期类型安全。 - 组合优于继承:将易变的操作逻辑进一步抽象为策略,访问者内部持有策略映射,元素通过类型标识获取对应策略执行。可避免接口膨胀。
- 访问者模式+工厂模式:由工厂根据元素类型创建对应的处理器,减少访问者接口的
visit方法数量。
Q7: 访问者模式与解释器模式如何配合使用?请以表达式求值为例说明
解答:
- 解释器模式定义文法并构建抽象语法树(AST),如
Expression接口包含interpret()方法。 - 但若需要对同一AST执行多种语义动作(求值、类型检查、代码生成),在
Expression中添加多个方法会违反开闭原则。 - 配合方式:解释器模式负责构建AST,访问者模式负责遍历AST并执行操作。
- 表达式求值示例:
Addition、Subtraction等节点实现accept方法。EvaluateVisitor实现visit(Addition)时递归求值左右子树并相加。这样,求值、打印、类型推导均可作为独立的访问者。
Q8: 在分布式SQL解析场景中,访问者模式如何实现SQL改写与审计?请画出架构图
解答:
- SQL解析:中间件(如ShardingSphere)首先将SQL解析为AST,包含
SelectStatement、TableSegment、WhereSegment等节点。 - 访问者实现:
SQLRewriteVisitor:遍历AST,将逻辑表名替换为物理表名,添加分片列等。SQLAuditVisitor:遍历AST,检查用户对表的操作权限、SQL注入风险。
- 架构图:见本文第四章“分布式SQL解析访问者架构流程图”。
Q9: 如何用Java 8的默认方法简化访问者模式的扩展?请给出代码示例
解答:
- 利用接口默认方法提供
visit的默认实现,使得新增元素类型时,已有访问者实现类无需强制实现新方法。
interface Visitor {
default void visit(ElementA a) { /* 默认实现 */ }
default void visit(ElementB b) { /* 默认实现 */ }
// 新增元素C时,添加默认方法
default void visit(ElementC c) { /* 默认实现,已有访问者无需修改 */ }
}
- 进一步,可结合函数式接口,将访问者简化为
Map<Class<?>, Consumer<Element>>,但会损失编译时类型检查。
Q10: 访问者模式中的对象结构应该由谁负责遍历?遍历逻辑放在ObjectStructure、访问者还是客户端各有什么优劣?
解答:
- ObjectStructure负责遍历:最常见方式。优点:客户端无需关心遍历细节,符合单一职责。缺点:遍历策略固定,难以定制。
- 访问者负责遍历:在
visit方法中递归遍历子节点(如ASM的MethodVisitor.visitCode后需主动调用visitEnd)。优点:可灵活控制遍历深度和顺序。缺点:访问者与元素结构耦合。 - 客户端负责遍历:客户端持有集合,手动迭代并调用
accept。优点:遍历逻辑完全自由。缺点:客户端代码重复,且可能破坏对象结构封装。 - 最佳实践:简单线性结构由
ObjectStructure遍历;树形递归结构由访问者在visit中递归;若需多种遍历策略,可引入迭代器模式配合访问者。
八、结语
访问者模式作为GoF 23种设计模式中较为复杂的一员,其核心价值在于解决了稳定的数据结构与多变的行为操作之间的矛盾。通过双重分派机制,它在单分派语言的局限下实现了操作与元素类型的动态绑定,为编译器、解析器、报表引擎等基础软件提供了优雅的扩展方案。
然而,访问者模式并非银弹。它对元素类型稳定的强依赖使得其在面对频繁新增元素类型的场景时显得力不从心。在实际应用中,我们需谨慎权衡数据结构的稳定性和操作的可变性。当元素类型相对固定,而上层业务逻辑层出不穷时,访问者模式无疑是解耦领域模型与业务操作的利器。
从JDK的文件遍历到ASM的字节码操作,从ShardingSphere的SQL解析到Apollo的配置处理,访问者模式在Java生态的底层框架中熠熠生辉。掌握其原理与变体,不仅能提升代码设计水平,更能深入理解众多优秀框架的设计哲学。希望本文详尽的剖析与丰富的Demo能帮助读者彻底驾驭这一模式,在复杂业务系统中游刃有余。