前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >实战:SpringBoot集成rabbitmq并实现延时队列

实战:SpringBoot集成rabbitmq并实现延时队列

作者头像
JAVA葵花宝典
发布于 2020-07-09 07:39:46
发布于 2020-07-09 07:39:46
1K00
代码可运行
举报
文章被收录于专栏:JAVA葵花宝典JAVA葵花宝典
运行总次数:0
代码可运行

前言

消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题。

实现高性能,高可用,可伸缩和最终一致性架构。「RabbitMQ」是实现了高级消息队列协议(AMQP)的开源消息,具有较高的系统吞吐量、可靠性、消息持久化、免费等优点,在软件项目中具有非常广泛的应用。

项目介绍

本项目以springboot集成rabbitmq,引导如何设计和优雅地集成rabbitmq相关的组件,并实现用死信队列实现延迟消息队列。

ps:本文实战代码都已发布到Github,文末有链接也可以点击阅读原文

项目设计与实战

配置

maven依赖

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
        <relativePath/> 
 </parent>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

配置文件

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
spring.rabbitmq.host=192.168.202.128
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

组件设计与实现

Exchange(交换机)

定义交换机名称、类型、持久化、延时交换机名称等属性。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface IRabbitMqExchange {

    /**
     * Exchange(交换机) 的名称
     * */
    String exchangeName();

    /**
     * exchange类型 DIRECT("direct"), FANOUT("fanout"), TOPIC("topic"), HEADERS("headers")
     * */
    default String type(){return "topic";}

    /**
     * 是否持久化
     */
    default boolean durable(){return true;}

    /**
     * 当所有队列在完成使用此exchange时,是否删除
     */
    default boolean autoDelete(){return false;}

    /**
     * 是否允许直接binding
     * 如果是true的话 则不允许直接binding到此 exchange
     */
    default boolean internal(){ return false;}

    /**
     * 其他的一些参数设置
     */
    default Map<String, Object> arguments(){ return null; }

    /**
     * 延时 Exchange
     * */
    default String delayExchangeName() {return "delay."+exchangeName();}

}
路由(Routing)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface IRabbitMqRouting {
    /**
     * rabbitmq路由key
     * */
    String routingKey();

}
队列(Queue)

定义队列名称、持久化、延时队列名称等属性

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface IRabbitMqQueue {
    /**
     * Queue(队列)名称
     */
    String queueName();

    /**
     * 是否持久化
     * */
    default boolean durable() {return true;}

    /**
     * 排他性
     * */
    default boolean exclusive(){return false;}

    /**
     * 是否自动删除
     * */
    default boolean autoDelete(){return false;}

    /**
     * 其他属性设置
     * */
    default Map<String, Object> arguments() { return null; }

    /**
     * 默认的延时队列名称
     * */
    default String delayQueueName(){return "delay."+this.queueName();}

}
绑定关系(Binding)

定义了 交换机(Exchange)-路由(Routing)-消息队列(Queue)的绑定关系,以及定义是否支持延时消息。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface IRabbitMqBinding {
    /**
     * 需要绑定的exchange(交换机)
     * */
    IRabbitMqExchange exchange();

    /**
     * 需要绑定的routing(路由)
     * */
    IRabbitMqRouting routing();

    /**
     * 需要绑定的queue(队列)
     * */
    IRabbitMqQueue queue();

    /**
     * 消息队列是否允许延时
     * */
    boolean allowDelay();
}
默认注册器

实现了交换机、消息队列、绑定关系的注册。如果绑定关系中定义支持延迟消息,则额外注册一个延时交换机和死信队列,以实现延时消息推送的功能。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class DefaultRabbitMqRegister implements IRabbitMqRegister, SmartLifecycle {

    ConnectionFactory connectionFactory;

    Channel channel;

    public DefaultRabbitMqRegister() {
    }

    public DefaultRabbitMqRegister(ConnectionFactory connectionFactory) {
        this.connectionFactory = connectionFactory;
    }

    @PostConstruct
    public void init() {
        channel = connectionFactory.createConnection().createChannel(false);
    }

    @Override
    public void registerExchange(IRabbitMqExchange... exchanges) throws IOException {
        for (IRabbitMqExchange exchange : exchanges) {
            channel.exchangeDeclare(exchange.exchangeName(), exchange.type(), exchange.durable(), exchange.autoDelete(), exchange.internal(), exchange.arguments());
        }
    }

    @Override
    public void registerQueue(IRabbitMqQueue... queues) throws IOException {
        for (IRabbitMqQueue queue : queues) {
            channel.queueDeclare(queue.queueName(), queue.durable(), queue.exclusive(), queue.autoDelete(), queue.arguments());
        }
    }

    @Override
    public void registerBinding(IRabbitMqBinding... bindings) throws IOException {
        for (IRabbitMqBinding binding : bindings) {
            channel.queueBind(binding.queue().queueName(), binding.exchange().exchangeName(), binding.routing().routingKey());
            if (binding.allowDelay()) {
                registerDelayBinding(binding);
            }
        }
    }

    /**
     * 创建一个内部的 死信队列 用来实现 延时队列
     */
    private void registerDelayBinding(IRabbitMqBinding binding) throws IOException {
        IRabbitMqExchange exchange = binding.exchange();
        // 注册一个延时的消息交换机
        channel.exchangeDeclare(exchange.delayExchangeName(), exchange.type(), exchange.durable(), exchange.autoDelete(), exchange.internal(), exchange.arguments());
        // 注册一个死信队列  设置消息超时后,将消息转发到原来的Router队列
        IRabbitMqQueue queue = binding.queue();
        Map<String, Object> arguments = queue.arguments();
        if (arguments == null) {
            arguments = new HashMap<>(4);
        }
        arguments.put("x-dead-letter-exchange", binding.exchange().exchangeName());
        arguments.put("x-dead-letter-routing-key", binding.routing().routingKey());
        channel.queueDeclare(queue.delayQueueName(), queue.durable(), queue.exclusive(), queue.autoDelete(), arguments);
        // 将交换机和队列绑定
        channel.queueBind(queue.delayQueueName(), exchange.delayExchangeName(), binding.routing().routingKey());
    }

    private List<MessageListenerContainer> listenerContainers = new LinkedList<>();

    @Override
    public void listenerQueue(IRabbitMqListener listener, IRabbitMqQueue... queues) {
        String[] queueNames = new String[queues.length];
        for (int idx = 0; idx < queues.length; idx++) {
            queueNames[idx] = queues[idx].queueName();
        }
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
        // 配置手动确认
        container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        container.setQueueNames(queueNames);
        container.setMessageListener(listener);
        listenerContainers.add(container);
    }

    @Override
    public void start() {
        for (MessageListenerContainer container : listenerContainers) {
            container.start();
        }
    }

    @Override
    public void stop() {
    }

    @Override
    public boolean isRunning() {
        return false;
    }

    @Override
    public boolean isAutoStartup() {
        return true;
    }

    @Override
    public void stop(Runnable runnable) {
    }

    @Override
    public int getPhase() {
        return 9999;
    }
}
消息监听器
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface IRabbitMqListener {
    /**
     * 处理rabbitMq的消息
     * */
    boolean handleMessage(Object obj);

}

抽象实现类(具体的消费者继承该抽象类,重写handleMessage()方法,实现消费逻辑)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public abstract class AbstractMessageListener implements ChannelAwareMessageListener, IRabbitMqListener {

    private Logger logger = LoggerFactory.getLogger(AbstractMessageListener.class);

    private MessageConverter messageConverter = new Jackson2JsonMessageConverter();

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long tag = message.getMessageProperties().getDeliveryTag();
        try {
            Object obj = messageConverter.fromMessage(message);
            boolean handleResult = handleMessage(obj);
            if (handleResult) {
                channel.basicAck(tag, false);
            } else {
                logger.error("消息处理失败 message: {}", message);
                channel.basicNack(tag, false, false);
            }
        } catch (Exception e) {
            channel.basicNack(tag, false, false);
            logger.error("消息处理异常 message: " + message + " " + e.getMessage(), e);
        }
    }

}
消息发送服务类

实现发送消息、发送延时消息等功能

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class RabbitMqServiceImpl implements IRabbitMqService, RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    private Logger logger = LoggerFactory.getLogger(RabbitMqServiceImpl.class);

    @Autowired
    protected RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
    }

    @Override
    public void send(IRabbitMqExchange exchange, IRabbitMqRouting routing, Object msg) {
        CorrelationData correlationId = new CorrelationData(UUID.randomUUID().toString());
        rabbitTemplate.convertAndSend(exchange.exchangeName(), routing.routingKey(), msg, correlationId);
    }

    @Override
    public void send(IRabbitMqExchange exchange, IRabbitMqRouting routing, Object msg, long delay) {
        CorrelationData correlationId = new CorrelationData(UUID.randomUUID().toString());
        if (delay > 0) {
            MessagePostProcessor processor = (Message message) -> {
                message.getMessageProperties().setExpiration(delay + "");
                return message;
            };
            rabbitTemplate.convertAndSend(exchange.delayExchangeName(), routing.routingKey(), msg, processor, correlationId);
        } else {
            rabbitTemplate.convertAndSend(exchange.exchangeName(), routing.routingKey(), msg, correlationId);
        }
    }
    /**
     * 消息发送的回调
     *
     * @param correlationId 消息Id
     * @param ack           是否成功的标示
     * @param cause         错误原因
     */
    @Override
    public void confirm(CorrelationData correlationId, boolean ack, String cause) {
        if (ack) {
            logger.info("消息发送成功 correlationId: {} cause: {}", correlationId, cause);
        } else {
            logger.error("消息发送失败 correlationId: {} cause: {}", correlationId, cause);
        }
    }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        logger.info("returnedMessage message: {} replyCode: {} exchange: {} routingKey: {}", message, replyCode, exchange, routingKey);
    }

}

实战

使用枚举定义消息队列配置

定义测试Exchange:mq.exchange.test

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * RabbitMq Exchange(交换机)定义
 * */
public enum RabbitMqExchange implements IRabbitMqExchange {

    MQ_EXCHANGE_TEST("mq.exchange.test") ;

    private String exchangeName;

    @Override
    public String exchangeName() {
        return this.exchangeName;
    }

    RabbitMqExchange(String exchangeName){
        this.exchangeName = exchangeName;
    }
}

定义测试Queue:mq.queue.test

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public enum RabbitMqQueue implements IRabbitMqQueue {
    MQ_QUEUE_TEST("mq.queue.test");

    private String queueName;

    @Override
    public String queueName() {
        return this.queueName;
    }

    RabbitMqQueue(String queueName){
        this.queueName = queueName;
    }

}

定义测试Routing:mq.routing.test

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * RabbitMq routing(路由定义)
 * */
public enum RabbitMqRouting implements IRabbitMqRouting {
    MQ_ROUTING_TEST("mq.routing.test");

    private String routingKey;

    @Override
    public String routingKey() {
        return this.routingKey;
    }

    RabbitMqRouting(String routingKey){
        this.routingKey = routingKey;
    }
}

定义绑定关系:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * RabbitMq Exchange(交换机) Routing(路由) Queue(队列) 的绑定关系
 * */
public enum RabbitMqBinding implements IRabbitMqBinding {

    MQ_BINDING_TEST(RabbitMqExchange.MQ_EXCHANGE_TEST,RabbitMqRouting.MQ_ROUTING_TEST,RabbitMqQueue.MQ_QUEUE_TEST,true);

    /**
     * exchange(交换机)
     */
    IRabbitMqExchange exchange;
    /**
     * routing(路由)
     */
    IRabbitMqRouting routing;
    /**
     * queue(队列)
     */
    IRabbitMqQueue queue;
    /**
     * 是否允许延时
     */
    boolean allowDelay = false;

    RabbitMqBinding(IRabbitMqExchange exchange,IRabbitMqRouting routing,IRabbitMqQueue queue){
        this.exchange = exchange;
        this.routing = routing;
        this.queue = queue;
    }

    RabbitMqBinding(IRabbitMqExchange exchange,IRabbitMqRouting routing,IRabbitMqQueue queue,boolean allowDelay){
        this.exchange = exchange;
        this.routing = routing;
        this.queue = queue;
        this.allowDelay = allowDelay;
    }

    @Override
    public IRabbitMqExchange exchange() {
        return this.exchange;
    }

    @Override
    public IRabbitMqRouting routing() {
        return this.routing;
    }

    @Override
    public IRabbitMqQueue queue() {
        return this.queue;
    }

    @Override
    public boolean allowDelay() {
        return this.allowDelay;
    }
}

测试消费者类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class TestConsumer extends AbstractMessageListener {

    Logger logger = LoggerFactory.getLogger(TestConsumer.class);
    @Override
    public boolean handleMessage(Object obj) {
        logger.info("rabbitmq消费者开始消费,消息内容:" +obj.toString());
        return true;
    }
}

启动项目

image-20200705215834650

登录rabbitmq控制台,已经自动创建了 交换机和延迟交换机,消息队列和死信队列

测试发送消息

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void testSendMq(){
     logger.info("生产者发送消息到mq");
     rabbitMqService.send(RabbitMqExchange.MQ_EXCHANGE_TEST, RabbitMqRouting.MQ_ROUTING_TEST,"测试发送消息");
 }

image-20200705221516347

image-20200705221535366

测试发送延时消息(60秒)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 @Test
public void testSendDelayMq(){
    logger.info("生产者发送延迟消息到mq");
     rabbitMqService.send(RabbitMqExchange.MQ_EXCHANGE_TEST, RabbitMqRouting.MQ_ROUTING_TEST,"测试发送延时消息60s",60*1000);
 }

代码获取

这个项目包含很多轮子实例,跪求star

https://github.com/pengziliu/GitHub-code-practice

已撸完部分开源轮子,更多精彩正在路上

模块

所属开源项目

项目介绍

springboot_api_encryption

rsa-encrypt-body-spring-boot

Spring Boot接口加密,可以对返回值、参数值通过注解的方式自动加解密 。

simpleimage-demo

simpleimage

图片处理工具类,具有加水印、压缩、裁切等功能

xxl-job-demo

xxl-job

分布式定时任务使用场景

xxl-sso-demo

xxl-sso

单点登录功能

vuepress-demo

vuepress

建立自己的知识档案库

xxl-conf-demo

xxl-conf

分布式配置中心

Shardingsphere-demo

Shardingsphere

分库分表

easyexcel-demo

easyexcel

excel操作工具类

kaptcha-demo

kaptcha

前后端分离验证码方案

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-07-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 JAVA葵花宝典 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
RabbitMQ之SpringAMQP
2、在生产者端编写测试类SpringAmqpTest,并利用RabbitTemplate实现消息发送:
叫我阿杰好了
2022/11/07
3500
RabbitMQ之SpringAMQP
18-RabbitMQ高级特性-死信队列
已经被加入到死信队列中了, 为啥是3呢, 应为我之前测试了两次, 这个时候, 如果是写业务的话, 就可以通过消费死信队列的消息, 完成消费失败的, 或者过期的补偿了~
彼岸舞
2022/10/06
2810
18-RabbitMQ高级特性-死信队列
RabbitMQ详解解答【面试+工作】
如果安装rabbitMQ首先安装基于erlang语言支持的OTP软件,然后在下载rabbitMQ软件进行安装(安装过程都是下一步,在此不在说了)
Java帮帮
2018/09/29
1.5K0
RabbitMQ详解解答【面试+工作】
RabbitMQ基础与实操复习
MQ(Message Quene) : 翻译为消息队列,通过典型的 生产者和消费者模型,生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,轻松的实现系统间解耦。别名为 消息中间件通过利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。
别团等shy哥发育
2023/10/17
2430
RabbitMQ基础与实操复习
快速入门RabbitMQ核心概念
我们知道RabbitMQ是基于Erlang编写的,所以在安装RabbitMQ之前需要确保安装了Erlang环境。RabbitMQ与Erlang是有版本对应关系的,可以参考官方列举的版本对应关系:
端碗吹水
2020/11/24
5210
RabbitMQ延迟消费和重复消费
转载自 https://blog.csdn.net/quliuwuyiz/article/details/79301054
allsmallpig
2021/02/25
2.4K0
java消息队列基础和RabbitMQ相关概念
消息队列一般简称为 MQ (Messges Queue),是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成,是在消息的传输过程中保存消息的容器。消息队列本质上是一个队列,而队列中存放的是一个个消息。
终有救赎
2024/06/04
1751
java消息队列基础和RabbitMQ相关概念
缓存架构之史上讲的最明白的RabbitMQ可靠消息传输实战演练
比如:某个广告主(如:天猫)想在我们的平台(如:今日头条)投放广告,当通过我们的广告系统新建广告的时候,该消息在同步给redis缓存(es)的时候丢失了,而我们又没有发现,造成该广告无法正常显示出来,那这损失就打了,如果1天都没有该广告的投放记录,那就有可能是上百万的损失了,所以消息的可靠传输多我们的广告系统也是很重要的。 其实,生活中这样的场景很场景,再比如:交易系统、订单系统都必须保证消息的可靠传输,否则,损失是巨大的!!!
Java知音
2018/09/26
7500
【Spring Boot实战与进阶】集成RabbitMQ的实例详解
   RabbitMQ是采用 Erlang语言实现AMQP协议的消息中间件,AMQP全称是 Advanced Message Queue Protocolo,高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。
程序员云帆哥
2022/05/12
5730
【Spring Boot实战与进阶】集成RabbitMQ的实例详解
RabbitMQ 延时交换机
安装插件后会生成新的Exchange类型x-delayed-message,该类型消息支持延迟投递机制,接收到消息后并未立即将消息投递至目标队列中,而是存储在mnesia(一个分布式数据系统)表中,并且当前节点是磁盘节点,那么节点重启后,消息还能保留。检测消息延迟时间,如达到可投递时间时并将其通过x-delayed-type类型标记的交换机类型投递至目标队列。但是要注意的是,如果集群中只有一个磁盘节点,如果说磁盘节点丢失,或者节点上的插件失效。意味着消息将会丢失。
王小明_HIT
2019/12/04
1.4K0
SpringBoot RabbitMQ
RabbitMQ的流程是:生产者将消息发送到对应交换机上,交换机再将消息转发到绑定的队列上,消费者从绑定的队列获取消息进行消费。
用户8682940
2021/12/02
5790
Rabbitmq小书
1.生产者(Publisher): 发布消息到RabbitMQ中的交换机(Exchange)上
大忽悠爱学习
2022/10/04
3.4K0
Rabbitmq小书
SpringBoot动态创建绑定rabbitMq队列
SpringBoot整合rabbitMq | 半月无霜 (banmoon.top)
半月无霜
2024/03/07
1K0
Spring Boot整合RabbitMQ详细教程
场景说明:用户注册后,需要发注册邮件和注册短信,传统的做法有两种1.串行的方式;2.并行的方式 (1)串行方式:将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。 这有一个问题是,邮件,短信并不是必须的,它只是一个通知,而这种做法让客户端等待没有必要等待的东西.
全栈程序员站长
2022/08/27
6620
Spring Boot整合RabbitMQ详细教程
RabbitMQ 学习笔记3 - Java 使用 RabbitMQ 示例
本节讲述 Java 使用 RabbitMQ 的示例,和 发送者确认回调,消费者回执的内容。
张云飞Vir
2021/07/23
7930
RabbitMQ---延迟队列,整合springboot
延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
大忽悠爱学习
2021/12/07
6550
RabbitMQ---延迟队列,整合springboot
【RabbitMQ】一文带你搞定RabbitMQ延迟队列
在上一篇中,介绍了RabbitMQ中的死信队列是什么,何时使用以及如何使用RabbitMQ的死信队列。相信通过上一篇的学习,对于死信队列已经有了更多的了解,这一篇的内容也跟死信队列息息相关,如果你还不了解死信队列,那么建议你先进行上一篇文章的阅读。
弗兰克的猫
2019/07/30
8940
【RabbitMQ】一文带你搞定RabbitMQ延迟队列
一起来学SpringBoot | 第十三篇:RabbitMQ延迟队列
初探RabbitMQ消息队列中介绍了 RabbitMQ的简单用法,顺带提及了下延迟队列的作用。所谓 延时消息就是指当消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费。
battcn
2018/08/03
1.3K0
一起来学SpringBoot | 第十三篇:RabbitMQ延迟队列
快速入门RabbitMQ
两种方式各有优劣,打电话可以立即得到响应,但是你却不能跟多个人同时通话。发送邮件可以同时与多个人收发邮件,但是往往响应会有延迟。
Maynor
2022/03/30
3550
快速入门RabbitMQ
RabbitMQ发布订阅实战-实现延时重试队列
RabbitMQ是一款使用Erlang开发的开源消息队列。本文假设读者对RabbitMQ是什么已经有了基本的了解,如果你还不知道它是什么以及可以用来做什么,建议先从官网的 RabbitMQ Tutorials 入门教程开始学习。
用户2131907
2018/05/15
3.3K1
相关推荐
RabbitMQ之SpringAMQP
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验