目录
RabbitMQ Java客户端使用com.rabbitmq.client作为顶级包名,关键的Class和Interface有Channel、Connection、ConnectionFactory、Consumer等。AMQP协议层面的操作通过Channel接口实现。Connection是用来开启Channel(信道)的,可以注册事件处理器,也可以在应用结束时关闭连接。与RabbitMQ相关的开发工作,基本上也是围绕Connection和Channel这两个类展开的。
这里按照一个完整的运转流程进行讲解,详细内容有:连接、交换器/队列的创建与绑定、发送消息、消费消息、消费消息的确认和关闭连接。
一、连接RabbitMQ服务器
连接ActiveMQ服务器的方式一:
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(IP_ADDRESS);
factory.setPort(PORT);
factory.setVirtualHost(virtualHost); //虚拟消息服务器host
factory.setUsername("root");
factory.setPassword("root");
Connection connection = factory.newConnection(); //创建连接
使用uri来连接ActiveMQ方式二:
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://username:password@ipAddress:portNumber/virtualHost");
Connection connection = factory.newConnection(); //创建连接
Channel channel = connection.createChannel();//创建信道
注意点:
Connection可以用来创建多个Channel实例,但是Channel实例不能在线程问共享,应用程序应该为每一个线程开辟一个Channel。某些情况下Channel的操作可以并发运行,但是在其他情况下会导致在网络上出现错误的通信帧交错,同时也会影响发送方确认(publisher confirm)机制的运行(后面介绍),所以多线程问共享Channel实例是非线程安全的。
Channel或者Connection中有个isOpen方法可以用来检测其是否己处于开启状态(),但是我们一般不使用这个方法,一般用CreateXXX,newXX方法后,我们默认的认为Connection,Channel已经成功的开启了,如果在使用Channel的时候其己经处于关闭状态,那么程序会抛出一个com.rabbitmq.client.ShutdownSignalException,我们只需捕获这个异常即可。当然同时也要试着捕获IOExceptio口或者SocketException,以防Connection意外关闭。
二、使用交换器和队列
交换器和队列是AMQP中high level层面的构建模块,应用程序需确保在使用它们的时候就已经存在了,在使用之前需要先声明(declare)它们,然后再bind起来,这其中涉及到了bindingKey,代码:
Connection connection = factory.newConnection(); //创建连接
Channel channel = connection.createChannel();//创建信道
//创建一个type="direct" 、持久化,非自动删除的交换器
channel.exchangeDeclare(EXCHANGE_NAME,"direct",true,false,null);
//创建一个持久化,非排他的,非自动删除的队列
channel.queueDeclare(QUEUE_NAME,true,false,false,null);
//将交换器与队列通过路由绑定
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,ROUTING_KEY);
1.声明查找删除交换器
1.1 声明交换器方法如下:
DeclareOk exchangeDeclare(String exchange, String type, boolean durable,
boolean autodelete,boolean internal, Map<String, Object> arguments) throws IOException;
参数详细说明如下所述:
- exchange:交换器的名称。
- type:交换器的类型,常见的如fanout、direct、topic,header 详情看上一章。
- durable:设置是否持久化。durable设置为true表示持久化,反之是非持久化。持久化可以将交换器存盘,在服务器重启的时候不会丢失相关信息。
- autoDelete:设置是否自动删除。autoDelete设置为true则表示自动删除。自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑。注意不能错误地把这个参数理解为:"当与此交换器连接的客户端都断开时,RabbitMQ会自动删除本交换器"。
- internal:设置是否是内置的。如果设置为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式。
- argument:其他一些结构化参数,比如alternate-exchange(有alternateexchange的详情后面介绍)。
相关的重载方法:
其中没有带的参数,布尔值默认是false,Map参数为null
DeclareOk exchangeDeclare(String exchange, String type) throws IOException;
DeclareOk exchangeDeclare(tring exchange, String type, boolean durable) throws IOException;
DeclareOk exchangeDeclare(String exchange, String type, boolean durable, boolean autoDelete, Map<String, Object> arguments) throws IOException;
DeclareOk exchangeDeclare(String exchange, String type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments) throws IOException;
1.2 与上面相对应的把第二参数变为BuiltinExchangeType type,这是封装的枚举type,也可以通过这个来生命交换器类型,源码如下:
public enum BuiltinExchangeType {
DIRECT("direct"),
FANOUT("fanout"),
TOPIC("topic"),
HEADERS("headers");
private final String type;
private BuiltinExchangeType(String type) {
this.type = type;
}
public String getType() {
return this.type;
}
}
1.3 还有声明为noWait的方法,这个nowait参数指的是AMQP中Exchange.Declare命令的参数,意思是不需要服务器返回,注意这个方法的返回值是void,而普通的exchangeDeclare方法的返回值是Exchange.DeclareOk,意思是在客户端声明了一个交换器之后,需要等待服务器的返回(服务器会返回Exchange.Declare-Ok这个AMQP命令)。针对"exchangeDeclareNoWait不需要服务器任何返回值"这一点,考虑这样一种情况,在声明完一个交换器之后(实际服务器还并未完成交换器的创建),那么此时客户端紧接着使用这个交换器,必然会发生异常。如果没有特殊的缘由和应用场景,并不建议使用这个方法。
public void exchangeDeclareNoWait(String exchange, String type, boolean durable,
boolean autoDelete, boolean internal, Map<String, Object> arguments) throws IOException
1.4 检测交换器是否存在的方法,这个方法在实际应用过程中还是非常有用的,它主要用来检测相应的交换器是否存在。如果存在则正常返回:如果不存在则抛出异常:404 channel exception,同时Channel也会被关闭。
public DeclareOk exchangeDeclarePassive(String name)
1.5 删除交换器的方法,方法中exchange表示交换器的名称,而ifUnused用来设置是否在交换器没有被使用的情况下删除。如果isUnused设置为true,则只有在此交换器没有被使用的情况下才会被删除:如果设置false,则无论如何这个交换器都要被删除,默认是false的。
DeleteOk exchangeDelete(String exchange, boolean ifUnused) throws IOException;
void exchangeDeleteNoWait(String exchange, boolean ifUnused) throws IOException;
DeleteOk exchangeDelete(String exchange) throws IOException; //默认为false
2. 声明查找删除队列
2.1 这里只有两个方法:
Queue.DeclareOk queueDeclare() throws IOException;
Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive,
boolean autoDelete, Map<String, Object> arguments) throws IOException;
不带任何参数的queueDeclare方法默认创建一个由RabbitMQ命名的(类似这种amq.gen-LhQzlgv3DOv8PIDabOXA名称,这种队列也称之为匿名队列〉、排他的(true)、自动删除的(true)、非持久化(false)的队列,如下所示获取队列的名字:
String queueName = channel.queueDeclare().getQueue( );
方法的参数详细说明如下:
- queue:队列的名称
- durable:设置是否持久化。为true则设置队列为持久化。持久化的队列会存盘,在服务器重启的时候可以保证不丢失相关信息
- exclusive:设置是否排他。为t r u e则设置队列为排他的。如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。这里需要注意三点:排他队列是基于连接(Connection )可见的,同一个连接的不同信道( Channel )是可以同时访问同一连接创建的排他队列;"首次"是指如果一个连接己经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同:即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除,这种队列适用于一个客户端同时发送和读取消息的应用场景
- autoDelete:设置是否自动删除。为t r u e则设置队列为自动删除。自动删除的前提是:至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除。不能把这个参数错误地理解为:"当连接到此队列的所有客户端断开时,这个队列自动删除",因为生产者客户端创建这个队列,或者没有消费者客户端与这个队列连接时,都不会自动删除这个队列
- argurnents:设置队列的其他一些参数,如x-message-ttl、x-expires、x-max-length、x-max-length-bytes、x-dead-letter-exchange、x-dead-letter-routing-key ,x-max-priority等
注意点:
生产者和消费者都能够使用queueDeclare来声明一个队列,但是如果消费者在同一个信道上订阅了另一个队列,就无法再声明队列了。必须先取消订阅,然后将信道置为"传输"模式,才能声明队列
2.2 这里也有一个nowait声明queue的方法,返回值void和exchangeDeclareNoWait类似, 表示不需要服务端的任何返回。同样也需要注意,在调用完queueDeclareNoWait方法之后,紧接着使用声明的队列时有可能会发生异常情况。
public void queueDeclareNoWait(String queue, boolean durable, boolean exclusive,
boolean autoDelete, Map<String, Object> arguments)
2.3 同样这里还有个queueDeclarePassive的方法,也比较常用。这个方法用来检测相应的队列是否存在。如果存在则正常返回,如果不存在则抛出异常:404 channel exception,同时Channel也会被关闭。方法定义如下:
public queueDeclarePassive(String queue)
2.4 删除队列的方法
- 其中queue表示队列的名称,
- ifUnused以参考交换器(ifUnused用来设置是否在交换器没有被使用的情况下删除。如果isUnused设置为true,则只有在此交换器没有被使用的情况下才会被删除:如果设置false,则无论如何这个交换器都要被删除,默认是false的)
- ifEmpty设置为t r u e表示在队列为空(队列里面没有任何消息堆积)的情况下才能够删除。
public com.rabbitmq.client.AMQP.Queue.DeleteOk queueDelete(String queue) throws IOException {
return this.queueDelete(queue, false, false);
}
public com.rabbitmq.client.AMQP.Queue.DeleteOk queueDelete(String queue, boolean ifUnused, boolean ifEmpty) throws IOException {
this.deleteRecordedQueue(queue);
return this.delegate.queueDelete(queue, ifUnused, ifEmpty);
}
public void queueDeleteNoWait(String queue, boolean ifUnused, boolean ifEmpty) throws IOException {
this.deleteRecordedQueue(queue);
this.delegate.queueDeleteNoWait(queue, ifUnused, ifEmpty);
}
3. 队列绑定交换器和解绑 queueBind
3.1 绑定队列和交换器
方法中涉及的参数详解:
- queue:队列名称
- exchange:交换器的名称
- routingKey:用来绑定队列和交换器的路由键
- argument:定义绑定的一些参数
public Queue.BindOk queueBind(String queue, String exchange, String routingKey, Map<String, Object> arguments)
public AMQP.Queue.BindOk queueBind(String queue, String exchange, String routingKey)
public void queueBindNoWait(String queue, String exchange, String routingKey, Map<String, Object> arguments)
3.2 将绑定的队列和交换器解绑
public Queue.UnbindOk queueUnbind(String queue, String exchange, String routingKey, Map<String, Object> arguments)
public Queue.UnbindOk queueUnbind(String queue, String exchange, String routingKey)
4.交换器和交换器绑定exchangeBind
这里的destination表示和队列相连的exchange交换器的名字,source表示和消息发布者相连的交换器名字。
public BindOk exchangeBind(String destination, String source, String routingKey, Map<String, Object> arguments)
public BindOk exchangeBind(String destination, String source, String routingKey)
void exchangeBindNoWait(String destination, String source, String routingKey, Map<String, Object> arguments) throws IOException
例子:
生产者发送消息至交换器source中,交换器source根据路由键找到与其匹配的另一个交换器destination,井把消息转发到destination中,进而存储在destination绑定的队列queue中,如下图。
channel.exchangeDeclare("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( )) ;
三.发送消息
方法用basicPublish如下:
void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body);
void basicPublish(String exchange, String routingKey, boolean mandatory, BasicProperties props, byte[] body);
void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body);
上面的参数解析如下:
- exchange:交换器的名称,指明消息需要发送到哪个交换器中。如果设置为空字符串,则消息会被发送到RabbitMQ默认的交换器中。
- routingKey:路由键,交换器根据路由键将消息存储到相应的队列之中。
- props:消息的基本属性集,其包含14个属性成员,分别有contentType、contentEncoding、headers(Map<String,Object>)、deliveryMode、priority、correlationld、replyTo、expiration、messageld、timestamp、type、userld、appld、clusterld。其中常用的几种都在上面的示例中进行了演示。
- byte[] body:消息体(payload),真正需要发送的消息。
- mandatory和immediate的详细内容后面讲解
常用的消息发送例子:
//发送持久化信息
String message = "hello world";
channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY,MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
这里的PERSISTENT_TEXT_PLAIN如下所示:
public static final BasicProperties PERSISTENT_TEXT_PLAIN = new BasicProperties("text/plain",
(String)null, (Map)null, 2, 0, (String)null, (String)null, (String)null, (String)null, (Date)null, (String)null, (String)null, (String)null, (String)null);
5.1 上面这行代码发送了一条消息,这条消息的投递模式(deliveryMode)设置为2,即消息会被持久化(即存入磁盘)在服务器中。同时这条消息的优先级(priority)设置为1,content-type为"text/plain ",可以自己设消息的属性:
channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY,
new AMQP.BasicProperties().builder()
.contentType("text/plain")
.deliveryMode(2)
.priority(1)
.userId("hidden")
.build(),
message.getBytes());
5.2 也可以发送带有headers的消息:
Map<String,Object> headers = new HashMap<>();
headers.put("location","here");
headers.put("time","today");
channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY,
new AMQP.BasicProperties().builder().headers(headers).build(),message.getBytes());
5.3 带有过期(expiration)信息的消息:
channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY,
new AMQP.BasicProperties().builder().expiration("60000").build(),message.getBytes());
四.消费消息
RabbitMQ的消费模式分两种:推(Push)模式和拉(Pull)模式。推模式采用Basic.Consume进行消费,而拉模式则是调用Basic.Get进行消费。
1. 推模式push
可以通过持续订阅的方式来消费,使用到的类Consumer,DefaultConsumer,接受消息一般通过实现Consumer接口或者继承DefaultConsumer类,当调用Consumer相关的API方法时,不同的订阅需要制定不同的消费者标签(consumerTag)来区分彼此,在同一个channel中的消费者也需要通过唯一的消费标签用来做区分,关键消费代码如下:
boolean autoAck = false;
channel.basicQos(64);//设置客户端最多接受违背ack的信息的个数
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("recieve message :"+new String(body));
try {
TimeUnit.SECONDS.sleep(1); //休眠一秒在返回确定信息
}catch (InterruptedException e){
e.printStackTrace();
}
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//这里的自动应答被关闭,通过手动确认收到消息后在做应答,这个可以有效的防止消息不必要的丢失。
channel.basicConsume(QUEUE_NAME,autoAck,"myConsumerTag",consumer);
相关的消费方法:
String basicConsume(String queue, Consumer callback) throws IOException;
String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException;
String basicConsume(String queue, boolean autoAck, String consumerTag, Consumer callback) throws IOException;
String basicConsume(String queue, boolean autoAck, Map<String, Object> arguments, Consumer callback) throws IOException;
String basicConsume(String queue, boolean autoAck, String consumerTag, boolean noLocal, boolean exclusive, Map<String, Object> arguments, Consumer callback) throws IOException;
对应的参数说明如下所述:
- queue:队列的名称;
- autoAck:设置是否自动确认。建议设成false,即不自动确认,默认不设置也是false;
- consumerTag:消者标签,用来区分多个消费者,不设置默认“”;
- noLocal:设置为true则表示不能将同一个Connection中生产者发送的消息传送给这个Connection中的消费者,默认是false;
- exclusive:设置是否排他,默认是false;
- arguments:设置消费者的其他参数,默认是null;
- callback:设置消费者的回调函数。用来处理RabbitMQ推送过来的消息,比如DefaultConsumer,使用时需要客户端重写(override )其中的方法。
对于消费者需要消费消息一般都是重写handleDelivery方法
public interface Consumer {
//这个方法会在其他方法调用前,返回消费者标签
void handleConsumeOk(String consumerTag);
//取消订阅的时候调用
void handleCancelOk(String consumerTag);
void handleCancel(String consumerTag) throws IOException;
//重写这个方法会在channel和Connection关闭时候调用
void handleShutdownSignal(String consumerTag, ShutdownSignalException sig);
void handleRecoverOk(String consumerTag);
void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException;
}
我们可以通过channel.basicCancel方法来显式地取消一个消费者的订阅,channel.basicCancel(consumerTag);这行代码执行会首先触发handleConsumerOk方法,然后调用handleDelivery方法,最后才出发handleCancelOk方法。
和生产者一样,消费者客户端同样需要考虑线程安全的问题。消费者客户端的这些callback会被分配到与Channel不同的线程池上,这意味着消费者客户端可以安全地调用这些阻塞方法,比如channel.queueDeclare、channel.basicCancel等。
每个Channel都拥有自己独立的线程。最常用的做法是一个Channel对应一个消费者,也就是意味着消费者彼此之间没有任何关联。当然也可以在一个Channel中维持多个消费者,但是要注意一个问题,如果Channel中的一个消费者一直在运行,那么其他消费者的callback会被"耽搁"。
2. 拉模式
拉模式的消费方式,通过channel.basicGet方法可以单条地获取消息,其返回值是GetResponse,Channel类的basicGet方法没有其他重载方法,只有:
GetResponse basicGet(String queueName, boolean autoAck) throws IOException;
其中中queueName代表队列的名称,如果设置autoAck为false,那么同样需要调用channel.basicAck来确认消息己被成功接收。
例子:
GetResponse response = channel.basicGet(QUEUE_NAME, false) ;
System.out.println(new String(response.getBody()));
channel.basicAck(response.getEnvelope().getDeliveryTag(), false);
注意点:推模式和拉模式的对比。Basic.Consume将信道(Channel)直为接收模式,直到取消队列的订阅为止。在接收模式期间,RabbitMQ会不断地推送消息给消费者,当然推送消息的个数还是会受到Basic.Qos的限制.如果只想从队列获得单条消息而不是持续订阅,建议还是使用Basic . Get进行消费.但是不能将Basic.Get放在一个循环里来代替Basic.Consume,这样做会严重影响RabbitMQ的性能.如果要实现高吞吐量,消费者理应使用Basic.Consume方法。
3. 消费信息的确认
为了保证消息从队列可靠地达到消费者,RabbitMQ提供了消息确认机制(message acknowledgement )。消费者在订阅队列时,可以指定autoAck参数,当autoAck等于false时,RabbitMQ会等待消费者显式地回复确认信号后才从内存(或者磁盘)中移去消息(实质上是先打上删除标记,之后再删除)。当autoAck等于t r u e时,RabbitMQ会自把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正地消费到了这些消息。
采用消息确认机制后,只要设置autoAck参数为false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为RabbitMQ会一直等待持有消息直到消费者显式调用Basic.Ack命令为止。当autoAck参数置为false,对于RabbitMQ服务端而言,队列中的消息分成了两个部分:一部分是等待投递给消费者的消息:一部分是己经投递给消费者,但是还没有收到消费者确认信号的消息。如果RabbitMQ一直没有收到消费者的确认信号,并且消费此消息的消费者己经断开连接,则RabbitMQ会安排该消息重新进入队列,等待投递给下一个消费者,当然也有可能还是原来的那个消费者。
boolean autoAck = false;
channel.basicQos(64);//设置客户端最多接受违背ack的信息的个数
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("recieve message :"+new String(body));
try {
TimeUnit.SECONDS.sleep(1); //休眠一秒在返回确定信息
}catch (InterruptedException e){
e.printStackTrace();
}
//这里在消费信息后手动确认已经获取到信息了,然后Broker好删除消息
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//这里设置的自动应答会false
channel.basicConsume(QUEUE_NAME,autoAck,"myConsumerTag",consumer);
4. 消息的拒绝
//拒绝一条消息
void basicReject(long deliveryTag, boolean requeue) throws IOException;
//拒绝批量消息
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;
其中deliveryTag是消息的编号,是一个64位的长整型数值。
如果requeue参数设置为true,RabbitMQ会重新将这条消息存入队列,以便可以发送给下一个订阅的消费者;如果requeue参数设置为false,则RabbitMQ立即会把消息从队列中移除,而不会把它发送给新的消费者。
multiple参数设置为false则表示拒绝编号为deliveryTag的这一条消息,这时候basicNack和basicReject方法一样;multiple参数设置为true则表示拒绝deliveryTag编号之前所有未被当前消费者确认的消息。
注意要点:将channel.basicReject或者channel.basicNack中的requeue设直为f a l s e,可以启用"死信队列"的功能。死信队列可以通过检测被拒绝或者未送达的消息来追踪问题,后面讲。
5. 消息的重新获取
RecoverOk basicRecover() throws IOException;
RecoverOk basicRecover(boolean requeue) throws IOException;
这个channel.basicRecover方法用来请求RabbitMQ重新发送还未被确认的消息。如果requeue参数设置为true,则未被确认的消息会被重新加入到队列中,这样对于同一条消息来说,可能会被分配给与前不同的消费者。如果requeue参数设置为false,那么同一条消息会被分配给与之前相同的消费者。默认情况下,如果不设置requeue这个参数,相当于channel.basicRecover(true),即requeue默认为true。
五.关闭连接
channel.close();
conn.close()
显式地关闭Channel个好习惯,但这不是必须的,在Connection关闭的时候,Channel也会自动关闭。
AMQP协议中的Connection和Channel采用同样的方式来管理网络失败、内部错误和显式地关闭连接。Connection和Channel所具备的生命周期如下所述:
- Open:开启状态,代表当前对象可以使用。
- Closing:正在关闭状态。当前对象被显式地通知调用关闭方法(shutdown),这样就产生了一个关闭请求让其内部对象进行相应的操作,并等待这些关闭操作的完成。
- Closed:已经关闭状态。当前对象已经接收到所有的内部对象已经完成关闭动作的通知,并且其也关闭了自身。Connection和Channel最终都是会成为losed的状态,不论是程序正常调用的关闭方法,或者是客户端的异常,再或者是发生了网络异常。
Connection和Channel中,与关闭相关的方法有:
addShutdownListener(ShutdownListenerlistener) ;
removeShutdownListener(ShutdownListner listener )
当Connection或者Channel的状态转变为Closed的时候会调用ShutdownListener。而且如果将一个ShutdownListener注册到一个己经处于Closed状态的对象(这里特指Connection和Channel对象)时,会立刻调用ShutdownListener。
getCloseReason方法可以让你知道对象关闭的原因;
isOpen方法检测对象当前是否处于开启状态;
close(int closeCode , StringcloseMessage方法显式地通知当前对象执行关闭操作。
可以根据这个添加关闭监听器,检测失败的原因,异常情况,例如:
connection.addShutdownListener(new ShutdownListener() {
@Override
public void shutdownCompleted(ShutdownSignalException e) {
if (e.isHardError()){ //这个可以知道是connection异常还是channel异常
Connection conn = (Connection) e.getReference();
if (!e.isInitiatedByApplication()){
Method reason = e.getReason();
...
}
}else {
Connection conn = (Connection) e.getReference();
}
}
});