RabbiMQ principle and use SpringBoot

RabbiMQ Introduction

Specific code can refer to my GitHub: https://github.com/UniqueDong/springboot-study

First, the use scene

RabbitMQ is a messaging middleware, so the most important role is to: information buffer, asynchronous and decoupling applications.

RabbitMQ is a messaging middleware implement AMQP (Advanced Message Queuing Protocol), and originated in the financial system to store and forward messages in a distributed system, in terms of ease of use, scalability, high availability, and so doing well . RabbitMQ is mainly to achieve two-way decoupling between the system and implementation. When a producer to produce large amounts of data, consumers can not consume fast, we need an intermediate layer. Save this data.

AMQP, namely Advanced Message Queuing Protocol, Advanced Message Queuing Protocol, is an open standard application layer protocol for message-oriented middleware design. Message middleware mainly for decoupling between the components, sender of the message without knowing the user's presence information, and vice versa. AMQP main feature is a message for the queue, the routing (including point and publish / subscribe), reliability and safety. Detailed concept can refer to the official guide RabbitMQ

Second, related concepts

Usually we talk about queue service, there will be three concepts: message's queue, who received the message, RabbitMQ on this basic concept, do a layer of abstraction between the person and message queues, adding switches (the exchange). such message queue and are not directly linked, in turn, becomes the message to the message by the switch, the switch then give the message queue according to the scheduling policy.

So, one of the more important concepts have four, namely: virtual hosts, switches, queues, and bindings.

  • Web Hosting v-host: a virtual host holds a group of switches, queues and bindings. Why do we need multiple virtual hosts it? Very simple, RabbitMQ them, users can only access control granularity in virtual host. So, if you need to access the switch prohibit Group A Group B / queues / bindings, you must create a virtual host for the A and B respectively. Each server has a default RabbitMQ virtual host.
  • Switches: Exchange for forwarding messages, but it does not do storage, if not Queue bind to the Exchange, then it drops off directly over the message sent by the Producer. There is a more important concept here: routing keys . Message to the switch when the opportunity to interact forwarded to the corresponding queue, then what are forwarded to which queue, it is necessary according to the routing keys.
  • Binding: the switch is required and bound to queue, which as shown in FIG.

Switch (Exchange)

The main function of the switch is to receive the message and forwarded to bind the queue, the switch does not store the message after ack mode is enabled, the switch can not find the queue returns an error. Switches There are four types: Direct, topic, Headers and Fanout

  • Direct: direct type of behavior is "first match, then delivery" that is set when a binding. Routing_key , message routing_key match will only be delivered to the bound exchanger queue.
  • Topic: according to the rules of forwarding messages (most flexible)
  • Headers: Set header attribute parameter type switch
  • Fanout: forward the message to all the binding queue

Direct Exchange

Direct Exchange RabbitMQ is the default mode switches, is the simplest mode, according to the key match to find the full text of the queue.
img

The first X - Q1 there is a binding key, the name is orange; X - Q2 have two binding key, the name for the black and green. When routing keys and the message corresponding to the binding key, then the message to know to which queue.

Ps: Why X Q2 to have a black, green, 2 Ge binding key it, a not on line yet? - This is mainly because there may Q3, and Q3 only information accepting black, and Q2 receive information not only black, but also to receive information of green.

Topic Exchange

The wildcard forwards the message to the queue, in this switch, the switch will be bound to the queue, and routing define a pattern, then, is necessary between such wildcard routing mode and routing switches can forward the message matches the key.

  • * (Asterisk) can substitute a word.
  • # (Hash) can substitute for zero or more words.

img

Headers Exchange

headers are matched according to the rules, and compared to the direct use of topic fixedly routing_key, headers matching rule type is a custom.
When the switch queue binding, will also set a key-value rule, the message comprising a set of key-value pair (headers attributes), such as a pair of key-value pairs, or all of the match, the message is delivered to the corresponding queue.

Fanout Exchange

News broadcast mode, that is, we publish-subscribe model. Fanout Exchange message broadcasting mode, regardless of routing or routing mode key, the message sent will bind to it all the queues, if configured routing_key will be ignored.

Message acknowledgment

How news consumer news consumption notice Rabbit success?

ACK messages are received correctly confirm whether each Message must be confirmed (acknowledged), you can go ACK ACK manual or automatic automatic confirmation will be confirmed immediately after the message is sent to the consumer, but there may be lost messages, if the consumer side consumer logic throws an exception, that is, the consumer does not end successfully processed the message, then the equivalent of lost messages if the message has been processed, but the follow-up code throws an exception, then use Spring to manage the consumer side business logic will be rolled back it also caused a meaningful message is lost if the manual confirmation when consumers call ack, nack, reject several methods for confirmation, manual confirmation of some operations can be carried out after a business failure, if the message is not ACK will be sent to If a service under a consumer forget the ACK, the RabbitMQ will not send data to it, because RabbitMQ think ACK mechanism limited processing capacity of the service can also play a limiting role, such as sleep when a message is received a few seconds the message acknowledgment modes are:

  • AcknowledgeMode.NONE: Automatic confirmation
  • AcknowledgeMode.AUTO: confirmation under the circumstances
  • AcknowledgeMode.MANUAL: manual confirmation

SpringBoot integrated RabbitMQ

  1. Configuring pom, mainly to add springboot-starter-amqp support, springboot based version 2.1.4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
  1. Springboot configuration file of yaml

    server:
      servlet:
        context-path: /rabbitmq
      port: 9004
    spring:
      application:
        name: rabbitmq
      rabbitmq:
        host: localhost
        virtual-host: /crawl
        username: xxxx
        password: xxx
        port: 5672
        # 消息失败返回,比如路由不到队列时触发回调
        publisher-returns: true
        # 消息正确发送确认
        publisher-confirms: true
        template:
          retry:
            enabled: true
            initial-interval: 2s
        listener:
          simple:
            # 手动ACK 不开启自动ACK模式,目的是防止报错后未正确处理消息丢失 默认 为 none
            acknowledge-mode: manual

In addition, we need to configure the ACK confirmation callback configuration, by implementing RabbitTemplate.ConfirmCallback interface message is sent to the Broker after the trigger callback, which is the only verify that the correct arrival Exchange.

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * @author lijianqing
 * @version 1.0
 * @ClassName RabbitTemplateConfirmCallback
 * @date 2019/4/23 12:55
 */
@Component
@Slf4j
public class RabbitTemplateConfirmCallback implements RabbitTemplate.ConfirmCallback {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init() {
        //指定 ConfirmCallback
        rabbitTemplate.setConfirmCallback(this);
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        log.info("消息唯一标识:{},确认结果:{},失败原因:{}", correlationData, ack, cause);
    }
}

Failure to return messages, such as routing step to the queue will be triggered if the Western Oce sent to the switch successfully, but no matching queue will trigger a callback

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * @author lijianqing
 * @version 1.0
 * @ClassName RabbitTemplateReturnCallback
 * @date 2019/4/23 12:55
 */
@Component
@Slf4j
public class RabbitTemplateReturnCallback implements RabbitTemplate.ReturnCallback {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init() {
        //指定 ReturnCallback
        rabbitTemplate.setReturnCallback(this);
        rabbitTemplate.setMandatory(true);
    }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.info("消息主体 message : " + message);
        log.info("消息主体 message : " + replyCode);
        log.info("描述:" + replyText);
        log.info("消息使用的交换器 exchange : " + exchange);
        log.info("消息使用的路由键 routing : " + routingKey);
    }
}

A simple start - Simple Queue

As shown below:

"P" is our producer, "C" is our consumers. The middle of the box is a queue - RabbitMQ on behalf of consumers to retain the message buffer.

(P) - > [|||] - >(C)

New SimpleConfig, we create a queue to be served: code is as follows

/**
 * 队列直接投放
 * @author lijianqing
 * @version 1.0
 * @ClassName SimpleConfig
 * @date 2019/4/26 15:11
 */
@Configuration
public class SimpleConfig {
    @Bean
    public Queue simpleQueue() {
        return new Queue("simple");
    }
}

Then create separate message sender and recipient of the message:

  • The message sender
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import zero.springboot.study.rabbitmq.model.User;

import java.util.UUID;

/**
 * @author lijianqing
 * @version 1.0
 * @ClassName HelloSender
 * @date 2019/4/23 11:22
 */
@Component
@Slf4j
public class HelloSender {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send() {
        User user = new User();
        user.setName("青");
        user.setPass("111111");
        //发送消息到hello队列
        log.info("发送消息:{}", user);
        rabbitTemplate.convertAndSend("hello", user, new CorrelationData(UUID.randomUUID().toString()));

        String msg = "hello qing";
        log.info("发送消息:{}", msg);
        rabbitTemplate.convertAndSend("simple", msg);
    }
}
  • The message recipient
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
import zero.springboot.study.rabbitmq.model.User;

import java.io.IOException;

/**
 * 监听hello队列
 *
 * @author lijianqing
 * @version 1.0
 * @ClassName HelloReceiver
 * @date 2019/4/23 11:42
 */
@Component
@Slf4j
@RabbitListener(queues = "simple")
public class HelloReceiver {

    @RabbitHandler
    public void processUser(User user, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        log.info("收到消息:{}", user);
        // 手动ACK
        try {
//            //消息确认,代表消费者确认收到当前消息,语义上表示消费者成功处理了当前消息。
            channel.basicAck(tag, false);
//             代表消费者拒绝一条或者多条消息,第二个参数表示一次是否拒绝多条消息,第三个参数表示是否把当前消息重新入队
//        channel.basicNack(deliveryTag, false, false);

            // 代表消费者拒绝当前消息,第二个参数表示是否把当前消息重新入队
//        channel.basicReject(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @RabbitHandler
    public void processString(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        log.info("收到消息:{}", message);
        // 手动ACK
        try {
//            //消息确认,代表消费者确认收到当前消息,语义上表示消费者成功处理了当前消息。
            channel.basicAck(tag, false);
//             代表消费者拒绝一条或者多条消息,第二个参数表示一次是否拒绝多条消息,第三个参数表示是否把当前消息重新入队
//        channel.basicNack(deliveryTag, false, false);

            // 代表消费者拒绝当前消息,第二个参数表示是否把当前消息重新入队
//        channel.basicReject(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

This realizes a simple message to the queue of the specified pattern. We write a test class

Two, Direct Exchange Mode

Our main configuration of Direct Exchange switch, and the switch is bound to create a queue by routing key

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 *
 * @author lijianqing
 * @version 1.0
 * @ClassName DirectConfig
 * @date 2019/4/23 11:15
 */
@Configuration
public class DirectConfig {

    //队列名字
    public static final String QUEUE_NAME = "direct_name";

    //交换机名称
    public static final String EXCHANGE = "zero-exchange";

    //路由键名称
    public static final String ROUTING_KEY = "routingKey";

    @Bean
    public Queue blueQueue() {
        return new Queue(QUEUE_NAME, true);
    }

    @Bean
    public DirectExchange defaultExchange() {
        return new DirectExchange(EXCHANGE);
    }

    @Bean
    public Binding bindingBlue() {
        return BindingBuilder.bind(blueQueue()).to(defaultExchange()).with(ROUTING_KEY);
    }

}

Next we create producers and consumers

  • Producers
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import zero.springboot.study.rabbitmq.config.DirectConfig;
import zero.springboot.study.rabbitmq.model.User;

import java.util.UUID;

/**
 * @author lijianqing
 * @version 1.0
 * @ClassName HelloSender
 * @date 2019/4/23 11:22
 */
@Component
@Slf4j
public class DirectSender {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send() {
        User user = new User();
        user.setName("青");
        user.setPass("111111");
        //发送消息到hello队列
        log.info("DirectReceiver发送消息:{}", user);
        rabbitTemplate.convertAndSend(DirectConfig.EXCHANGE, DirectConfig.ROUTING_KEY, user, new CorrelationData(UUID.randomUUID().toString()));

        String msg = "hello qing";
        log.info("DirectReceiver发送消息:{}", msg);
        rabbitTemplate.convertAndSend(DirectConfig.EXCHANGE, DirectConfig.ROUTING_KEY, msg);
    }
}
  • consumer
/**
 *
 * @author lijianqing
 * @version 1.0
 * @ClassName HelloReceiver
 * @date 2019/4/23 11:42
 */
@Component
@Slf4j
@RabbitListener(queues = "direct_name")
public class DirectReceiver {

    @RabbitHandler
    public void processUser(User user, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        log.info("DirectReceiver收到消息:{}", user);
        // 手动ACK
        try {
//            //消息确认,代表消费者确认收到当前消息,语义上表示消费者成功处理了当前消息。
            channel.basicAck(tag, false);
//             代表消费者拒绝一条或者多条消息,第二个参数表示一次是否拒绝多条消息,第三个参数表示是否把当前消息重新入队
//        channel.basicNack(deliveryTag, false, false);

            // 代表消费者拒绝当前消息,第二个参数表示是否把当前消息重新入队
//        channel.basicReject(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @RabbitHandler
    public void processString(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        log.info("收到消息:{}", message);
        // 手动ACK
        try {
//            //消息确认,代表消费者确认收到当前消息,语义上表示消费者成功处理了当前消息。
            channel.basicAck(tag, false);
//             代表消费者拒绝一条或者多条消息,第二个参数表示一次是否拒绝多条消息,第三个参数表示是否把当前消息重新入队
//        channel.basicNack(deliveryTag, false, false);

            // 代表消费者拒绝当前消息,第二个参数表示是否把当前消息重新入队
//        channel.basicReject(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Three, Topic Exchange Mode

Create a queue and switches. By matching rules and routing queue bindings on the switch

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 *  queueMessages 匹配topic.#,queueMessage 只匹配 "topic.message"
 *
 * @author lijianqing
 * @version 1.0
 * @ClassName TopicRabbitConfig
 * @date 2019/4/23 15:03
 */
@Configuration
public class TopicRabbitConfig {
    final static String message = "topic.message";
    final static String messages = "topic.messages";

    @Bean
    public Queue queueMessage() {
        return new Queue(TopicRabbitConfig.message);
    }

    @Bean
    public Queue queueMessages() {
        return new Queue(TopicRabbitConfig.messages);
    }

    @Bean
    TopicExchange exchange() {
        return new TopicExchange("topicExchange");
    }

    @Bean
    Binding bindingExchangeMessage(@Qualifier("queueMessage") Queue queueMessage, TopicExchange exchange) {
        return BindingBuilder.bind(queueMessage).to(exchange).with("topic.message");
    }

    @Bean
    Binding bindingExchangeMessages(@Qualifier("queueMessages") Queue queueMessages, TopicExchange exchange) {
        return BindingBuilder.bind(queueMessages).to(exchange).with("topic.#");
    }

}
  • Creating Producer
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author lijianqing
 * @version 1.0
 * @ClassName TopicSender
 * @date 2019/4/23 15:10
 */
@Component
@Slf4j
public class TopicSender {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 匹配topic.message,两个队列都会收到
     */
    public void send1() {
        String context = "hi, i am message 1";
        log.info("主题发送 : {}" , context);
        rabbitTemplate.convertAndSend("topicExchange", "topic.message", context);
    }

    /**
     * 匹配topic.messages
     */
    public void send2() {
        String context = "hi, i am messages 2";
        log.info("主题发送 : {}" , context);
        rabbitTemplate.convertAndSend("topicExchange", "topic.messages", context);
    }
}
  • Creating consumer, here we are two consumer create queues
@Component
@RabbitListener(queues = "topic.message")
@Slf4j
public class TopicReceiver {

    @RabbitHandler
    public void process(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        log.info("topic.message Receiver1  {}: ", message);
        // 手动ACK
        try {
//            //消息确认,代表消费者确认收到当前消息,语义上表示消费者成功处理了当前消息。
            channel.basicAck(tag, false);
//             代表消费者拒绝一条或者多条消息,第二个参数表示一次是否拒绝多条消息,第三个参数表示是否把当前消息重新入队
//        channel.basicNack(deliveryTag, false, false);

            // 代表消费者拒绝当前消息,第二个参数表示是否把当前消息重新入队
//        channel.basicReject(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

The second consumer

@Component
@RabbitListener(queues = "topic.messages")
@Slf4j
public class TopicReceiver2 {

    @RabbitHandler
    public void process(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        log.info("topic.messages Receiver2  : {}", message);
        // 手动ACK
        try {
//            //消息确认,代表消费者确认收到当前消息,语义上表示消费者成功处理了当前消息。
            channel.basicAck(tag, false);
//             代表消费者拒绝一条或者多条消息,第二个参数表示一次是否拒绝多条消息,第三个参数表示是否把当前消息重新入队
//        channel.basicNack(deliveryTag, false, false);

            // 代表消费者拒绝当前消息,第二个参数表示是否把当前消息重新入队
//        channel.basicReject(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Four, Fanout mode

That is, publish, subscribe. All bound to the switch receives a message queue, any character specified routing key transmitting side will be ignored

Configure the switch queue

@Configuration
public class FanoutRabbitConfig {
    @Bean
    public Queue AMessage() {
        return new Queue("fanout.A");
    }

    @Bean
    public Queue BMessage() {
        return new Queue("fanout.B");
    }

    @Bean
    public Queue CMessage() {
        return new Queue("fanout.C");
    }

    @Bean
    FanoutExchange fanoutExchange() {
        return new FanoutExchange("fanoutExchange");
    }
    @Bean
    Binding bindingExchangeA(Queue AMessage, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(AMessage).to(fanoutExchange);
    }

    @Bean
    Binding bindingExchangeB(Queue BMessage, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(BMessage).to(fanoutExchange);
    }

    @Bean
    Binding bindingExchangeC(Queue CMessage, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(CMessage).to(fanoutExchange);
    }
}
  • Creating Sender
@Component
@Slf4j
public class FanoutSender {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send() {
        String context = "hi, fanout msg ";
        rabbitTemplate.convertAndSend("fanoutExchange", null, context);
    }

}
  • Creating A, B, C queue consumers
@Component
@RabbitListener(queues = "fanout.A")
@Slf4j
public class FanoutReceiverA {

    @RabbitHandler
    public void process(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        log.info("fanout Receiver A  : {}" , message);
        // 手动ACK
        try {
//            //消息确认,代表消费者确认收到当前消息,语义上表示消费者成功处理了当前消息。
            channel.basicAck(tag, false);
//             代表消费者拒绝一条或者多条消息,第二个参数表示一次是否拒绝多条消息,第三个参数表示是否把当前消息重新入队
//        channel.basicNack(deliveryTag, false, false);

            // 代表消费者拒绝当前消息,第二个参数表示是否把当前消息重新入队
//        channel.basicReject(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

The remaining B, C not repeat posted.

unit test

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import zero.springboot.study.rabbitmq.direct.DirectSender;
import zero.springboot.study.rabbitmq.fanout.FanoutSender;
import zero.springboot.study.rabbitmq.simple.HelloSender;
import zero.springboot.study.rabbitmq.topic.TopicSender;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RabbitmqApplication.class)
public class RabbitmqApplicationTests {

    @Autowired
    private DirectSender directSender;

    @Autowired
    private TopicSender topicSender;

    @Autowired
    private FanoutSender fanoutSender;

    @Autowired
    private HelloSender helloSender;

    @Test
    public void testDirect() {
        directSender.send();
    }

    @Test
    public void topic1() {
        topicSender.send1();
    }

    @Test
    public void topic2() {
        topicSender.send2();
    }

    @Test
    public void testFanout() {
        fanoutSender.send();
    }

    @Test
    public void testSimple() {
        helloSender.send();
    }

}

All code is on github my share, you can view the specific comments . github rabbitmq module

Like your point-and-forward is my greatest sure.

I welcome attention to the micro-channel public number JavaStorm_ Joint communications scan code search pattern - green micro letter Standard Edition .png

Guess you like

Origin www.cnblogs.com/uniqueDong/p/10944659.html