SpringBoot 集成 RabbitMQ 实战(消息队列):实现异步通信与系统解耦

在复杂后端系统中,同步通信往往会导致系统耦合度高、响应延迟、容错性差 ------ 如用户下单后,需同步完成库存扣减、订单通知、日志记录等操作,任一环节故障都会导致整个流程失败。RabbitMQ 作为开源消息队列中间件,基于 AMQP 协议,支持多种交换机模式、消息持久化、死信队列等功能,可实现系统间异步通信、业务解耦、流量削峰,提升系统容错性与可扩展性。

本文聚焦 SpringBoot 与 RabbitMQ 的实战落地,从环境搭建、交换机模式、消息收发、可靠性保障,到死信队列、延迟队列实现,全程嵌入代码教学,帮你快速掌握消息队列核心应用,打造高可用、低耦合的分布式系统。

一、核心认知:RabbitMQ 核心价值与适用场景

1. 核心优势

  • 系统解耦:生产者与消费者通过消息队列通信,无需直接依赖,降低系统耦合度,便于独立迭代;
  • 异步通信:生产者发送消息后立即返回,无需等待消费者处理,提升系统响应速度;
  • 流量削峰:高并发场景下,消息队列缓冲请求,消费者按能力消费,避免下游服务被压垮;
  • 容错性强:支持消息持久化、重试机制、死信队列,确保消息不丢失、不重复消费;
  • 功能丰富:支持 Direct、Fanout、Topic、Headers 四种交换机模式,适配复杂业务场景。

2. 核心适用场景

  • 异步任务处理:订单创建后异步发送通知、日志收集、数据统计分析;
  • 系统间通信:微服务架构下,服务间通过消息队列异步通信(替代同步 HTTP 调用);
  • 流量削峰填谷:秒杀、促销活动中,缓冲大量下单请求,避免库存服务过载;
  • 可靠消息传递:重要业务消息(如转账、订单支付)需确保必达,支持重试与死信处理。

3. RabbitMQ 核心概念

  • 生产者(Producer):发送消息的服务 / 应用;
  • 消费者(Consumer):接收并处理消息的服务 / 应用;
  • 交换机(Exchange):接收生产者消息,按规则路由到队列,不存储消息;
  • 队列(Queue):存储消息,供消费者消费,支持持久化;
  • 绑定(Binding):将交换机与队列关联,指定路由规则;
  • 路由键(Routing Key):生产者发送消息时指定,交换机根据路由键路由消息。

4. 四种交换机模式

  • Direct:精准匹配,消息路由键与绑定键完全一致时,路由到对应队列;
  • Fanout:广播模式,忽略路由键,将消息路由到所有绑定的队列;
  • Topic:模糊匹配,支持通配符(* 匹配一个单词,# 匹配多个单词),路由到符合规则的队列;
  • Headers:基于消息头信息路由,而非路由键,使用场景较少。

二、核心实战一:环境搭建(Docker 快速部署)

1. Docker 部署 RabbitMQ(带管理界面)

bash

运行

复制代码
# 1. 拉取 RabbitMQ 镜像(带管理界面版本)
docker pull rabbitmq:3-management

# 2. 启动 RabbitMQ 容器(配置账号密码、端口映射)
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 \
  -v rabbitmq-data:/var/lib/rabbitmq \ # 挂载数据卷,持久化数据
  -e RABBITMQ_DEFAULT_USER=admin \ # 管理员账号
  -e RABBITMQ_DEFAULT_PASS=admin123 \ # 管理员密码
  rabbitmq:3-management
  • 管理界面访问:http://localhost:15672(账号 / 密码:admin/admin123),可可视化管理交换机、队列、消息;
  • 核心端口:5672(生产者 / 消费者通信端口),15672(管理界面端口)。

2. 控制台初始化(创建交换机与队列,可选)

可通过管理界面手动创建交换机、队列并绑定,也可通过代码自动创建(推荐代码创建,便于版本控制)。

三、核心实战二:SpringBoot 集成 RabbitMQ 基础配置

1. 引入依赖(Maven)

xml

复制代码
<!-- Spring AMQP 依赖(简化 RabbitMQ 操作) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- Web 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

2. 配置文件(application.yml)

yaml

复制代码
# RabbitMQ 配置
spring:
  rabbitmq:
    host: localhost # 服务地址
    port: 5672 # 通信端口
    username: admin # 账号
    password: admin123 # 密码
    virtual-host: / # 虚拟主机(默认 /,可隔离不同业务)
    publisher-confirm-type: correlated # 开启生产者确认机制(确保消息发送到交换机)
    publisher-returns: true # 开启生产者回退机制(消息无法路由时回调)
    listener:
      simple:
        acknowledge-mode: manual # 开启手动确认模式(确保消费者处理完成后再确认消息)
        concurrency: 1 # 最小消费者数量
        max-concurrency: 5 # 最大消费者数量
        prefetch: 1 # 每次只获取一条消息,处理完成后再获取下一条(避免消息堆积)

# 服务端口
server:
  port: 8086

3. 队列与交换机配置类(代码创建,推荐)

通过配置类创建交换机、队列并绑定,支持四种交换机模式,适配不同业务场景。

java

运行

复制代码
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {
    // 🌟 Direct 交换机配置(精准匹配)
    public static final String DIRECT_EXCHANGE = "direct_exchange";
    public static final String DIRECT_QUEUE = "direct_queue";
    public static final String DIRECT_ROUTING_KEY = "direct.routing.key";

    // 🌟 Fanout 交换机配置(广播模式)
    public static final String FANOUT_EXCHANGE = "fanout_exchange";
    public static final String FANOUT_QUEUE1 = "fanout_queue1";
    public static final String FANOUT_QUEUE2 = "fanout_queue2";

    // 🌟 Topic 交换机配置(模糊匹配)
    public static final String TOPIC_EXCHANGE = "topic_exchange";
    public static final String TOPIC_QUEUE1 = "topic_queue1"; // 匹配 user.*
    public static final String TOPIC_QUEUE2 = "topic_queue2"; // 匹配 user.#
    public static final String TOPIC_ROUTING_KEY1 = "user.info";
    public static final String TOPIC_ROUTING_KEY2 = "user.order.pay";

    // 1. Direct 交换机、队列、绑定
    @Bean
    public DirectExchange directExchange() {
        // durable = true:持久化交换机(重启 RabbitMQ 不丢失)
        return new DirectExchange(DIRECT_EXCHANGE, true, false);
    }

    @Bean
    public Queue directQueue() {
        // durable = true:持久化队列;exclusive = false:非独占;autoDelete = false:不自动删除
        return QueueBuilder.durable(DIRECT_QUEUE).build();
    }

    @Bean
    public Binding directBinding() {
        // 将 Direct 队列绑定到 Direct 交换机,指定路由键
        return BindingBuilder.bind(directQueue()).to(directExchange()).with(DIRECT_ROUTING_KEY);
    }

    // 2. Fanout 交换机、队列、绑定
    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange(FANOUT_EXCHANGE, true, false);
    }

    @Bean
    public Queue fanoutQueue1() {
        return QueueBuilder.durable(FANOUT_QUEUE1).build();
    }

    @Bean
    public Queue fanoutQueue2() {
        return QueueBuilder.durable(FANOUT_QUEUE2).build();
    }

    @Bean
    public Binding fanoutBinding1() {
        // Fanout 模式无需路由键,直接绑定
        return BindingBuilder.bind(fanoutQueue1()).to(fanoutExchange());
    }

    @Bean
    public Binding fanoutBinding2() {
        return BindingBuilder.bind(fanoutQueue2()).to(fanoutExchange());
    }

    // 3. Topic 交换机、队列、绑定
    @Bean
    public TopicExchange topicExchange() {
        return new TopicExchange(TOPIC_EXCHANGE, true, false);
    }

    @Bean
    public Queue topicQueue1() {
        return QueueBuilder.durable(TOPIC_QUEUE1).build();
    }

    @Bean
    public Queue topicQueue2() {
        return QueueBuilder.durable(TOPIC_QUEUE2).build();
    }

    @Bean
    public Binding topicBinding1() {
        // 绑定键为 user.*,匹配 user.info、user.order 等(一个单词后缀)
        return BindingBuilder.bind(topicQueue1()).to(topicExchange()).with("user.*");
    }

    @Bean
    public Binding topicBinding2() {
        // 绑定键为 user.#,匹配 user.info、user.order.pay 等(多个单词后缀)
        return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("user.#");
    }
}

四、核心实战三:消息收发实现(三种交换机模式)

1. 生产者(发送消息)

封装消息发送工具类,支持不同交换机模式发送消息,同时处理生产者确认与回退回调。

(1)生产者回调配置(确保消息可靠发送)

java

运行

复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;

@Slf4j
@Component
public class RabbitMQProducerCallback implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
    @Resource
    private RabbitTemplate rabbitTemplate;

    // 初始化回调
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }

    // 生产者确认回调(消息是否发送到交换机)
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            log.info("消息发送到交换机成功,消息ID:{}", correlationData.getId());
        } else {
            log.error("消息发送到交换机失败,消息ID:{},原因:{}", correlationData.getId(), cause);
        }
    }

    // 消息回退回调(消息无法路由到队列时触发)
    @Override
    public void returnedMessage(ReturnedMessage returned) {
        log.error("消息无法路由,交换机:{},路由键:{},消息内容:{}",
                returned.getExchange(), returned.getRoutingKey(), new String(returned.getMessage().getBody()));
    }
}
(2)消息发送工具类

java

运行

复制代码
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.UUID;

@Component
public class RabbitMQProducer {
    @Resource
    private RabbitTemplate rabbitTemplate;
    @Resource
    private RabbitMQProducerCallback producerCallback;

    // 初始化:设置消息转换器(JSON 格式)、回调函数
    @PostConstruct
    public void init() {
        rabbitTemplate.setMessageConverter(messageConverter());
        producerCallback.init();
    }

    // 消息转换器(JSON 序列化,支持复杂对象)
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    // 发送消息(通用方法)
    public void sendMessage(String exchange, String routingKey, Object message) {
        // 生成唯一消息ID(用于确认与追踪)
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        rabbitTemplate.convertAndSend(exchange, routingKey, message, correlationData);
    }
}
(3)Controller 层接口(触发消息发送)

java

运行

复制代码
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.rabbitmq.config.RabbitMQConfig;
import com.example.rabbitmq.entity.Order;
import com.example.rabbitmq.producer.RabbitMQProducer;
import com.example.rabbitmq.result.Result;
import javax.annotation.Resource;
import java.time.LocalDateTime;

@RestController
@RequestMapping("/message")
public class MessageController {
    @Resource
    private RabbitMQProducer rabbitProducer;

    // ✅ 发送 Direct 消息(精准匹配)
    @GetMapping("/direct/send")
    public Result<Void> sendDirectMessage(@RequestParam String content) {
        Order order = new Order(1L, "DIRECT_ORDER_001", content, LocalDateTime.now());
        rabbitProducer.sendMessage(RabbitMQConfig.DIRECT_EXCHANGE, RabbitMQConfig.DIRECT_ROUTING_KEY, order);
        return Result.success();
    }

    // ✅ 发送 Fanout 消息(广播)
    @GetMapping("/fanout/send")
    public Result<Void> sendFanoutMessage(@RequestParam String content) {
        Order order = new Order(2L, "FANOUT_ORDER_001", content, LocalDateTime.now());
        // Fanout 模式路由键无效,可设为 null
        rabbitProducer.sendMessage(RabbitMQConfig.FANOUT_EXCHANGE, "", order);
        return Result.success();
    }

    // ✅ 发送 Topic 消息(模糊匹配)
    @GetMapping("/topic/send")
    public Result<Void> sendTopicMessage(@RequestParam String routingKey, @RequestParam String content) {
        Order order = new Order(3L, "TOPIC_ORDER_001", content, LocalDateTime.now());
        rabbitProducer.sendMessage(RabbitMQConfig.TOPIC_EXCHANGE, routingKey, order);
        return Result.success();
    }
}

2. 消费者(接收并处理消息)

开启手动确认模式,确保消息处理完成后再确认,避免消息丢失或重复消费。

(1)Direct 队列消费者

java

运行

复制代码
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import com.example.rabbitmq.config.RabbitMQConfig;
import com.example.rabbitmq.entity.Order;
import java.io.IOException;

@Slf4j
@Component
public class RabbitMQConsumer {
    // 消费 Direct 队列消息
    @RabbitListener(queues = RabbitMQConfig.DIRECT_QUEUE)
    public void consumeDirectMessage(Order order, Channel channel, Message message) throws IOException {
        try {
            // 处理消息业务逻辑(如订单通知、库存扣减)
            log.info("消费 Direct 队列消息,订单信息:{}", order);
            // 手动确认消息(multiple = false:仅确认当前消息)
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            log.error("处理 Direct 消息失败", e);
            // 消息处理失败,拒绝消息并重回队列(requeue = true:重回队列,可重试)
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        }
    }

    // 消费 Fanout 队列1消息
    @RabbitListener(queues = RabbitMQConfig.FANOUT_QUEUE1)
    public void consumeFanoutMessage1(Order order, Channel channel, Message message) throws IOException {
        try {
            log.info("消费 Fanout 队列1消息,订单信息:{}", order);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            log.error("处理 Fanout 队列1消息失败", e);
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        }
    }

    // 消费 Fanout 队列2消息
    @RabbitListener(queues = RabbitMQConfig.FANOUT_QUEUE2)
    public void consumeFanoutMessage2(Order order, Channel channel, Message message) throws IOException {
        try {
            log.info("消费 Fanout 队列2消息,订单信息:{}", order);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            log.error("处理 Fanout 队列2消息失败", e);
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        }
    }

    // 消费 Topic 队列1消息(匹配 user.*)
    @RabbitListener(queues = RabbitMQConfig.TOPIC_QUEUE1)
    public void consumeTopicMessage1(Order order, Channel channel, Message message) throws IOException {
        try {
            log.info("消费 Topic 队列1消息,订单信息:{}", order);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            log.error("处理 Topic 队列1消息失败", e);
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        }
    }

    // 消费 Topic 队列2消息(匹配 user.#)
    @RabbitListener(queues = RabbitMQConfig.TOPIC_QUEUE2)
    public void consumeTopicMessage2(Order order, Channel channel, Message message) throws IOException {
        try {
            log.info("消费 Topic 队列2消息,订单信息:{}", order);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            log.error("处理 Topic 队列2消息失败", e);
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        }
    }
}

五、核心实战四:进阶功能(死信队列 + 延迟队列)

1. 死信队列(处理无法消费的消息)

死信队列用于存储无法正常消费的消息(如多次重试失败、消息过期),避免消息丢失,便于后续排查与处理。

(1)死信队列配置

java

运行

复制代码
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DeadLetterQueueConfig {
    // 死信交换机、队列
    public static final String DEAD_LETTER_EXCHANGE = "dead_letter_exchange";
    public static final String DEAD_LETTER_QUEUE = "dead_letter_queue";
    public static final String DEAD_LETTER_ROUTING_KEY = "dead.letter.routing.key";

    // 普通队列(绑定死信交换机)
    public static final String NORMAL_QUEUE = "normal_queue";

    // 1. 死信交换机、队列、绑定
    @Bean
    public DirectExchange deadLetterExchange() {
        return new DirectExchange(DEAD_LETTER_EXCHANGE, true, false);
    }

    @Bean
    public Queue deadLetterQueue() {
        return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
    }

    @Bean
    public Binding deadLetterBinding() {
        return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange()).with(DEAD_LETTER_ROUTING_KEY);
    }

    // 2. 普通队列(配置死信参数)
    @Bean
    public Queue normalQueue() {
        return QueueBuilder.durable(NORMAL_QUEUE)
                .deadLetterExchange(DEAD_LETTER_EXCHANGE) // 绑定死信交换机
                .deadLetterRoutingKey(DEAD_LETTER_ROUTING_KEY) // 死信路由键
                .ttl(60000) // 消息过期时间(60秒,过期后进入死信队列)
                .maxLength(10) // 队列最大长度(超过后新消息进入死信队列)
                .build();
    }

    // 3. 普通队列绑定到原 Direct 交换机
    @Bean
    public Binding normalBinding() {
        return BindingBuilder.bind(normalQueue()).to(RabbitMQConfig.DIRECT_EXCHANGE).with("normal.routing.key");
    }
}
(2)死信队列消费者

java

运行

复制代码
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import com.example.rabbitmq.config.DeadLetterQueueConfig;
import com.example.rabbitmq.entity.Order;
import java.io.IOException;

@Slf4j
@Component
public class DeadLetterConsumer {
    @RabbitListener(queues = DeadLetterQueueConfig.DEAD_LETTER_QUEUE)
    public void consumeDeadLetterMessage(Order order, Channel channel, Message message) throws IOException {
        log.error("消费死信队列消息,订单信息:{},消息ID:{}", order, message.getMessageProperties().getMessageId());
        // 死信消息处理(如记录日志、人工排查)
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}

2. 延迟队列(实现定时任务)

RabbitMQ 本身不支持延迟队列,但可通过「普通队列 + 死信队列 + 消息过期时间」实现延迟功能,适用于定时通知、订单超时取消等场景。

(1)延迟队列使用示例(订单超时取消)

java

运行

复制代码
// 生产者发送延迟消息(设置消息过期时间)
@GetMapping("/delay/send")
public Result<Void> sendDelayMessage(@RequestParam Long orderId, @RequestParam Integer delaySeconds) {
    Order order = new Order(orderId, "DELAY_ORDER_" + orderId, "延迟订单", LocalDateTime.now());
    // 发送消息到普通队列,设置消息过期时间(毫秒)
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    rabbitTemplate.convertAndSend(
            RabbitMQConfig.DIRECT_EXCHANGE,
            "normal.routing.key",
            order,
            msg -> {
                msg.getMessageProperties().setExpiration(String.valueOf(delaySeconds * 1000));
                return msg;
            },
            correlationData
    );
    log.info("发送延迟消息,订单ID:{},延迟时间:{}秒", orderId, delaySeconds);
    return Result.success();
}
  • 实现逻辑:消息发送到普通队列,过期后进入死信队列,消费者监听死信队列,触发定时任务(如订单超时取消)。

六、避坑指南

坑点 1:消息丢失

表现:生产者发送消息后,RabbitMQ 宕机或消费者未处理,消息丢失;✅ 解决方案:开启消息持久化(队列、交换机、消息均设置 durable = true),开启生产者确认机制与消费者手动确认模式。

坑点 2:消息重复消费

表现:消费者重复处理同一消息,导致业务逻辑异常(如重复扣减库存);✅ 解决方案:消息幂等性处理(如基于消息 ID 去重、数据库唯一约束),避免重复消费。

坑点 3:消息堆积

表现:消费者处理速度慢,消息在队列中大量堆积;✅ 解决方案:增加消费者数量(调整 concurrency 参数),优化消费者处理逻辑,设置队列最大长度与死信机制。

坑点 4:生产者确认回调不生效

表现:发送消息后,确认回调未触发,无法确认消息是否发送成功;✅ 解决方案:配置文件中设置 publisher-confirm-type: correlated,并初始化回调函数(绑定到 RabbitTemplate)。

坑点 5:手动确认消息时抛异常

表现:消费者处理消息后,手动确认时抛出 IOException;✅ 解决方案:确保手动确认的 DeliveryTag 正确,避免重复确认;处理消息时捕获异常,避免异常导致确认失败。

七、终极总结:RabbitMQ 实战的核心是「可靠通信 + 解耦高效」

RabbitMQ 实战的核心价值,是通过异步通信实现系统解耦,通过丰富的可靠性机制确保消息不丢失、不重复消费,同时应对高并发场景下的流量削峰。企业级开发中,需根据业务场景选择合适的交换机模式,配置完善的可靠性保障,平衡系统性能与数据一致性。

核心原则总结:

  1. 可靠性配置必开:生产环境必须开启消息持久化、生产者确认、消费者手动确认,避免消息丢失;
  2. 交换机模式适配业务:精准匹配用 Direct,广播用 Fanout,模糊匹配用 Topic;
  3. 异常消息妥善处理:通过死信队列存储无法消费的消息,便于排查与补救;
  4. 幂等性不可忽视:消费者必须实现消息幂等性,避免重复消费导致业务异常。
相关推荐
超级数据查看器2 小时前
超级数据查看器 更新日志(包含的功能)
android·java·数据库·sqlite·安卓
青衫码上行2 小时前
如何构建maven项目
java·学习·maven
不穿格子的程序员2 小时前
JVM篇2:根可达性算法-垃圾回收算法和三色标记算法-CMS和G1
java·jvm·g1·根可达性算法·三色标记算法
凌冰_2 小时前
Thymeleaf Maven+Servlet+Mysql图书框架—2(八)
java·mysql·maven
indexsunny2 小时前
互联网大厂Java面试实战:Spring Boot与微服务在电商场景中的应用解析
java·数据库·spring boot·微服务·maven·flyway·电商
sunnyday04262 小时前
从混乱到清晰:Maven 依赖版本管理最佳实践
java·spring boot·后端·maven
BD_Marathon2 小时前
动态SQL(四) choose、when、otherwise
mybatis
roman_日积跬步-终至千里2 小时前
【大数据框架】Calcite 基础概念:从 SQL 到执行计划的思维路径
java·大数据·sql
cypking2 小时前
后端框架搭建完全指南
java