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