文章目录

  • 1、基本使用Demo
  • 2、连接RabbitMQ
  • 3、使用交换器和队列
  • 3.1 exchangeDeclare方法
  • 3.2 queueDeclare方法
  • 3.3 queueBind方法
  • 3.4 exchangeBind方法
  • 3.5 何时创建
  • 4、发送消息
  • 5、消费消息
  • 5.1 推模式
  • basicConsume方法:
  • 5.2 拉模式
  • 6、消费端的确认与拒绝
  • 6.1 确认消息
  • 6.2 明确拒绝消息
  • 7、关闭连接


1、基本使用Demo

<!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
<dependency>
	<groupid>com.rabbitmq</groupId>
	<artifactId>amqp-client</artifactId>
	<version>4.2.1</version> 
</dependency>

生产者客户端代码:

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class RabbitProducer {
    private static final String EXCHANGE_NAME = "exchange demo";
    private static final String ROUTING_KEY = "routingkey demo";
    private static final String QUEUE_NAME = "queue demo";
    private static final String IP_ADDRESS ="192.168.0.2";
    private static final int PORT = 5672; //RabbitMQ服务端默认端口号为5672

    public static void main(String[] args) throws IOException,TimeoutException, InterruptedException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(IP_ADDRESS);
        factory.setPort(PORT);
        factory.setUsername("root");
        factory.setPassword("root123");
        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);
        //发送一条持久化的消息: hello world!
        String message = "Hello World!";
        channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
        //关闭资源
        channel.close();
        connection.close();
    }
}

消费者客户端代码:

import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class RabbitConsumer {
    private static final String QUEUE_NAME = "queue demo";
    private static final String IP_ADDRESS ="192.168.0.2";
    private static final int PORT = 5672;

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException{
        Address[] addresses = new Address[]{
                new Address (IP_ADDRESS, PORT)
        };
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("root");
        factory.setPassword("root123");
        //这里的连接方式与生产者的demo略有不同,注意辨别区别
        Connection connection = factory.newConnection(addresses) ; //创建连接
        final Channel channel = connection.createChannel() ;//创建信道
        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("recv message: " + new String(body));
                try{
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        channel.basicConsume(QUEUE_NAME, consumer);
        //等待回调函数执行完毕之后,关闭资源
        TimeUnit.SECONDS.sleep (5);
        channel.close();
        connection.close();
    }
}

2、连接RabbitMQ

指定参数方式:

ConnectionFactory factory = new ConnectionFactory();
     factory.setUsername(USERNAME);
     factory.setPassword(PASSWORD);
     factory.setVirtualHost(virtualHost);
     factory.setHost(IP_ADDRESS);
     factory.setPort(PORT);
     Connection conn = factory.newConnection();

用URI方式:

ConnectionFactory factory = new ConnectionFactory();
     factory.setUri("amqp://userName:password@ipAddress:portNumber/virtualHost");
     Connection conn = factory.newConnection();

Connection接口被用来创建一个Channel:

Channel channel = conn.createChannel();

在创建之后,Channel可以用来发送或者接收消息了

注意:Connection可以用来创建多个Channel实例,但是Channel实例不能在线程间共享,应用程序应该为每一个线程开辟一个Channel。某些情况下Channel的操作可以并发运行,但是在其他情况下会导致在网络上出现错误的通信帧交错,同时也会影响发送方确认(publisher confirm)机制的运行,所以多线程间共享Channel实例是非线程安全的。

3、使用交换器和队列

3.1 exchangeDeclare方法

Exchange.DeclareOk exchangeDeclare (String exchange, string type, boolean durable, boolean autoDelete, boolean internal, 
									Map<String, object> arguments) throws IOException;

这个方法的返回值是Exchange.DeclareOK,用来标识成功声明了一个交换器。

各个参数详细说明如下所述。

  • exchange:交换器的名称。
  • type:交换器的类型,常见的如fanout、direct、topic。
  • durable:设置是否持久化。durable设置为true表示持久化,反之是非持久化。持久化可以将交换器存盘,在服务器重启的时候不会丢失相关信息。
  • autoDelete:设置是否自动删除。autoDelete设置为true则表示自动删除。自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑。注意不能错误地把这个参数理解为:“当与此交换器连接的客户端都断开时,RabbitMQ会自动删除本交换器”。
  • internal:设置是否是内置的。如果设置为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式。
  • argument:其他一些结构化参数,比如alternate-exchange(有关alternate-exchange的详情可以参考4.1.3节)。

3.2 queueDeclare方法

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-LhQz1gv3GhDOv8PIDabOXA名称,这种队列也称之为匿名队列)、排他的、自动删除的、非持久化的队列。

方法的参数详细说明如下所述。

  • queue:队列的名称。
  • durable:设置是否持久化。为true则设置队列为持久化。持久化的队列会存盘,在服务器重启的时候可以保证不丢失相关信息。
  • exclusive:设置是否排他。为true则设置队列为排他的。如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。这里需要注意三点:排他队列是基于连接(Connection)可见的,同一个连接的不同信道(Channel)是可以同时访问同一连接创建的排他队列;“首次”是指如果一个连接已经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同;即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除,这种队列适用于一个客户端同时发送和读取消息的应用场景。
  • autoDelete:设置是否自动删除。为true则设置队列为自动删除。自动删除的前提是:至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除。不能把这个参数错误地理解为:“当连接到此队列的所有客户端断开时,这个队列自动删除”,因为生产者客户端创建这个队列,或者没有消费者客户端与这个队列连接时,都不会自动删除这个队列。
  • arguments:设置队列的其他一些参数,如x-message-ttl、x-expires、x-max-length、x-max-length-bytes、x-dead-letter-exchange、x-deadletter-routing-key、x-max-priority等。

注意要点:
生产者和消费者都能够使用queueDeclare来声明一个队列,但是如果消费者在同一个信道上订阅了另一个队列,就无法再声明队列了。必须先取消订阅,然后将信道置为“传输”模式,之后才能声明队列。

3.3 queueBind方法

Queue.BindOk queueBind(String queue, String exchange, String routingKey, Map<String, Object> arguments)
					 throws IOException;

方法中涉及的参数详解。

  • queue:队列名称;
  • exchange:交换器的名称;
  • routingKey:用来绑定队列和交换器的路由键;
  • argument:定义绑定的一些参数。

不仅可以将队列和交换器绑定起来,也可以将已经被绑定的队列和交换器进行解绑。

Queue.UnbindOk queueUnbind(String queue, String exchange, String routingKey, Map<String, Object> arguments)
							 throws IOException;

3.4 exchangeBind方法

Exchange.BindOk exchangeBind(String destination, String source, String routingKey, Map<String, Object> arguments)
							 throws IOException;

生产者发送消息至交换器source中,交换器source根据路由键找到与其匹配的另一个交换器destination,并把消息转发到destination中,进而存储在destination绑定的队列queue中,可参考图交换器与交换器绑定:

搭建rabbitmq项目 java rabbitmq开发_搭建rabbitmq项目 java

3.5 何时创建

RabbitMQ的消息存储在队列中,交换器的使用并不真正耗费服务器的性能,而队列会。

如果业务本身在架构设计之初已经充分地预估了队列的使用情况,完全可以在业务程序上线之前在服务器上创建好,这样业务程序也可以免去声明的过程,直接使用即可。预先创建好资源还有一个好处是,可以确保交换器和队列之间正确地绑定匹配。

至于是使用预先分配创建资源的静态方式还是动态的创建方式,需要从业务逻辑本身、公司运维体系和公司硬件资源等方面考虑。

4、发送消息

如果要发送一个消息,可以使用Channel类的basicPublish方法。

void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, 
					BasicProperties props, byte[] body) throws IOException;

对应的具体参数解释如下所述。

  • exchange:交换器的名称,指明消息需要发送到哪个交换器中。如果设置为空字符串,则消息会被发送到RabbitMQ默认的交换器中。
  • routingKey:路由键,交换器根据路由键将消息存储到相应的队列之中。
  • props:消息的基本属性集,其包含14个属性成员,分别有contentType、contentEncoding、headers(Map<String,Object>)、deliveryMode、priority、correlationId、replyTo、expiration、messageId、timestamp、type、userId、appId、clusterId。
  • byte[] body:消息体(payload),真正需要发送的消息。
  • mandatory:当mandatory参数设为true时,交换器无法根据自身的类型和路由键找到一个符合条件的队列,那么RabbitMQ会调用Basic.Return命令将消息返回给生产者。当mandatory参数设置为false时,出现上述情形,则消息直接被丢弃。
    那么生产者如何获取到没有被正确路由到合适队列的消息呢?这时候可以通过调用channel.addReturnListener来添加ReturnListener监听器实现。如下图所示:
  • 搭建rabbitmq项目 java rabbitmq开发_客户端_02

  • immediate:RabbitMQ 3.0版本开始去掉了对immediate参数的支持,对此RabbitMQ官方解释是:immediate参数会影响镜像队列的性能,增加了代码复杂性,建议采用TTL(过期时间)和DLX(死信队列)的方法替代。immediate参数告诉服务器,如果该消息关联的队列上有消费者,则立刻投递;如果所有匹配的队列上都没有消费者,则直接将消息返还给生产者。

发送消息代码示例:

channel.basicPublish(exchangeName, routinqkey, mandatory, MessageProperties.PERSISTENT_TEXT_PLAIN, messageBodyBytes);

这条消息的投递模式(delivery mode)设置为2,即消息会被持久化(即存入磁盘)在服务器中。同时这条消息的优先级(priority)设置为0,content-type为“text/plain”.

可以自己设定消息的属性:

搭建rabbitmq项目 java rabbitmq开发_客户端_03

也可以发送一条带有headers的消息:

搭建rabbitmq项目 java rabbitmq开发_搭建rabbitmq项目 java_04


还可以发送一条带有过期时间(expiration)的消息:

搭建rabbitmq项目 java rabbitmq开发_搭建rabbitmq项目 java_05

5、消费消息

RabbitMQ的消费模式分两种:推(Push)模式和拉(Pull)模式。推模式采用Basic.Consume进行消费,而拉模式则是调用Basic.Get进行消费。

5.1 推模式

在推模式中,可以通过持续订阅的方式来消费消息,使用到的相关类有:

import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;

接收消息一般通过实现Consumer接口或者继承DefaultConsumer类来实现。当调用与Consumer相关的API方法时,不同的订阅采用不同的消费者标签(consumerTag)来区分彼此,在同一个Channel中的消费者也需要通过唯一的消费者标签以作区分.

关键消费代码如下所示:

boolean autoAck = false;
channel.basicQos(64);
channel.basicConsume (queueName, autoAck, "myConsumerTag", new DefaultConsumer(channel){
    @Override
    public void handleDelivery(String consumerTaq, Envelope envelope, AMQP.BasicProperties properties, byte [] body)
            throws IOException {
        String routingkey = envelope.getRoutingKey();
        String contentType = properties.getContentType();
        long deliveryTag = envelope.getDeliveryTag();
        // (process the message components here ..)
        channel.basicAck(deliveryTag, false);
    }
});

注意,上面代码中显式地设置autoAck为false,然后在接收到消息之后进行显式ack操作(channel.basicAck),对于消费者来说这个设置是非常必要的,可以防止消息不必要地丢失。

basicConsume方法:

String basicConsume(String queue, boolean autoAck, String consumerTag, boolean noLocal, boolean exclusive,
 						Map<String, Object> arguments, Consumer callback) throws IOException;

其对应的参数说明如下所述。

  • queue:队列的名称;
  • autoAck:设置是否自动确认。建议设成false,即不自动确认;
  • consumerTag:消费者标签,用来区分多个消费者;
  • noLocal:设置为true则表示不能将同一个Connection中生产者发送的消息传送给这个Connection中的消费者;
  • exclusive:设置是否排他;
  • arguments:设置消费者的其他参数;
  • callback:设置消费者的回调函数。用来处理RabbitMQ推送过来的消息,比如DefaultConsumer,使用时需要客户端重写(override)其中的方法。对于消费者客户端来说重写handleDelivery方法是十分方便的。更复杂的消费者客户端会重写更多的方法。

5.2 拉模式

通过channel.basicGet方法可以单条地获取消息,其返回值是GetRespone。Channel类的basicGet方法没有其他重载方法,只有:

GetResponse basicGet(String queue, boolean autoAck) throws IOException;

其中queue代表队列的名称,如果设置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方法。

6、消费端的确认与拒绝

为了保证消息从队列可靠地达到消费者, RabbitMQ 提供了消息确认机制(message acknowledgement) 。

6.1 确认消息

消费者在订阅队列时,可以指定 autoAck 参数

  • 当 autoAck 等于 false 时, RabbitMQ 会等待消费者显式地回复确认信号后才从内存(或者磁盘)中移去消息(实质上 是先打上删除标记,之后再删除) 。
  • 当 autoAck 等于 true 时, RabbitMQ 会自动把发送出去的 消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正地消费到了这些消息 。

当 autoAck 参数置为 false,对于 RabbitMQ 服务端而言 ,队列中的消息分成了两个部分 : 一部分是等待投递给消费者的消息:一部分是己经投递给消费者,但是还没有收到消费者确认 信号的消息。 如果 RabbitMQ 一直没有收到消费者的确认信号,并且消费此消息的消费者己经断开连接,则 RabbitMQ 会安排该消息重新进入队列,等待投递给下一个消费者,当然也有可能还是原来的那个消费者。

RabbitMQ 不会为未确认的消息设置过期时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否己经断开,这么设计的原因是 RabbitMQ 允许消费者消费一条消息的时间可以很久很久。

6.2 明确拒绝消息

Basic.Reject命令一次只能拒绝一条消息,如果想要批量拒绝消息,则可以使用Basic.Nack这个命令。

void basicReject(long deliveryTag, boolean requeue) throws IOException;
  • deliveryTag可以看作消息的编号,它是一个64位的长整型值,最大值是9223372036854775807。
  • 如果requeue参数设置为true,则RabbitMQ会重新将这条消息存入队列,以便可以发送给下一个订阅的消费者;如果requeue参数设置为false,则RabbitMQ立即会把消息从队列中移除,而不会把它发送给新的消费者。
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throwsIOException;
  • 其中deliveryTag和requeue的含义可以参考basicReject方法。
  • multiple参数设置为false则表示拒绝编号为deliveryTag的这一条消息,这时候basicNack和basicReject方法一样;multiple参数设置为true则表示拒绝deliveryTag编号之前所有未被当前消费者确认的消息。

注意要点
将channel.basicReject或者channel.basicNack中的requeue设置为false,可以启用“死信队列”的功能。死信队列可以通过检测被拒绝或者未送达的消息来追踪问题。

Basic.RecoverOk basicRecover(boolean requeue) throws IOException;

这个channel.basicRecover方法用来请求RabbitMQ重新发送还未被确认的消息。如果requeue参数设置为true,则未被确认的消息会被重新加入到队列中,这样对于同一条消息来说,可能会被分配给与之前不同的消费者。如果requeue参数设置为false,那么同一条消息会被分配给与之前相同的消费者。默认情况下,如果不设置requeue这个参数,相当于channel.basicRecover(true),即requeue默认为true。

7、关闭连接

在应用程序使用完之后,需要关闭连接,释放资源:

channel.close();
conn.close();

显式地关闭Channel是个好习惯,但这不是必须的,在Connection关闭的时候,Channel也会自动关闭。

Connection和Channel所具备的生命周期:

  • Open:开启状态,代表当前对象可以使用。
  • Closing:正在关闭状态。当前对象被显式地通知调用关闭方法(shutdown),这样就产生了一个关闭请求让其内部对象进行相应的操作,并等待这些关闭操作的完成。
  • Closed:已经关闭状态。当前对象已经接收到所有的内部对象已完成关闭动作的通知,并且其也关闭了自身。

getCloseReason方法可以让你知道对象关闭的原因;isOpen方法检测对象当前是否处于开启状态(不适合在生产用,用于测试);close(int closeCode, String closeMessage)方法显式地通知当前对象执行关闭操作。

在Connection和Channel中,与关闭相关的方法有addShutdownListener(ShutdownListener listener)removeShutdownListener (ShutdownListner listener)。当Connection或者Channel的状态转变为Closed的时候会调用ShutdownListener。而且如果将一个ShutdownListener注册到一个已经处于Closed状态的对象(这里特指Connection和Channel对象)时,会立刻调用ShutdownListener。

搭建rabbitmq项目 java rabbitmq开发_客户端_06


当触发ShutdownListener的时候,就可以获取到ShutdownSignalException,这个ShutdownSignalException包含了关闭的原因,这里原因也可以通过调用前面所提及的getCloseReason方法获取。