2
头图
为方便更好交流,可关注公众号:Java课代表,每日一更,等你来呦!

4 路由(Routing)

上一篇教程中我们创建了一个简易的日志系统。可以将日志消息广播给多个接收者。

本教程中,我们将给它添加一个新特性——让单独订阅某一部分消息(子集)成为可能。比如,我们只把严重错误写入到磁盘文件中(只保存严重错误日志可以节省磁盘),同时仍然将所有日志都在终端输出。

绑定(Bindings)

在前面的例子中,我们已经创建了绑定,你可能还记得如下代码:

channel.queueBind(queueName, EXCHANGE_NAME, "");

绑定是交换(exchange)和队列(queue)之间的一种关系。可以简单地理解为:这个队列对来自这个交换的消息感兴趣。

绑定可以有一个 routingKey参数。为了避免和basic_publish方法的参数混淆(课代表注:Channel#queueBind(String queue, String exchange, String routingKey)方法的routingKey参数用于实现绑定关系,而Channel#basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)方法的routingKey用来路由消息),我们将称其为binding key

channel.queueBind(queueName, EXCHANGE_NAME, "black");

binding key 的含义取决于交换的类型。我们前面使用过的fanout类型的交换会忽略它的值。

Direct交换(Direct exchange)

我们前面教程中的日志记录系统会广播所有消息给所有消费者。我们希望可以根据消息的紧急性进行过滤。比如,希望某个应用只记录严重错误到磁盘,不记录警告和信息类日志消息以节约磁盘。

之前我们使用的fanout交换灵活性并不高——他只是无脑广播。

接下来将使用direct exchange替代, direct exchange的路由算法很简单——消息会进入那个队列的binding key与消息的routing key完全匹配的队列。

为了阐明该点,考虑如下设置:

在上图的设置中,可以看到direct exchange X 绑定了两个queue.Q1使用orange 作为 binding keyQ2有两个绑定,一个是black,另一个是green.

在该设置下,使用orange作为routing key的消息,发送到exchange后将被路由到Q1.使用blackgreen作为routing key的消息将被路由到Q2。其他消息将会被丢弃。

多重绑定(Multiple bindings)

使用相同的binding key绑定到多个queue是完全合法的。在上图中,我们将XQ1通过black作为binding key 绑定起来。这样一来,direct exchange会像fanout一样广播消息到所有匹配的队列,(课代表注:fanout是无脑广播到所有queuedirect是发送给所有匹配的queue)。

发送日志(Emitting logs)

我们将把这个模型应用到日志系统上。使用direct exchange替换fanout来发送消息。我们将提供日志级别作为routing key。这样一来,接收程序就可以根据他想要的级别来接收消息。下面先来看一下发送日志。

跟之前一样,首先要创建一个exchange:

channel.exchangeDeclare(EXCHANGE_NAME, "direct");

然后就可以发送消息了:

channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes());

为了简化起见,我们假设日志级别分为:'info', 'warning', 'error'。

订阅(Subscribing)

接收消息程序和先前教程中的一样正常工作,不过有一点例外,我们需要为感兴趣的日志级别创建绑定。

String queueName = channel.queueDeclare().getQueue();

for(String severity : argv){
  channel.queueBind(queueName, EXCHANGE_NAME, severity);
}

代码整合(Putting it all together)

EmitLogDirect.java 完整代码:

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public class EmitLogDirect {

  private static final String EXCHANGE_NAME = "direct_logs";

  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    try (Connection connection = factory.newConnection();
         Channel channel = connection.createChannel()) {
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");

        String severity = getSeverity(argv);
        String message = getMessage(argv);

        channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes("UTF-8"));
        System.out.println(" [x] Sent '" + severity + "':'" + message + "'");
    }
  }
  //..
}

ReceiveLogsDirect.java完整代码:

import com.rabbitmq.client.*;

public class ReceiveLogsDirect {

  private static final String EXCHANGE_NAME = "direct_logs";

  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();

    channel.exchangeDeclare(EXCHANGE_NAME, "direct");
    String queueName = channel.queueDeclare().getQueue();

    if (argv.length < 1) {
        System.err.println("Usage: ReceiveLogsDirect [info] [warning] [error]");
        System.exit(1);
    }

    for (String severity : argv) {
        channel.queueBind(queueName, EXCHANGE_NAME, severity);
    }
    System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

    DeliverCallback deliverCallback = (consumerTag, delivery) -> {
        String message = new String(delivery.getBody(), "UTF-8");
        System.out.println(" [x] Received '" +
            delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
    };
    channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
  }
}

跟前面一样编译(参考第一节的编译和classpath设置)。为了使用方便,我们在运行例子时使用环境变量 $CP(Windows下是%CP%)作为classpath。

javac -cp $CP ReceiveLogsDirect.java EmitLogDirect.java

如果你只想保存 'warning' 和'error' (不想要 'info') 级别的日志到文件,只需要打开终端,输入:

java -cp $CP ReceiveLogsDirect warning error > logs_from_rabbit.log

如果想在屏幕上看到所有消息,打开新终端,输入:

java -cp $CP ReceiveLogsDirect info warning error
# => [*] Waiting for logs. To exit press CTRL+C

最后作为例子,发送一个'error'级别的日志消息:

java -cp $CP EmitLogDirect error "Run. Run. Or it will explode."
# => [x] Sent 'error':'Run. Run. Or it will explode.'

(完整源码参见 (EmitLogDirect.java source)(ReceiveLogsDirect.java source))

继续学习第5篇教程,了解如何基于模式来侦听消息。


【推荐阅读】

RabbitMQ教程 3.发布/订阅(Publish/Subscribe)
RabbitMQ教程 2.工作队列(Work Queue)
RabbitMQ教程 1.“Hello World”
Freemarker 教程(一)-模板开发手册
下载的附件名总乱码?你该去读一下 RFC 文档了!
深入浅出 MySQL 优先队列(你一定会踩到的order by limit 问题)


码字不易,欢迎点赞关注和分享。
搜索:【Java课代表】,关注公众号,及时获取更多Java干货。


Java课代表
640 声望1k 粉丝

JavaWeb一线开发,5年编程经验