RabbitMQ之客户端开发向导
RabbitMQ Java客户端使用com.rabbitmq.client作为顶级包名,关键的Class和Interface有Channel、Connection、ConnectionFactory、Consumer等。AMQP协议层面的操作通过Channel接口实现。Connection是用来开启Channel(通道)的,可以注册事件处理器,也可以在应用结束时关闭连接。 与RabbitMQ 相关的开发工作,基本上是围绕 Connection和Channel 这两个类展开的。本文按照一个完整的运转流程进行讲解,详细内容有这几点:连接、交换器/队列的创建与绑定、发送消息、消费消息、消费消息的确认和关闭连接。
客户端开发:建立连接,创建交换器和队列,发送消息
1. 连接RabbitMQ
创建连接工厂并设置生产者基本的信息(IP地址、端口号、虚拟主机、用户名、密码等),通过连接工厂创建连接对象,通过连接对象创建通道channel,channel可以用来接收或者发送消息。
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername(USERNAME);
factory.setPassword(PASSWORD);
factory.setVirtualHost(virtualHost) ;
factory.setHost(IP ADDRESS);
factory.setPort(PORT) ;
//也可以用过url进行设置
//factory.setUri( "amqp://userName:password@ipAddress:portNumber/virtualHost");
Connection conn = factory.newConnection();
Channel channel = conn.createChannel();
注意:
Connection可以用来创建多个Channel实例,但是Channel实例不能再线程间共享,应用程序应该为每一个线程开辟一个Channel。某些情况下Channel的操作可以并发运行,但是再其他情况下会导致在网络上出现错误的通信帧交错,同时也会影响发送方确认(publisher confirm)机制的运行,所以多线程间共享Channel实例是非线程安全的。
2. 使用交换器(exchange)和队列(queue)
交换器和队列是AMQP中high-level层面的构建模块,应用程序需要确保在使用它们的时候就已经存在了,在使用之前需要先声明(declare)它们。
如下代码演示了如何声明一个交换器和临时队列:
String exchangeName = "logs_direct";
// 通道声明交换器以及交换的类型
channel.exchangeDeclare(exchangeName, "direct");
// 创建一个临时队列
String queue = channel.queueDeclare().getQueue();
// 基于route key绑定队列和交换器
channel.queueBind(queue, exchangeName, "error");
上面声明的队列具有特性:只对当前应用中同一个Connection层面可用,同一个Connection的不同Channel可共用,并且也会在应用连接断开时自动删除。
如果要在应用中共享一个队列,可以做如下声明:
// 获取连接中通道
Channel channel = connection.createChannel();
// 通道声明交换器以及交换的类型
channel.exchangeDeclare(exchangeName, "direct");
// 通道绑定对应的消息队列
// 参数1:队列名称,如果队列不存在,则自动创建
// 参数2:用来定义队列特性是否持久化,true表示持久化,false表示不持久化
// 参数3:exclusive:是否独占队列,true独占队列,false不独占队列
// 参数4:autoDelete:是否在消费完成后自动删除队列,true自动删除,false不自动删除
// 参数5:额外附加参数
channel.queueDeclare("hello", false, false, false, null);
// 基于route key绑定队列和交换器
channel.queueBind("hello", exchangeName, "info");
注意:Channel的API方法都是可以重载的。
exchangeDeclare方法详解
public DeclareOk exchangeDeclare(String exchange, String type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments) throws IOException {
return (DeclareOk)this.exnWrappingRpc((new com.rabbitmq.client.AMQP.Exchange.Declare.Builder()).exchange(exchange).type(type).durable(durable).autoDelete(autoDelete).internal(internal).arguments(arguments).build()).getMethod();
}
这个方法的返回值是Exchange.DeclareOK用来标识成功声明了一个交换器。
各个参数详细说明如下:
①exchange:交换器的名称。
②type:交换器的类型,常见的如:fanout,direct,topic,headers。
③durable:设置是否持久化, durable 设置为 true 表示持久化, 反之是非持久化 。持久化可以将交换器存盘,在服务器重启的时候不会丢失相关信息。
④autoDelete:设置是否自动删除。 autoDelete 设置为 true 则表示自动删除。自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑。注意不能错误地把这个参数理解为 "当与此交换器连接的客户端都断开时 RabbitMQ 会自动删除本交换器
⑤internal:设置是否是内置的。如果设置为 true ,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式。
⑥argument:其他一些结构化参数,比如 alternate-exchange。
exchangeDeclare的其他重载方法:
public DeclareOk exchangeDeclare(String exchange, String type) throws IOException {
return this.exchangeDeclare(exchange, (String)type, false, false, (Map)null);
}
public DeclareOk exchangeDeclare(String exchange, String type, boolean durable) throws IOException {
return this.exchangeDeclare(exchange, (String)type, durable, false, (Map)null);
}
public DeclareOk exchangeDeclare(String exchange, String type, boolean durable, boolean autoDelete, Map<String, Object> arguments) throws IOException {
return this.exchangeDeclare(exchange, type, durable, autoDelete, false, arguments);
}
与此对应,将第二个参数String type换成BuiltinExchangeType type对应的几个重载方法:
public DeclareOk exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, Map<String, Object> arguments) throws IOException {
return this.exchangeDeclare(exchange, type.getType(), durable, autoDelete, arguments);
}
public DeclareOk exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments) throws IOException {
return this.exchangeDeclare(exchange, type.getType(), durable, autoDelete, internal, arguments);
}
public DeclareOk exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable) throws IOException {
return this.exchangeDeclare(exchange, type.getType(), durable);
}
public DeclareOk exchangeDeclare(String exchange, BuiltinExchangeType type) throws IOException {
return this.exchangeDeclare(exchange, type.getType());
}
另外,还有几个与exchangeDeclare相似的方法,比如exchangeDeclareNoWait,具体定义如下:
public void exchangeDeclareNoWait(String exchange, String type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments) throws IOException {
this.transmit(new AMQCommand((new com.rabbitmq.client.AMQP.Exchange.Declare.Builder()).exchange(exchange).type(type).durable(durable).autoDelete(autoDelete).internal(internal).arguments(arguments).passive(false).nowait(true).build()));
}
public void exchangeDeclareNoWait(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments) throws IOException {
this.exchangeDeclareNoWait(exchange, type.getType(), durable, autoDelete, internal, arguments);
}
这个exchangeDeclareNoWait比exchangeDeclare多了一个NoWait参数。这个NoWait参数指的是AMQP中Exchange.Declare命名的参数,注意,该方法为void方法,意思是不需要服务器返回。而普通的exchangeDeclare方法的返回值是DechareOK,意思是在客户端声明了一个交换器之后,需要等待服务器的返回。而对于exchangeDeclareNoWait,考虑这样一种情况,在声明完一个交换器之后(实际服务器还并未完成交换器的创建) ,那么此时客户端紧接着使用这个交换器,必然会发生异常。如果没有特殊的缘由和应用场景,并不建议使用这个方法。
另一个与exchangeDeclare相似的方法是exchangeDeclarePassive:
public DeclareOk exchangeDeclarePassive(String exchange) throws IOException {
return (DeclareOk)this.exnWrappingRpc((new com.rabbitmq.client.AMQP.Exchange.Declare.Builder()).exchange(exchange).type("").passive().build()).getMethod();
}
这个方法在实际应用过程中还是非常有用的,它主要用来检测相应的交换器是否存在。如果存在则正常返回:如果不存在则抛出异常:404 channel exception ,同时 Channel 也会被关闭。
有声明创建交换器的方法,当然也有删除交换器的方法。相应的方法如下:
public DeleteOk exchangeDelete(String exchange) throws IOException {
return this.exchangeDelete(exchange, false);
}
public DeleteOk exchangeDelete(String exchange, boolean ifUnused) throws IOException {
return (DeleteOk)this.exnWrappingRpc((new com.rabbitmq.client.AMQP.Exchange.Delete.Builder()).exchange(exchange).ifUnused(ifUnused).build()).getMethod();
}
public void exchangeDeleteNoWait(String exchange, boolean ifUnused) throws IOException {
this.transmit(new AMQCommand((new com.rabbitmq.client.AMQP.Exchange.Delete.Builder()).exchange(exchange).ifUnused(ifUnused).nowait(true).build()));
}
其中 exchange 表示交换器的名称,而ifUnused 用来设置是否在交换器没有被使用的情况下删除,如果 ifUnused 设置为 true ,则只有在此交换器没有被使用的情况下才会被删除,如果设置 false ,则无论如何这个交换器都要被删除。
queueDeclare方法详解
queueDeclare 相对于 exchangeDeclare 方法而言,重载方法的个数就少很多,它只有两个重载方法:
public com.rabbitmq.client.AMQP.Queue.DeclareOk queueDeclare() throws IOException {
return this.queueDeclare("", false, true, true, (Map)null);
}
public com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) throws IOException {
validateQueueNameLength(queue);
return (com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk)this.exnWrappingRpc((new com.rabbitmq.client.AMQP.Queue.Declare.Builder()).queue(queue).durable(durable).exclusive(exclusive).autoDelete(autoDelete).arguments(arguments).build()).getMethod();
}
不带任何参数的 queueDeclare 方法默认创建一个由 RabbitMQ 命名的(类似这种amq.gen-LhQzlgv3GhDOv8PIDabOXA 名称,这种队列也称之为匿名队列)、排他的、自动删除的、非持久化的队列。
方法的参数详细说明如下所述:
①queue:队列的名称。
②durable: 设置是否持久化。为 true 则设置队列为持久化。持久化的队列会存盘,在服务器重启的时候可以保证不丢失相关信息。
③exclusive 设置是否排他。为 true 则设置队列为排他的。如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。这里需要注意三点:排他队列是基于连接( Connection) 可见的,同一个连接的不同信道 (Channel)是可以同时访问同一连接创建的排他队列; "首次"是指如果某个连接己经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同:即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除,这种队列适用于一个客户端同时发送和读取消息的应用场景。
④autoDelete: 设置是否自动删除。为 true 则设置队列为自动删除。**自动删除的前提是:至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除。**不能把这个参数错误地理解为: “当连接到此队列的所有客户端断开时,这个队列自动删除”,因为生产者客户端创建这个队列,或者没有消费者客户端与这个队列连接时,都不会自动删除这个队列。
⑤argurnents: 设置队列的其他一些参数,如:x-rnessage-ttl、x-expires、x-rnax-length、x-rnax-length-bytes、x-dead-letter-exchange、x-deadletter-routing-key、x-rnax-priority 等。
注意:生产者和消费者都能够使用 queueDeclare 来声明一个队列,但是如果消费者在同一个信道上订阅了另一个队列,就无法再声明队列了。必须先取消订阅,然后将信道置为"传输"模式,之后才能声明队列。
与交换器方法相同,队列也有两个师出同门的方法:queueDeclareNoWait和queueDeclarePassive,意思与交换器相同,不做解释了。
与交换器对应,关于队列也有删除的相应方法:
public com.rabbitmq.client.impl.AMQImpl.Queue.DeleteOk queueDelete(String queue, boolean ifUnused, boolean ifEmpty) throws IOException {
validateQueueNameLength(queue);
return (com.rabbitmq.client.impl.AMQImpl.Queue.DeleteOk)this.exnWrappingRpc((new com.rabbitmq.client.AMQP.Queue.Delete.Builder()).queue(queue).ifUnused(ifUnused).ifEmpty(ifEmpty).build()).getMethod();
}
public void queueDeleteNoWait(String queue, boolean ifUnused, boolean ifEmpty) throws IOException {
validateQueueNameLength(queue);
this.transmit(new AMQCommand((new com.rabbitmq.client.AMQP.Queue.Delete.Builder()).queue(queue).ifUnused(ifUnused).ifEmpty(ifEmpty).nowait(true).build()));
}
public com.rabbitmq.client.impl.AMQImpl.Queue.DeleteOk queueDelete(String queue) throws IOException {
return this.queueDelete(queue, false, false);
}
其中 queue 表示队列的名称,ifUnused 可以参考上一小节的交换器。 ifEmpty 设置为true 表示在队列为空(队列里面没有任何消息堆积)的情况下才能够删除。
与队列相关的还有一个有意思的方法——queuepurge,区别于 queueDelete ,这个方法用来清空队列中的内容,而不删除队列本身,具体定义如下:
public PurgeOk queuePurge(String queue) throws IOException {
validateQueueNameLength(queue);
return (PurgeOk)this.exnWrappingRpc((new com.rabbitmq.client.AMQP.Queue.Purge.Builder()).queue(queue).build()).getMethod();
}
queueBind方法详解
将队列和交换器绑定的方法如下,可以与前两节中的方法定义进行类比。
public com.rabbitmq.client.impl.AMQImpl.Queue.BindOk queueBind(String queue, String exchange, String routingKey, Map<String, Object> arguments) throws IOException {
validateQueueNameLength(queue);
return (com.rabbitmq.client.impl.AMQImpl.Queue.BindOk)this.exnWrappingRpc((new com.rabbitmq.client.AMQP.Queue.Bind.Builder()).queue(queue).exchange(exchange).routingKey(routingKey).arguments(arguments).build()).getMethod();
}
public com.rabbitmq.client.impl.AMQImpl.Queue.BindOk queueBind(String queue, String exchange, String routingKey) throws IOException {
return this.queueBind(queue, exchange, routingKey, (Map)null);
}
public void queueBindNoWait(String queue, String exchange, String routingKey, Map<String, Object> arguments) throws IOException {
validateQueueNameLength(queue);
this.transmit(new AMQCommand((new com.rabbitmq.client.AMQP.Queue.Bind.Builder()).queue(queue).exchange(exchange).routingKey(routingKey).arguments(arguments).nowait(true).build()));
}
方法中涉及的参数详解:
②queue:队列名称:
②exchange:交换器的名称:
③routingKey:用来绑定队列和交换器的路由键;
④argument:定义绑定的一些参数。
不仅可以将队列和交换器绑定起来,也可以将已经被绑定的队列和交换器进行解绑。具体方法可以参考如下(具体的参数解释可以参考前面的内容,这里不再赘述):
public com.rabbitmq.client.impl.AMQImpl.Queue.UnbindOk queueUnbind(String queue, String exchange, String routingKey, Map<String, Object> arguments) throws IOException {
validateQueueNameLength(queue);
return (com.rabbitmq.client.impl.AMQImpl.Queue.UnbindOk)this.exnWrappingRpc((new com.rabbitmq.client.AMQP.Queue.Unbind.Builder()).queue(queue).exchange(exchange).routingKey(routingKey).arguments(arguments).build()).getMethod();
}
public com.rabbitmq.client.impl.AMQImpl.Queue.UnbindOk queueUnbind(String queue, String exchange, String routingKey) throws IOException {
return this.queueUnbind(queue, exchange, routingKey, (Map)null);
}
exchangeBind方法详解
不仅可以将交换器与队列绑定,也可以将交换器与交换器绑定,后者和前者的用法如下:
public BindOk exchangeBind(String destination, String source, String routingKey, Map<String, Object> arguments) throws IOException {
return (BindOk)this.exnWrappingRpc((new com.rabbitmq.client.AMQP.Exchange.Bind.Builder()).destination(destination).source(source).routingKey(routingKey).arguments(arguments).build()).getMethod();
}
public void exchangeBindNoWait(String destination, String source, String routingKey, Map<String, Object> arguments) throws IOException {
this.transmit(new AMQCommand((new com.rabbitmq.client.AMQP.Exchange.Bind.Builder()).destination(destination).source(source).routingKey(routingKey).arguments(arguments).nowait(true).build()));
}
public BindOk exchangeBind(String destination, String source, String routingKey) throws IOException {
return this.exchangeBind(destination, source, routingKey, (Map)null);
}
绑定之后,消息从source交换器转发到destination交换器,某种程序上说destination交换器可以看作一个队列,示例代码如下:
channel.exchangeDechare("source","direct",false,true,null);
channel.exchangeDeclare("destination","fanout",false,true,null);
channel.exchangeBind("destination","source","exKey");
channel.queueDeclare("queue",false,false,true,null);
channel.queueBind("queue","destination","");
channel.basicPublish("source","exKey",null,"exToExDemo".getBytes());
生产者发送消息至交换器 source 中,交换器 source 根据路由键找到与其匹配的另一个交换器destination,并把消息转发到 destination中,进而存储在 destination 绑定的队列 queue中,如下图:

3. 何时创建
从前面的文章我们知道,RabbitMQ可以选择在生产者创建队列,也可以在消费者端创建队列,也可以提前创建好队列,而生产者消费者直接使用即可。
**RabbitMQ的消息存储在队列中,交换器的使用并不真正耗费服务器的性能,而队列会。**如在实际业务应用中,需要对所创建的队列的流量、内存占用及网卡占用有一个清晰的认知,预估其平均值和峰值,以便在固定硬件资源的情况下能够进行合理有效的分配。
按照RabbitMQ官方建议,生产者和消费者都应该尝试创建(这里指声明操作)队列。这虽然是一个很好的建议,但是在我看来这个时间上没有最好的方案,只有最适合的方案。我们往往需要结合业务、资源等方面在各种方案里面选择一个最适合我们的方案。
如果业务本身在架构设计之初己经充分地预估了队列的使用情况,完全可以在业务程序上线之前在服务器上创建好(比如通过页面管理、RabbitMQ命令或者更好的是从配置中心下发),这样业务程序也可以免去声明的过程,直接使用即可。
**预先创建好资源还有一个好处是,可以确保交换器和队列之间正确地绑定匹配。**很多时候,由于人为因素、代码缺陷等,发送消息的交换器并没有绑定任何队列,那么消息将会丢失;或者交换器绑定了某个队列,但是发送消息时的路由键无法与现存的队列匹配,那么消息也会丢失。当然可以配合mandatory参数或者备份交换器来提高程序的健壮性。
与此同时,预估好队列的使用情况非常重要,如果在后期运行过程中超过预定的阈值,可以根据实际情况对当前集群进行扩容或者将相应的队列迁移到其他集群。迁移的过程也可以对业务程序完全透明。此种方法也更有利于开发和运维分工,便于相应资源的管理。如果集群资源充足,而即将使用的队列所占用的资源又在可控的范围之内,为了增加业务程序的灵活性,也完全可以在业务程序中声明队列。
至于是使用预先分配创建资源的静态方式还是动态的创建方式,需要从业务逻辑本身、公司运维体系和公司硬件资源等方面考虑。
4. 发送消息
如果要发送一个消息,可以使用Channel类的basicPublish 方法,比如发送一条内容为"Hello World! "的消息。
channel.basicPublish(exchangeName , routingKey , null , "Hello World! ".getBytes());
basicPublish还有几个重载方法:
public void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException {
this.delegate.basicPublish(exchange, routingKey, props, body);
}
public void basicPublish(String exchange, String routingKey, boolean mandatory, BasicProperties props, byte[] body) throws IOException {
this.delegate.basicPublish(exchange, routingKey, mandatory, props, body);
}
public void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body) throws IOException {
this.delegate.basicPublish(exchange, routingKey, mandatory, immediate, props, body);
}
对应的具体参数解释如下所述。
①exchange:交换器的名称,指明消息需要发送到哪个交换器中。如果设置为空字符串,则消息会被发送到 RabbitMQ 默认的交换器中。
②routingKey:路由键,交换器根据路由键将消息存储到相应的队列之中
③props:消息的基本属性集,其包含 14 个属性成员,分别有 contentType、contentEncoding 、headers (Map<String,Object>) 、deliveryMode、priority、correlationld、 replyTo、expiration、messageld、timestamp、type、userld、appld、clusterId 。
④byte[] body:消息体(payload),真正需要发送的消息。
⑤mandatory 和immediate。
示例:
channel.basicPub1ish(exchangeName,routingKey,
new AMQP.BasicProperties.Bui1der ()
.contentType( " text/p1ain" )
.de1iveryMode(2)
.priority(1)
.userld("hidden")
.headers(new HashMap<String, Object>())
.expiration("60000")
.build()),
messageBodyBytes)
上面这行代码发送了一条带有headers和过期时间expiration的消息,这条消息的投递模式 (delivery mode )设置为 2 ,即消息会被持久化(即存入磁盘)在服务器中。同时这条消息的优先级(priority)设置为 1,content-type为“text/plain” 。
下面是一段实际测试代码:
生产者:
package helloworld;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.Test;
import utils.RabbitMQUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Provider {
@Test
public void testSendMessage() throws IOException, TimeoutException {
/*// 创建连接mq的连接工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
// 设置连接mq主机
connectionFactory.setHost("192.168.23.121");
// 设置端口号
connectionFactory.setPort(5672);
// 设置连接哪个虚拟主机
connectionFactory.setVirtualHost("/ems");
// 设置访问虚拟主机的用户名和密码
connectionFactory.setUsername("ems");
connectionFactory.setPassword("123");
// 获取连接对象
Connection connection = connectionFactory.newConnection();*/
// 通过工具类获取连接对象
Connection connection = RabbitMQUtils.getConnection();
// 获取连接中通道
Channel channel = connection.createChannel();
// 通道绑定对应的消息队列
// 参数1:队列名称,如果队列不存在,则自动创建
// 参数2:用来定义队列特性是否持久化,true表示持久化,false表示不持久化
// 参数3:exclusive:是否独占队列,true独占队列,false不独占队列
// 参数4:autoDelete:是否在消费完成后自动删除队列,true自动删除,false不自动删除
// 参数5:额外附加参数
channel.queueDeclare("hello", false, false, false, null);
// 发布消息
// 参数1:交换器名称,参数2:队列名称,参数3:传递消息额外设置,参数4:消息的具体内容
channel.basicPublish("", "hello", null, "hello rabbitmq".getBytes());
/*channel.close();
connection.close();*/
//调用工具类
RabbitMQUtils.closeConnectionAndChannel(channel, connection);
}
}
消费者:
package helloworld;
import com.rabbitmq.client.*;
import org.junit.Test;
import utils.RabbitMQUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException {
/*// 创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.23.121");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/ems");
connectionFactory.setUsername("ems");
connectionFactory.setPassword("123");
// 创建连接对象
Connection connection = connectionFactory.newConnection();*/
// 通过工具类获取连接
Connection connection = RabbitMQUtils.getConnection();
// 创建通道
Channel channel = connection.createChannel();
// 通道绑定对象
channel.queueDeclare("hello", false, false, false, null);
// 消费消息
// 参数1:消费哪个队列的消息,队列的名称
// 参数2:开启消息的自动确认机制
// 参数3:消费消息时的回调接口
channel.basicConsume("hello", true, new DefaultConsumer(channel) {
@Override //最后以恶参数:消息队列中取出的消息
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("new String(body) = " + new String(body));
}
});
}
}
参考资料:
- 《RabbitMQ实战指南》朱忠华 著
- https://blog.youkuaiyun.com/qq_39022311/article/details/107013065
- https://www.cnblogs.com/hunternet/p/9713710.html
1万+

被折叠的 条评论
为什么被折叠?



