Docker下RabbitMQ四部曲之三:细说java开发

释放双眼,带上耳机,听听看~!

本文是《Docker下RabbitMQ四部曲》系列的第三篇,实战两个基于SpringBoot的工程,分别用来生产和消费RabbitMQ消息;

本文内容简述

今天的实战由以下几部分组成:

  1. 开发SpringBoot工程rabbitmqproducer,用来生产消息;
  2. 分析docker-compose.yml中对rabbitmqproducer镜像的使用;
  3. 开发SpringBoot工程rabbitmqconsumer,用来消费消息;
  4. 分析docker-compose.yml中对rabbitmqconsumer镜像的使用;

源码下载

您可以在GitHub下载本文涉及到的文件和源码,地址和链接信息如下表所示:

项目主页
https://github.com/zq2599/blog_demos
该项目在GitHub上的主页
git仓库地址(https)
https://github.com/zq2599/blog_demos.git
该项目源码的仓库地址,https协议
git仓库地址(ssh)
git@github.com:zq2599/blog_demos.git
该项目源码的仓库地址,ssh协议

这个git项目中有多个文件夹,本章创建的两个工程分别在
rabbitmqproducer、rabbitmqconsumer这两个文件夹下,如下图红框所示:

如何将SpringBoot的web工程制作成Docker镜像

如果您想了解制作Docker镜像的更多细节,请参考以下三篇文章:

  1. 《maven构建docker镜像三部曲之一:准备环境》;
  2. 《maven构建docker镜像三部曲之二:编码和构建镜像》;
  3. 《maven构建docker镜像三部曲之三:推送到远程仓库(内网和阿里云)》;

生产消息的rabbitmqproducer

rabbitmqproducer是个普通的SpringBoot工程,pom.xml内容如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
1<?xml version="1.0" encoding="UTF-8"?>
2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4   <modelVersion>4.0.0</modelVersion>
5
6   <groupId>com.bolingcavalry</groupId>
7   <artifactId>rabbitmqproducer</artifactId>
8   <version>0.0.1-SNAPSHOT</version>
9   <packaging>jar</packaging>
10
11  <name>rabbitmqproducer</name>
12  <description>Demo project for Spring Boot</description>
13
14  <parent>
15      <groupId>org.springframework.boot</groupId>
16      <artifactId>spring-boot-starter-parent</artifactId>
17      <version>2.0.1.RELEASE</version>
18      <relativePath/> <!-- lookup parent from repository -->
19  </parent>
20
21  <properties>
22      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
23      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
24      <java.version>1.8</java.version>
25  </properties>
26
27  <dependencies>
28      <dependency>
29          <groupId>org.springframework.boot</groupId>
30          <artifactId>spring-boot-starter-amqp</artifactId>
31      </dependency>
32      <dependency>
33          <groupId>org.springframework.boot</groupId>
34          <artifactId>spring-boot-starter-web</artifactId>
35      </dependency>
36
37      <dependency>
38          <groupId>org.springframework.boot</groupId>
39          <artifactId>spring-boot-starter-test</artifactId>
40          <scope>test</scope>
41      </dependency>
42  </dependencies>
43
44  <build>
45      <plugins>
46          <plugin>
47              <groupId>org.springframework.boot</groupId>
48              <artifactId>spring-boot-maven-plugin</artifactId>
49          </plugin>
50          <plugin>
51              <groupId>com.spotify</groupId>
52              <artifactId>docker-maven-plugin</artifactId>
53              <version>0.4.12</version>
54              <!--docker镜像相关的配置信息-->
55              <configuration>
56                  <!--镜像名,这里用工程名-->
57                  <imageName>bolingcavalry/${project.artifactId}</imageName>
58                  <!--TAG,这里用工程版本号-->
59                  <imageTags>
60                      <imageTag>${project.version}</imageTag>
61                  </imageTags>
62                  <!--镜像的FROM,使用java官方镜像-->
63                  <baseImage>java:8u111-jdk</baseImage>
64                  <!--该镜像的容器启动后,直接运行spring boot工程-->
65                  <entryPoint>["java", "-jar", "/${project.build.finalName}.jar"]</entryPoint>
66                  <!--构建镜像的配置信息-->
67                  <resources>
68                      <resource>
69                          <targetPath>/</targetPath>
70                          <directory>${project.build.directory}</directory>
71                          <include>${project.build.finalName}.jar</include>
72                      </resource>
73                  </resources>
74              </configuration>
75          </plugin>
76      </plugins>
77  </build>
78</project>
79
80

上述poml.xml中,有以下两点要注意:

  1. 依赖

spring-boot-starter-amqp这个starter,用于RabbitMQ;

  1. 使用插件

docker-maven-plugin将工程制作为docker镜像;

接下来看看关键源码:

  1. application.properties文件内容如下,是关于RabbitMQ的vhost和交换机名称的配置:


1
2
3
4
1mq.rabbit.virtualHost=/
2mq.rabbit.exchange.name=bolingcavalryFanoutExchange
3
4
  1. RabbitConfig类加了

@Configuration注解,表示这是个配置类,代码如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1@Bean(name = "connectionFactory")
2    public ConnectionFactory connectionFactory() {
3        CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
4
5        connectionFactory.setUsername(username);
6        connectionFactory.setPassword(password);
7        connectionFactory.setVirtualHost(mqRabbitVirtualHost);
8        connectionFactory.setPublisherConfirms(true);
9
10        //该方法配置多个host,在当前连接host down掉的时候会自动去重连后面的host
11        connectionFactory.setAddresses(address);
12
13        return connectionFactory;
14    }
15
16    @Bean
17    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
18    //必须是prototype类型
19    public RabbitTemplate rabbitTemplate() {
20        RabbitTemplate template = new RabbitTemplate(connectionFactory());
21        return template;
22    }
23
24    //Fanout交换器
25    @Bean
26    FanoutExchange fanoutExchange() {
27        return new FanoutExchange(exchangeName);
28    }
29
30

上述代码将RabbitTemplate对象和交换机对象的实例化分别通过两个加了@Bean注解的方法实现;
3. RabbitConfig类中有三个@Value注解配置的成员变量:address、username、password,这三个配置对应的值并没有写在application.properties中,因此SpringBoot会去系统环境变量中查找,找到后设置给它们;
4. SendController类,在收到http请求后,会向RabbitMQ发送一条消息,代码如下,调用rabbitTemplate.convertAndSend即可发送:


1
2
3
4
5
6
7
8
9
10
1@RequestMapping(value = "/send/{name}/{message}", method = RequestMethod.GET)
2    public @ResponseBody String send(@PathVariable("name") final String name, @PathVariable("message") final String message) {
3        SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
4        String timeStr = simpleDateFormat.format(new Date());
5        String sendMessage = "hello, " + name + ", " + message  + ", " + timeStr;
6        rabbitTemplate.convertAndSend(exchangeName,"", sendMessage);
7        return "send message to [" +  name + "] success (" + timeStr + ")";
8    }
9
10

以上就是消息生产者的功能代码,在pom.xml所在目录下执行
mvn clean package -U -DskipTests docker:build即可编译构建工程并生成docker镜像文件;

接下来我们看看docker-compose.yml中是如何使用这个镜像的;

docker-compose.yml中对rabbitmqproducer镜像的使用

在集群版的docker-compose.yml文件中,关于镜像的配置如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1producer:
2    image: bolingcavalry/rabbitmqproducer:0.0.2-SNAPSHOT
3    hostname: producer
4    depends_on:
5      - rabbit3
6    links:
7      - rabbit1:rabbitmqhost
8    ports:
9      - "18080:8080"
10    environment:
11      - mq.rabbit.address=rabbitmqhost:5672
12      - mq.rabbit.username=admin
13      - mq.rabbit.password=888888
14
15

这些配置中有以下几点需要注意:

  1. links参数将rabbit1容器对应为别名rabbitmqhost,在environment参数中,mq.rabbit.address等于rabbitmqhost:5672,也就是rabbit1容器的IP地址;
  2. mq.rabbit.address、mq.rabbit.username、mq.rabbit.password都被设置到容器的环境变量中,因此RabbitConfig.java中的address、username、password就会被设置为mq.rabbit.address、mq.rabbit.username、mq.rabbit.password对应的值;
  3. 将当前电脑的18080端口映射到8080端口,假设当前电脑IP地址为192.168.119.155,那么要向容器发送http请求时,向这个地址发起http请求即可:http://192.168.119.155:18080/send/aaa/bbb

至此,消息生产者的开发已经完成,接下来开始开发消息消费者吧;

消费消息的rabbitmqconsumer

rabbitmqconsumer是个普通的SpringBoot工程,pom.xml的内容和rabbitmqproducer的pom.xml是一样的(除了工程名不一样):

接下来看看关键源码:

  1. 配置类是RabbitConfig.java,和rabbitmqproducer的RabbitConfig.java基本一致,不同的是多了个成员变量queuename,用于表示队列名称,对应的值也是来自环境变量;

  2. 作为RabbitMQ的消息消费者,rabbitmqconsumer要主动连接到RabbitMQ的队列上,以保持随时可以消费消息,对应的绑定代码如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1 @Bean
2    FanoutExchange fanoutExchange() {
3        return new FanoutExchange(exchangeName);
4    }
5
6    //队列A
7    @Bean
8    public Queue fanoutQueue() {
9        return new Queue(queuename);
10    }
11
12    //绑定对列到Fanout交换器
13    @Bean
14    Binding bindingFanoutExchange(Queue fanoutQueue, FanoutExchange fanoutExchange) {
15        return BindingBuilder.bind(fanoutQueue).to(fanoutExchange);
16    }
17
18
  1. 收到消息后,对消息的消费操作由FanoutReceiver类负责,在日志中将消息打印出来:


1
2
3
4
5
6
7
8
9
10
11
12
1@Component
2@RabbitListener(queues = "${mq.rabbit.queue.name}")
3public class FanoutReceiver {
4    private static final Logger logger = LoggerFactory.getLogger(FanoutReceiver.class);
5
6    @RabbitHandler
7    public void process(String message) {
8        logger.info("receive message : " + message);
9    }
10}
11
12

以上就是消息消费者的功能代码,在pom.xml所在目录下执行
mvn clean package -U -DskipTests docker:build即可编译构建工程并生成docker镜像文件;

特别注意的地方

为了让rabbitmqconsumer的Docker镜像更通用,rabbitmq的账号、密码、队列名称等参数都没有写死在工程中,而是从系统环境变量中获取,对应的环境变量的名成分别是:mq.rabbit.username、mq.rabbit.password、mq.rabbit.queue.name,这些环境变量的值是谁负责设置的呢?是docker-compose.yml中的environment参数配置的,容器启动后docker就会在该容器中设置这些环境变量;

在实际使用过程中,经常会出现rabbitmqconsumer进程在连接RabbitMQ时经常报错提示:
ACCESS_REFUSED – Login was refused using authen

目前的推测是:启动rabbitmqconsumer进程和环境变量设置并没有严格的先后顺序,导致了rabbitmqconsumer在使用mq.rabbit.username这些系统环境变量的时候,可能docker还没有设置好这些环境变量,因此导致连接RabbitMQ失败;

针对这个问题,我在工程的启动类RabbitmqconsumerApplication的main方法中,先做了6秒的延时,再启动SpringApplication,目前经过多次测试问题已经不再出现,代码如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
1@SpringBootApplication
2public class RabbitmqconsumerApplication {
3   public static void main(String[] args) {
4       try{
5           Thread.sleep(1000);
6       }catch(Exception e){
7           e.printStackTrace();
8       }
9       SpringApplication.run(RabbitmqconsumerApplication.class, args);
10  }
11}
12
13

为什么rabbitmqproducer应用没有这个问题?
查看容器的日志,重点是
Attempting to connect to: [rabbitmqhost:5672]这段信息,会发现对于rabbitmqproducer应用,只有在第一次发消息的时候才会去连接RabbitMQ,这时候环境变量已经设置好了,所以连接不会有问题;
但是rabbitmqconsumer应用由于是消费者,应用一启动就要主动去连接队列绑定交换机,所以一旦此时环境变量没有设置好,就会导致拿不到正确的参数信息;

如果您不想使用延时这种笨办法,可以在application.properties中将mq.rabbit.username、mq.rabbit.password、mq.rabbit.queue.name这几个参数配置好,再去构建镜像,此时记得在docker-compose.yml中去掉对应的环境变量配置;

docker-compose.yml中对rabbitmqconsumer镜像的使用

在集群版的docker-compose.yml文件中,关于镜像的配置如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
1consumer1:
2    image: bolingcavalry/rabbitmqconsumer:0.0.5-SNAPSHOT
3    hostname: consumer1
4    depends_on:
5      - producer
6    links:
7      - rabbit2:rabbitmqhost
8    environment:
9     - mq.rabbit.address=rabbitmqhost:5672
10     - mq.rabbit.username=admin
11     - mq.rabbit.password=888888
12     - mq.rabbit.queue.name=consumer1.queue
13  consumer2:
14    image: bolingcavalry/rabbitmqconsumer:0.0.5-SNAPSHOT
15    hostname: consumer2
16    depends_on:
17      - consumer1
18    links:
19      - rabbit3:rabbitmqhost
20    environment:
21      - mq.rabbit.address=rabbitmqhost:5672
22      - mq.rabbit.username=admin
23      - mq.rabbit.password=888888
24      - mq.rabbit.queue.name=consumer2.queue
25
26

这些配置中有以下几点需要注意:

  1. 有两个消费者镜像consumer1、consumer2;
  2. 由于交换器是fanout类型,生成者每生产一条消息,由consumer1、consumer2分别消费一次;
  3. consumer1、consumer2各自连接一个队列,这一点在第一章的截图中有过体现,如下图:

至此,消息生产和消费对应的java代码就全部实战完毕了,希望能帮助您快速的开发RabbitMQ相关的java应用,下一章我们将实战RabbitMQ的高可用,通过docker stop命令模拟生产环境中的宕机,看看部分机器的故障是否影响正常的RabbitMQ服务;

给TA打赏
共{{data.count}}人
人已打赏
安全网络

CDN安全市场到2022年价值76.3亿美元

2018-2-1 18:02:50

安全经验

IIS负载均衡-Application Request Route详解第六篇:使用失败请求跟踪规则来诊断ARR

2021-11-28 16:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索