Thématiques principales

dimanche 23 février 2020

Spring-boot: RabbitMQ

On a jamais parler de Spring-boot [9], ça fait même assez longtemps que l’on a pas parler Java! Du coup comme nous sommes lancé dans les articles sur RabbitMQ et que ce message broker est assez courant dans ces écosystèmes, on va en profiter!

On va donc finir cette liste d’articles [2, 3] avec une mise en situation en réalisant une application client server implémentée avec Spring-boot (sur lequel nous reviendrons de façon plus général dans un autre article).




Initialisation des Projets


Bon on va commencer par préparer nos deux projets: un petit tour par SpringInitializr [1]

Voici le pom maven du client, (le server c’est le même!), on verra au fil de l’eau s’il faut y ajouter des choses


 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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.2.4.RELEASE</version>
  <relativePath/> <!-- lookup parent from repository -->
 </parent>
 <groupId>org.tc.rabbitmq</groupId>
 <artifactId>client</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <name>client</name>
 <description>Demo project for Spring Boot</description>

 <properties>
  <java.version>13</java.version>
 </properties>

 <dependencies>
  <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-devtools</artifactId>
   <scope>runtime</scope>
   <optional>true</optional>
  </dependency>
  <dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <optional>true</optional>
  </dependency>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
   <exclusions>
    <exclusion>
     <groupId>org.junit.vintage</groupId>
     <artifactId>junit-vintage-engine</artifactId>
    </exclusion>
   </exclusions>
  </dependency>
  <dependency>
   <groupId>org.springframework.amqp</groupId>
   <artifactId>spring-rabbit-test</artifactId>
   <scope>test</scope>
  </dependency>
 </dependencies>

 <build>
  <plugins>
   <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
   </plugin>
  </plugins>
 </build>
</project>


Du coup on build les projets avec un classique “mvn clean install”. -> Ok on a toutes les dépendances et on constate que l’on a déjà produit un jar contenant notre application vide dans le target. On peut donc avant même d’avancer dans le codage, préparer la partie intégration et Docker.

Intégration docker


On va donc ajouter un Dockerfile dans chaque projet pour conteneuriser le client et le server:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
FROM openjdk:15-slim-buster

RUN mkdir -p /app

VOLUME /app

ENV JAVA_OPTS=""
ENV JAR_NAME=""

COPY ./target/${JAR_NAME} /app

ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -jar /app/${JAR_NAME}" ]


Et on peut du coup mettre à jour le docker-compose afin qu’il intègre les deux micro service client et serveur tout en les buildant à la volée.


 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
version: "3"
services:

  rabbitmq:
    container_name: rabbitmq
    image: bitnami/rabbitmq:latest
    environment:
      RABBITMQ_USERNAME: user
      RABBITMQ_PASSWORD: password
    ports:
      - "15672:15672"

  init-rabbitmq:
    container_name: init-rabbitmq
    image: softonic/rabbitmqadmin
    environment:
      RABBITMQ_USERNAME: user
      RABBITMQ_PASSWORD: password
    volumes:
      - ./:/home:Z
    depends_on:
      - rabbitmq
    command: "/home/test.init.sh"

  client:
    container_name: client
    build:
      context: ./client
    image: client:latest
    environment:
      JAR_NAME: client-0.0.1-SNAPSHOT.jar
    depends_on:
      - rabbitmq

  server:
    container_name: server
    build:
      context: ./server
    image: server:latest
    environment:
      JAR_NAME : server-0.0.1-SNAPSHOT.jar
    depends_on:
      - rabbitmq

Config spring-boot


Maintenant que l’on a toute la chaîne de construction et de test, on va passer à la configuration de nos applications elle-même en initialisant le paramétrage dans le fichier yml pour aller ensuite produire et consommer dans la RabbitMQ.

Globalement le fichier de paramétrage des deux projets va être très similaire en dehors de la partie configuration applicative que nous ajouterons quand nous aborderons le code. Comme base, nous allons donc utiliser le fichier de conf suivant (au nom du projet prêt).


 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
server:
  port: 8081

spring:
  application:
    name: client
  rabbitmq:
    host: rabbitmq
    port: 5672
    username: user
    password: password
 
endpoints:
  enabled: true
  health:
    enabled: true
  consul:
    enabled: true

logging:
  level:
    root: info
    org:
      tc: debug
  

Code Client


Pour commencer, la partie cliente, il faut d’abord considérer la configuration des exchanges que l’on va exploiter. Pour cela, on utilisera la config proposé dans l’article [3]. C’est à dire un exchange de type Direct avec deux binding et routingKey et un autre exchange de type Topic avec la aussi deux routingKey. Ces deux exchanges servent les deux même queues.

Au niveau configuration, nous allons donc ajouter la conf suivante que nous exploitons avec des @Value.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
notification:
  exchange:
    myExchangeA:
      name: myExchangeA
      message:
        flux1: flux1
        flux2: flux2
    myExchangeB:
      name: myExchangeB
      message:
        flux1: "flux1.toto"
        flux2: "tata.flux2"


Cet conf, nous allons l’utiliser dans un classe de type @Service qui utilisera un bean de type AmqpTemplate. Ce bean va nous permettre de pousser nos messages de type MessageDto:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package org.tc.rabbitmq.client.notification;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MessageDto {
    private String message;
    private String exchangeName;
    private String routingKeyUsed;
}


AmqpTemplate est une interface [6], il faut donc proposer une implémentation que l’on configurera pour que l'implémentation sérialise en Json nos messages (sans quoi, il est possible aussi de s’appuyer sur des sérialisation object java classique ou en xml) [7]

Cette implémentation on va la déclarer dans une classe de configuration annoté en conséquence:


 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
package org.tc.rabbitmq.client;

import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitConfig {


    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory factory, MessageConverter messageConverter) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(factory);
        rabbitTemplate.setMessageConverter(messageConverter);
        return rabbitTemplate;
    }

    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}


Maintenant que l’on a une implémentation disponible pour l’interface AmqpTemplate, il ne reste plus qu’à déclarer des méthodes permettant de pousser des messages portant diverses routingKey dans les différents exchanges. Pour automatiser les envoies il suffit d’ajouter l’annotation @Scheduled [4] qui va nous permettre de définir des délais de ré-émission des messages (mettre aussi @EnableSheduling [5] sur la classe portant l’annotation @SpringBootApplication ).


 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
package org.tc.rabbitmq.client.notification;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class RMQMessageProducer {

    @Autowired
    private AmqpTemplate template;

    @Value("${notification.exchange.myExchangeA.name}")
    private String myExchangeA;

    @Value("${notification.exchange.myExchangeA.message.flux1}")
    private String flux1A;

    @Value("${notification.exchange.myExchangeA.message.flux2}")
    private String flux2A;

    @Value("${notification.exchange.myExchangeB.name}")
    private String myExchangeB;

    @Value("${notification.exchange.myExchangeB.message.flux1}")
    private String flux1B;

    @Value("${notification.exchange.myExchangeB.message.flux2}")
    private String flux2B;

    @Scheduled(fixedDelay = 1000, initialDelay = 500)
    public void sendDirectToQueueA() {
        MessageDto message = MessageDto.builder().message("Hello World!").exchangeName(myExchangeA).routingKeyUsed(flux1A).build();
        this.template.convertAndSend(myExchangeA, flux1A, message);
        log.debug(" Envoie " + message);
    }

    @Scheduled(fixedDelay = 1250, initialDelay = 500)
    public void sendDirectToQueueB() {
        MessageDto message = MessageDto.builder().message("Hello World!").exchangeName(myExchangeA).routingKeyUsed(flux2A).build();
        this.template.convertAndSend(myExchangeA, flux2A, message);
        log.debug(" Envoie " + message  );
    }


    @Scheduled(fixedDelay = 500, initialDelay = 500)
    public void sendTopicToQueueA() {
        MessageDto message = MessageDto.builder().message("Hello World!").exchangeName(myExchangeB).routingKeyUsed(flux1B).build();
        this.template.convertAndSend(myExchangeB, flux1B, message);
        log.debug(" Envoie " + message);
    }

    @Scheduled(fixedDelay = 2000, initialDelay = 500)
    public void sendTopicToQueueB() {
        MessageDto message = MessageDto.builder().message("Hello World!").exchangeName(myExchangeB).routingKeyUsed(flux2B).build();
        this.template.convertAndSend(myExchangeB, flux2B, message);
        log.debug(" Envoie " + message  );
    }

    @Scheduled(fixedDelay = 5000, initialDelay = 500)
    public void sendTopicTo() {
        MessageDto message = MessageDto.builder().message("Hello World!").exchangeName(myExchangeB).routingKeyUsed("flux1.flux2").build();
        this.template.convertAndSend(myExchangeB, "flux1.flux2", message);
        log.debug(" Envoie " + message  );
    }

}


Pour tester cela, on lance un docker-compose. Un petit docker logs sur notre client nous permet de nous assurer que celui-ci émet des messages et enfin on peut aller dans l’IHM de RabbitMQ afin de constater que les queues sont bien alimentés (avec application des routingKey).

Code server


Le client fonctionne, reste à faire notre server. Pour cela c’est plus simple car on peut reprendre à la fois la classe MessageDto (histoire que les deux applications parlent le même langage) et la classe de config (histoire de disposer du même deserialiser, Json2Object)

Coté configuration, c’est pareil, on ajoute un bout de conf pour faire juste état des deux queues dans lesquelles nous allons aller chercher nos messages.


1
2
3
4
notification:
  queue:
    myQueueA: myQueueA
    myQueueB: myQueueB


Enfin pour finir ce server, on va déclarer une classe de type @Service dans laquelle, par queue, nous déclarons un listener dédié via l’annotation @Listener.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.tc.rabbitmq.server.notification;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class RMQMessageListners {


    @RabbitListener(queues = "${notification.queue.myQueueA}")
    public void listenQueueA(MessageDto message) {
        log.debug("Message receive with myQueueA: "+message);
    }

    @RabbitListener(queues = "${notification.queue.myQueueB}")
    public void listenQueueB(MessageDto message) {
        log.debug("Message receive with myQueueB: "+message);
    }

}


Voila la boucle est blouclé, pour finalisé les tests, on realiser un nouveau docker-compose. La une fois les applications en ligne avec RabbitMQ, inutile de chercher les messages dans les queues dans l’IHM Web, ceux ci vont être consommés à la volée. Ainsi à part les voir dans les statistiques des flux, pour visionner nos messages, il ne reste plus qu’à consulter les log du composant server. Op un docker logs et on est fixé!

Conclusion


Un article un peu plus long que d’habitude (surtout ces derniers temps) mais nous aurons fait le tour de l‘utilisation de Rabbit et en prime on aura fait un premier cas d’utilisation de Spring-boot, surtout qu’il le fallait depuis déjà un moment, mais… faut de temps et d’envie, il faut avouer que … bon en tout cas maintenant c’est fait! Seule chose que nous avons pas regarder, c’est comment tester mais la je vous invite ici -> [8]

Références:


[1] https://start.spring.io/
[2] https://un-est-tout-et-tout-est-un.blogspot.com/2020/02/rabbitmq.html
[3] https://un-est-tout-et-tout-est-un.blogspot.com/2020/02/rabbitmq-configuration.html
[4] https://www.rabbitmq.com/tutorials/tutorial-one-spring-amqp.html
[5] https://thepracticaldeveloper.com/2016/10/23/produce-and-consume-json-messages-with-spring-boot-amqp/
[6] https://docs.spring.io/spring-amqp/reference/html/#amqp-template
[7] https://docs.spring.io/spring-amqp/docs/2.1.6.BUILD-SNAPSHOT/reference/#_jackson2xmlmessageconverter
[8] https://www.programcreek.com/java-api-examples/?api=org.springframework.jms.support.converter.SimpleMessageConverter
[9] https://spring.io/projects/spring-boot

Aucun commentaire:

Enregistrer un commentaire