零基础设计模式——行为型模式 - 访问者模式

第四部分:行为型模式 - 访问者模式 (Visitor Pattern)

我们来到了行为型模式的最后一个------访问者模式。这是一个相对复杂但功能强大的模式,它允许你在不修改现有对象结构的前提下,向该结构中的元素添加新的操作。

  • 核心思想:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

访问者模式 (Visitor Pattern)

"表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。" (Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.)

想象一个动物园 (Object Structure),里面有各种动物 (Elements),比如狮子 (Lion)、猴子 (Monkey)、海豚 (Dolphin)。

现在,我们想对这些动物执行一些操作,比如:

  1. 喂食 (FeedOperation):狮子吃肉,猴子吃香蕉,海豚吃鱼。
  2. 听声音 (SoundOperation):狮子吼叫,猴子吱吱叫,海豚发出咔嗒声。
  3. 体检 (HealthCheckOperation):对不同动物进行不同的健康检查项目。

如果我们将这些操作直接添加到动物类中,那么每当需要一个新的操作时,就必须修改所有动物类。这违反了开闭原则,并且如果动物种类很多,维护起来会很麻烦。

访问者模式通过引入一个"访问者 (Visitor)"对象来解决这个问题。访问者对象封装了要对元素执行的操作。

  • 动物 (Element) :每个动物类都有一个 accept(Visitor v) 方法。当调用这个方法时,动物会把自己(this)传递给访问者的一个特定方法,比如 visitor.visitLion(this)visitor.visitMonkey(this)
  • 访问者 (Visitor) :访问者接口定义了一系列 visitConcreteElementX(ConcreteElementX elem) 方法,每种具体动物对应一个。例如,visitLion(Lion lion)visitMonkey(Monkey monkey)
  • 具体访问者 (ConcreteVisitor) :实现 Visitor 接口。例如,FeedVisitor 会在 visitLion 方法中实现喂肉的逻辑,在 visitMonkey 中实现喂香蕉的逻辑。

这样,当你想添加一个新的操作(比如"给动物拍照")时,只需要创建一个新的具体访问者类(PhotoVisitor),而不需要修改任何动物类。

这个过程利用了双重分派 (Double Dispatch)

  1. 第一次分派:客户端调用元素的 accept(visitor) 方法。具体调用哪个元素的 accept 方法是在运行时确定的(取决于元素的实际类型)。
  2. 第二次分派:在元素的 accept 方法内部,调用访问者的 visitConcreteElement(this) 方法。具体调用访问者的哪个 visit 方法(visitLion 还是 visitMonkey)是基于传递给 accept 方法的元素的静态类型(在 accept 方法内部,this 的类型是已知的具体元素类型)和访问者自身的动态类型来确定的。

1. 目的 (Intent)

访问者模式的主要目的:

  1. 分离操作与对象结构:将对对象结构中各元素的操作封装在独立的访问者对象中。
  2. 在不修改元素类的前提下添加新操作:可以轻松地向现有的对象结构添加新的功能(操作),而无需修改构成该结构的元素类。
  3. 集中相关操作:可以将对同一对象结构的多种相关操作集中在一个访问者类中,或者将针对不同元素但属于同一逻辑操作的不同行为集中管理。

2. 生活中的例子 (Real-world Analogy)

  • 税务审计员 (Visitor) 访问不同类型的公司 (Elements)

    • 公司 (Element):上市公司、私营公司、非营利组织。
    • 审计员 (Visitor):税务审计员。
    • 审计员对不同类型的公司有不同的审计流程和关注点。公司允许审计员"访问"(提供账簿、记录等),审计员根据公司类型执行相应的审计操作。
  • 电脑维修技师 (Visitor) 检查电脑的不同部件 (Elements)

    • 电脑部件 (Element):CPU、内存、硬盘、显卡。
    • 维修技师 (Visitor):硬件诊断访问者、性能优化访问者。
    • 技师对不同部件有不同的检查和操作方法。部件允许技师"访问"(提供接口或物理接触),技师根据部件类型执行操作。
  • 编译器处理抽象语法树 (AST)

    • AST节点 (Element):变量声明节点、赋值节点、函数调用节点等。
    • 编译器阶段 (Visitor):类型检查器、代码生成器、优化器。
    • 不同的编译器阶段会对AST节点执行不同的操作。AST节点提供 accept 方法,允许不同的访问者(编译器阶段)遍历并处理它们。

3. 结构 (Structure)

访问者模式通常包含以下角色:

  1. Visitor (访问者接口或抽象类)

    • 为对象结构中的每一种 ConcreteElement 声明一个 visitConcreteElementX(ConcreteElementX element) 操作。
    • 方法的名称通常能反映其所访问的具体元素的类名(例如,visitBookvisitFruit)。
  2. ConcreteVisitor (具体访问者)

    • 实现 Visitor 接口中声明的每个操作。
    • 每一个操作实现对相应 ConcreteElement 的一部分算法。
    • ConcreteVisitor 通常会累积(或计算)一些状态,这些状态反映了遍历元素后的结果。
  3. Element (元素接口或抽象类)

    • 定义一个 accept(Visitor visitor) 操作,它以一个访问者为参数。
  4. ConcreteElement (具体元素)

    • 实现 Element 接口中的 accept() 操作。
    • accept() 操作的典型实现是调用访问者的 visitConcreteElementX(this) 方法,将自身(this)传递给访问者。
  5. ObjectStructure (对象结构)

    • 通常是一个元素的集合(如列表、树)。
    • 可以提供一个高层接口以允许访问者访问它的元素。
    • 可以是一个组合(Composite)模式的实现。
    • 负责枚举其元素,并允许访问者访问这些元素,通常通过迭代器或直接遍历并对每个元素调用 accept() 方法。

4. 适用场景 (When to Use)

  • 当一个对象结构包含许多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作时。
  • 当需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你希望避免让这些操作"污染"这些对象的类时。访问者使得你可以将相关的操作集中起来定义在一个类中。
  • 当对象结构对应的类很少改变,但经常需要在此对象结构上定义新的操作时。
  • 当操作必须作用于整个对象结构,而不仅仅是单个元素时(例如,计算一个复杂结构中所有元素的总和)。访问者可以在遍历过程中累积状态。
  • 当你想在不修改现有类层次结构的前提下,向其添加新的行为时。

5. 优缺点 (Pros and Cons)

优点:

  1. 易于增加新的操作:添加一个新的操作只需要增加一个新的 Visitor 子类,并实现对各个 ConcreteElement 的访问方法。不需要修改现有的 Element 类层次结构,符合开闭原则。
  2. 将有关行为集中到一个访问者对象中:相关的操作被组织在同一个 Visitor 类中,而不是分散到各个 Element 类中,使得代码更易于理解和维护。
  3. 可以访问对象结构中的不同类型元素:访问者可以处理具有不同接口的类。
  4. 可以累积状态:访问者在遍历对象结构时可以累积状态信息。

缺点:

  1. 难以增加新的 ConcreteElement 类 :如果对象结构中的 Element 类层次结构经常发生变化(特别是增加新的 ConcreteElement 类),则使用访问者模式会很麻烦。每增加一个新的 ConcreteElement,就必须在所有 Visitor 接口和所有 ConcreteVisitor 类中添加一个新的 visitConcreteElementX 方法。这违反了开闭原则(对于Element的扩展)。
  2. 破坏封装:访问者模式通常需要 ConcreteElement 提供公共方法来获取其内部状态,以便访问者能够执行操作。这可能会暴露元素的内部实现细节,从而破坏其封装性。有时,为了让访问者能够访问到足够的信息,可能需要在元素类中添加一些原本不需要的 getter 方法。
  3. 访问者可能需要访问元素的私有成员:在某些语言中(如 C++ 的友元机制,或 Java 的包级私有),可以解决这个问题,但在其他情况下可能比较棘手。
  4. 双重分派的复杂性:理解双重分派的机制可能需要一些时间。

6. 实现方式 (Implementations)

让我们以一个简单的购物车系统为例。购物车中有不同类型的商品(BookElectronics),我们想对它们应用不同的操作,比如计算价格(可能包含税费)、计算运费(可能根据重量或类型)。

元素接口和具体元素 (ItemElement, Book, Electronics)
go 复制代码
// item.go (Element interface and ConcreteElements)
package element

import "fmt"

// Forward declaration for Visitor interface
type ItemVisitor interface {
	VisitBook(book *Book)
	VisitElectronics(electronics *Electronics)
}

// ItemElement 元素接口
type ItemElement interface {
	Accept(visitor ItemVisitor)
	GetPrice() float64 // Example property
	GetName() string   // Example property
}

// --- Book --- (具体元素)
type Book struct {
	Name        string
	Price       float64
	ISBN        string
	WeightGrams int
}

func NewBook(name string, price float64, isbn string, weight int) *Book {
	return &Book{Name: name, Price: price, ISBN: isbn, WeightGrams: weight}
}

func (b *Book) Accept(visitor ItemVisitor) {
	visitor.VisitBook(b)
}
func (b *Book) GetPrice() float64 { return b.Price }
func (b *Book) GetName() string   { return b.Name }
func (b *Book) GetISBN() string   { return b.ISBN }
func (b *Book) GetWeight() int    { return b.WeightGrams }

// --- Electronics --- (具体元素)
type Electronics struct {
	Name         string
	Price        float64
	Model        string
	WeightGrams  int
	Fragile      bool
}

func NewElectronics(name string, price float64, model string, weight int, fragile bool) *Electronics {
	return &Electronics{Name: name, Price: price, Model: model, WeightGrams: weight, Fragile: fragile}
}

func (e *Electronics) Accept(visitor ItemVisitor) {
	visitor.VisitElectronics(e)
}
func (e *Electronics) GetPrice() float64 { return e.Price }
func (e *Electronics) GetName() string   { return e.Name }
func (e *Electronics) GetModel() string  { return e.Model }
func (e *Electronics) GetWeight() int    { return e.WeightGrams }
func (e *Electronics) IsFragile() bool   { return e.Fragile }
java 复制代码
// ItemElement.java (Element interface)
package com.example.element;

import com.example.visitor.ItemVisitor;

public interface ItemElement {
    void accept(ItemVisitor visitor);
    double getPrice();
    String getName();
}

// Book.java (ConcreteElement)
package com.example.element;

import com.example.visitor.ItemVisitor;

public class Book implements ItemElement {
    private String name;
    private double price;
    private String isbnNumber;
    private int weightGrams;

    public Book(String name, double price, String isbn, int weight) {
        this.name = name;
        this.price = price;
        this.isbnNumber = isbn;
        this.weightGrams = weight;
    }

    @Override
    public void accept(ItemVisitor visitor) {
        visitor.visitBook(this);
    }

    @Override
    public double getPrice() {
        return price;
    }

    @Override
    public String getName() {
        return name;
    }

    public String getIsbnNumber() {
        return isbnNumber;
    }

    public int getWeightGrams() {
        return weightGrams;
    }
}

// Electronics.java (ConcreteElement)
package com.example.element;

import com.example.visitor.ItemVisitor;

public class Electronics implements ItemElement {
    private String name;
    private double price;
    private String modelNumber;
    private int weightGrams;
    private boolean fragile;

    public Electronics(String name, double price, String model, int weight, boolean fragile) {
        this.name = name;
        this.price = price;
        this.modelNumber = model;
        this.weightGrams = weight;
        this.fragile = fragile;
    }

    @Override
    public void accept(ItemVisitor visitor) {
        visitor.visitElectronics(this);
    }

    @Override
    public double getPrice() {
        return price;
    }

    @Override
    public String getName() {
        return name;
    }

    public String getModelNumber() {
        return modelNumber;
    }

    public int getWeightGrams() {
        return weightGrams;
    }

    public boolean isFragile() {
        return fragile;
    }
}
访问者接口和具体访问者 (ItemVisitor, PriceCalculatorVisitor, ShippingCostVisitor)
go 复制代码
// visitor.go (Visitor interface and ConcreteVisitors)
package visitor

import (
	"../element" // Assuming element package is in the parent directory
	"fmt"
)

// ItemVisitor 访问者接口 (already forward-declared in item.go, actual definition here)
// type ItemVisitor interface {
// 	VisitBook(book *element.Book)
// 	VisitElectronics(electronics *element.Electronics)
// }

// --- PriceCalculatorVisitor --- (具体访问者:计算总价,可能含税)
type PriceCalculatorVisitor struct {
	TotalCost float64
}

func (pc *PriceCalculatorVisitor) VisitBook(book *element.Book) {
	cost := book.GetPrice()
	// Example: Books might have a 5% discount
	cost *= 0.95
	fmt.Printf("Book '%s' (ISBN: %s) - Original Price: $%.2f, Discounted Price: $%.2f\n",
		book.GetName(), book.GetISBN(), book.GetPrice(), cost)
	pc.TotalCost += cost
}

func (pc *PriceCalculatorVisitor) VisitElectronics(e *element.Electronics) {
	cost := e.GetPrice()
	// Example: Electronics might have a 10% tax
	cost *= 1.10
	fmt.Printf("Electronics '%s' (Model: %s) - Original Price: $%.2f, Price with Tax: $%.2f\n",
		e.GetName(), e.GetModel(), e.GetPrice(), cost)
	pc.TotalCost += cost
}

func (pc *PriceCalculatorVisitor) GetTotalCost() float64 {
	return pc.TotalCost
}

// --- ShippingCostVisitor --- (具体访问者:计算运费)
type ShippingCostVisitor struct {
	TotalShippingCost float64
}

func (sc *ShippingCostVisitor) VisitBook(book *element.Book) {
	// Example: Shipping cost for books is $0.05 per 100g
	shipping := float64(book.GetWeight()) / 100.0 * 0.05
	fmt.Printf("Book '%s' - Weight: %dg, Shipping: $%.2f\n", book.GetName(), book.GetWeight(), shipping)
	sc.TotalShippingCost += shipping
}

func (sc *ShippingCostVisitor) VisitElectronics(e *element.Electronics) {
	// Example: Shipping cost for electronics is $0.1 per 100g, +$5 if fragile
	shipping := float64(e.GetWeight()) / 100.0 * 0.10
	if e.IsFragile() {
		shipping += 5.0
		fmt.Printf("Electronics '%s' - Weight: %dg, Fragile, Shipping: $%.2f\n", e.GetName(), e.GetWeight(), shipping)
	} else {
		fmt.Printf("Electronics '%s' - Weight: %dg, Shipping: $%.2f\n", e.GetName(), e.GetWeight(), shipping)
	}
	sc.TotalShippingCost += shipping
}

func (sc *ShippingCostVisitor) GetTotalShippingCost() float64 {
	return sc.TotalShippingCost
}
java 复制代码
// ItemVisitor.java (Visitor interface)
package com.example.visitor;

import com.example.element.Book;
import com.example.element.Electronics;

public interface ItemVisitor {
    void visitBook(Book book);
    void visitElectronics(Electronics electronics);
}

// PriceCalculatorVisitor.java (ConcreteVisitor)
package com.example.visitor;

import com.example.element.Book;
import com.example.element.Electronics;

public class PriceCalculatorVisitor implements ItemVisitor {
    private double totalCost = 0;

    @Override
    public void visitBook(Book book) {
        double cost = book.getPrice();
        // Example: Books might have a 5% discount
        cost *= 0.95;
        System.out.printf("Book '%s' (ISBN: %s) - Original Price: $%.2f, Discounted Price: $%.2f%n",
                book.getName(), book.getIsbnNumber(), book.getPrice(), cost);
        totalCost += cost;
    }

    @Override
    public void visitElectronics(Electronics electronics) {
        double cost = electronics.getPrice();
        // Example: Electronics might have a 10% tax
        cost *= 1.10;
        System.out.printf("Electronics '%s' (Model: %s) - Original Price: $%.2f, Price with Tax: $%.2f%n",
                electronics.getName(), electronics.getModelNumber(), electronics.getPrice(), cost);
        totalCost += cost;
    }

    public double getTotalCost() {
        return totalCost;
    }
}

// ShippingCostVisitor.java (ConcreteVisitor)
package com.example.visitor;

import com.example.element.Book;
import com.example.element.Electronics;

public class ShippingCostVisitor implements ItemVisitor {
    private double totalShippingCost = 0;

    @Override
    public void visitBook(Book book) {
        // Example: Shipping cost for books is $0.05 per 100g
        double shipping = (double) book.getWeightGrams() / 100.0 * 0.05;
        System.out.printf("Book '%s' - Weight: %dg, Shipping: $%.2f%n", book.getName(), book.getWeightGrams(), shipping);
        totalShippingCost += shipping;
    }

    @Override
    public void visitElectronics(Electronics electronics) {
        // Example: Shipping cost for electronics is $0.1 per 100g, +$5 if fragile
        double shipping = (double) electronics.getWeightGrams() / 100.0 * 0.10;
        if (electronics.isFragile()) {
            shipping += 5.0;
            System.out.printf("Electronics '%s' - Weight: %dg, Fragile, Shipping: $%.2f%n",
                    electronics.getName(), electronics.getWeightGrams(), shipping);
        } else {
            System.out.printf("Electronics '%s' - Weight: %dg, Shipping: $%.2f%n",
                    electronics.getName(), electronics.getWeightGrams(), shipping);
        }
        totalShippingCost += shipping;
    }

    public double getTotalShippingCost() {
        return totalShippingCost;
    }
}
对象结构 (ShoppingCart - ObjectStructure)
go 复制代码
// shopping_cart.go (ObjectStructure)
package objectstructure

import (
	"../element"
	"../visitor"
)

// ShoppingCart 对象结构
type ShoppingCart struct {
	items []element.ItemElement
}

func NewShoppingCart() *ShoppingCart {
	return &ShoppingCart{items: make([]element.ItemElement, 0)}
}

func (sc *ShoppingCart) AddItem(item element.ItemElement) {
	sc.items = append(sc.items, item)
}

// AcceptAll 允许访问者访问所有元素
func (sc *ShoppingCart) AcceptAll(visitor visitor.ItemVisitor) { // Go's ItemVisitor is in element package due to cyclic dependency avoidance
	for _, item := range sc.items {
		item.Accept(visitor)
	}
}

// Go 的 ItemVisitor 接口在 element 包中定义,以避免 visitor 包和 element 包之间的循环依赖。
// 在 visitor 包中,我们使用 element.ItemVisitor。
// 在 element 包中,我们使用 type ItemVisitor interface { ... } 来引用它。
// 更好的做法可能是将 ItemVisitor 接口放在一个独立的、两者都依赖的包中,或者都放在一个大包里。
// For simplicity here, we assume element.ItemVisitor is the one defined in element/item.go
// and visitor.ItemVisitor is the same interface used by concrete visitors.
// To make this compile, the ItemVisitor in shopping_cart.go's AcceptAll method
// should match the type expected by item.Accept().
// Let's assume the ItemVisitor in visitor.go is the one being passed around.
// So, the AcceptAll method signature should be:
// func (sc *ShoppingCart) AcceptAll(visitor element.ItemVisitor) {
// ...
// }
// And concrete visitors in visitor.go should implement element.ItemVisitor.
// This is a common structural challenge in Go with Visitor pattern due to package organization.
// For this example, let's assume the ItemVisitor type is consistently used.
java 复制代码
// ShoppingCart.java (ObjectStructure)
package com.example.objectstructure;

import com.example.element.ItemElement;
import com.example.visitor.ItemVisitor;
import java.util.ArrayList;
import java.util.List;

public class ShoppingCart {
    private List<ItemElement> items;

    public ShoppingCart() {
        this.items = new ArrayList<>();
    }

    public void addItem(ItemElement item) {
        this.items.add(item);
    }

    public void removeItem(ItemElement item) {
        this.items.remove(item);
    }

    // Allow visitor to visit all items
    public void acceptAll(ItemVisitor visitor) {
        for (ItemElement item : items) {
            item.accept(visitor);
        }
    }
}
客户端使用
go 复制代码
// main.go (示例用法)
/*
package main

import (
	"./element"
	"./objectstructure"
	"./visitor"
	"fmt"
)

func main() {
	cart := objectstructure.NewShoppingCart()
	cart.AddItem(element.NewBook("Design Patterns", 55.00, "978-0201633610", 700))
	cart.AddItem(element.NewElectronics("Laptop Pro", 1200.00, "LPX15", 2200, false))
	cart.AddItem(element.NewBook("Clean Code", 45.00, "978-0132350884", 500))
	cart.AddItem(element.NewElectronics("Wireless Mouse", 25.00, "WM-101", 150, false))
	cart.AddItem(element.NewElectronics("Glass Monitor", 300.00, "GM-27", 5500, true))

	fmt.Println("--- Calculating Total Price with Discounts/Taxes ---")
	priceCalcVisitor := &visitor.PriceCalculatorVisitor{}
	cart.AcceptAll(priceCalcVisitor) // Pass the concrete visitor
	fmt.Printf("==> Grand Total Price: $%.2f\n", priceCalcVisitor.GetTotalCost())

	fmt.Println("\n--- Calculating Total Shipping Cost ---")
	shippingCalcVisitor := &visitor.ShippingCostVisitor{}
	cart.AcceptAll(shippingCalcVisitor)
	fmt.Printf("==> Grand Total Shipping Cost: $%.2f\n", shippingCalcVisitor.GetTotalShippingCost())

	// Example of combined total
	grandTotal := priceCalcVisitor.GetTotalCost() + shippingCalcVisitor.GetTotalShippingCost()
	fmt.Printf("\n==> Overall Total (Price + Shipping): $%.2f\n", grandTotal)
}
*/
java 复制代码
// Main.java (示例用法)
/*
package com.example;

import com.example.element.Book;
import com.example.element.Electronics;
import com.example.objectstructure.ShoppingCart;
import com.example.visitor.PriceCalculatorVisitor;
import com.example.visitor.ShippingCostVisitor;

public class Main {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();
        cart.addItem(new Book("Design Patterns", 55.00, "978-0201633610", 700));
        cart.addItem(new Electronics("Laptop Pro", 1200.00, "LPX15", 2200, false));
        cart.addItem(new Book("Clean Code", 45.00, "978-0132350884", 500));
        cart.addItem(new Electronics("Wireless Mouse", 25.00, "WM-101", 150, false));
        cart.addItem(new Electronics("Glass Monitor", 300.00, "GM-27", 5500, true));

        System.out.println("--- Calculating Total Price with Discounts/Taxes ---");
        PriceCalculatorVisitor priceCalcVisitor = new PriceCalculatorVisitor();
        cart.acceptAll(priceCalcVisitor);
        System.out.printf("==> Grand Total Price: $%.2f%n", priceCalcVisitor.getTotalCost());

        System.out.println("\n--- Calculating Total Shipping Cost ---");
        ShippingCostVisitor shippingCalcVisitor = new ShippingCostVisitor();
        cart.acceptAll(shippingCalcVisitor);
        System.out.printf("==> Grand Total Shipping Cost: $%.2f%n", shippingCalcVisitor.getTotalShippingCost());

        // Example of combined total
        double grandTotal = priceCalcVisitor.getTotalCost() + shippingCalcVisitor.getTotalShippingCost();
        System.out.printf("\n==> Overall Total (Price + Shipping): $%.2f%n", grandTotal);
    }
}
*/

7. 总结

访问者模式是一种强大的行为模式,它允许你在不改变组成对象结构的元素类的前提下,向这些元素添加新的操作。它通过将操作逻辑封装在访问者对象中,并利用双重分派机制来实现这一点。虽然它在增加新元素类型方面不够灵活,并且可能一定程度上破坏元素的封装性,但在对象结构相对稳定而操作需求经常变化的场景下,访问者模式提供了一个优雅的解决方案,能够有效地分离数据结构和作用于其上的算法,保持代码的清晰和可扩展性。

相关推荐
code bean9 分钟前
【C#】 C#中 nameof 和 ToString () 的用法与区别详解
android·java·c#
圆仔00712 分钟前
【Java生成指定背景图片的PDF文件】
java
小猫咪怎么会有坏心思呢28 分钟前
华为OD机考-分班问题/幼儿园分班-字符串(JAVA 2025B卷)
java·开发语言·华为od
charlie11451419129 分钟前
从C++编程入手设计模式——外观模式
c++·设计模式·外观模式
在未来等你1 小时前
设计模式精讲 Day 4:建造者模式(Builder Pattern)
java·: design-patterns·builder-pattern·software-design·object-oriented-programming
今天我要乾重生1 小时前
java基础学习(三十)
java·开发语言·学习
JWASX3 小时前
【RocketMQ 生产者和消费者】- 消费者重平衡(1)
java·rocketmq·重平衡
剽悍一小兔3 小时前
自动化文档生成工具(亲测可运行)
java
程序员皮皮林3 小时前
使用 Java + WebSocket 实现简单实时双人协同 pk 答题
java·websocket
栗然3 小时前
Spring Boot 项目中使用 MyBatis 的 @SelectProvider 注解并解决 SQL 注入的问题
java·后端