Introduction à Spring Cloud Stream

Introduction à Spring Cloud Stream

1. Vue d'ensemble

Spring Cloud Stream est un framework construit sur Spring Boot et Spring Integration quehelps in creating event-driven or message-driven microservices.

Dans cet article, nous présenterons les concepts et les constructions de Spring Cloud Stream avec quelques exemples simples.

2. Dépendances Maven

Pour commencer, nous devons ajouter la dépendance MavenSpring Cloud Starter Stream with the broker RabbitMQ en tant que middleware de messagerie à nospom.xml:


    org.springframework.cloud
    spring-cloud-starter-stream-rabbit
    1.3.0.RELEASE

Et nous ajouterons lesmodule dependency from Maven Central pour activer également la prise en charge de JUnit:


    org.springframework.cloud
    spring-cloud-stream-test-support
    1.3.0.RELEASE
    test

3. Concepts principaux

L'architecture de Microservices suit le principe «https://martinfowler.com/articles/microservices.html#SmartEndpointsAndDumbPipes[smart and dumb pipes]». La communication entre les points de terminaison est assurée par des tiers de messagerie tels que RabbitMQ ou Apache Kafka. Services communicate by publishing domain events via these endpoints or channels.

Passons en revue les concepts qui composent le framework Spring Cloud Stream, ainsi que les paradigmes essentiels dont nous devons être conscients pour créer des services axés sur les messages.

3.1. Construit

Examinons un service simple dans Spring Cloud Stream qui écoute la liaison deinput et envoie une réponse à la liaison deoutput:

@SpringBootApplication
@EnableBinding(Processor.class)
public class MyLoggerServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyLoggerServiceApplication.class, args);
    }

    @StreamListener(Processor.INPUT)
    @SendTo(Processor.OUTPUT)
    public LogMessage enrichLogMessage(LogMessage log) {
        return new LogMessage(String.format("[1]: %s", log.getMessage()));
    }
}

L'annotation@EnableBinding configure l'application pour lier les canauxINPUT etOUTPUT définis dans l'interfaceProcessor. Both channels are bindings that can be configured to use a concrete messaging-middleware or binder.

Jetons un œil à la définition de tous ces concepts:

  • Bindings - une collection d'interfaces qui identifient les canaux d'entrée et de sortie de manière déclarative

  • Binder - implémentation de middleware de messagerie telle que Kafka ou RabbitMQ

  • Channel - représente le canal de communication entre le middleware de messagerie et l'application

  • StreamListeners - méthodes de gestion des messages dans les beans qui seront automatiquement invoquées sur un message du canal après queMessageConverter effectue la sérialisation / désérialisation entre les événements spécifiques au middleware et les types d'objet de domaine / POJO

  • MessageSchemas - utilisé pour la sérialisation et la désérialisation des messages, ces schémas peuvent être lus statiquement à partir d'un emplacement ou chargés dynamiquement, prenant en charge l'évolution des types d'objets de domaine

3.2. Modèles de communication

Messages designated to destinations are delivered by the Publish-Subscribe messaging pattern. Les éditeurs classent les messages en rubriques, chacune identifiée par un nom. Les abonnés expriment leur intérêt pour un ou plusieurs sujets. Le middleware filtre les messages et transmet ceux des sujets intéressants aux abonnés.

Maintenant, les abonnés pourraient être groupés. Unconsumer group est un ensemble d'abonnés ou de consommateurs, identifiés par ungroup id, au sein duquel les messages d'une rubrique ou d'une partition de rubrique sont livrés de manière équilibrée.

4. Modèle de programmation

Cette section décrit les bases de la création d'applications Spring Cloud Stream.

4.1. Test fonctionel

Le support de test est une implémentation de classeur qui permet d’interagir avec les canaux et d’inspecter les messages.

Envoyons un message au serviceenrichLogMessage ci-dessus et vérifions si la réponse contient le texte“[1]: “ au début du message:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = MyLoggerServiceApplication.class)
@DirtiesContext
public class MyLoggerApplicationTests {

    @Autowired
    private Processor pipe;

    @Autowired
    private MessageCollector messageCollector;

    @Test
    public void whenSendMessage_thenResponseShouldUpdateText() {
        pipe.input()
          .send(MessageBuilder.withPayload(new LogMessage("This is my message"))
          .build());

        Object payload = messageCollector.forChannel(pipe.output())
          .poll()
          .getPayload();

        assertEquals("[1]: This is my message", payload.toString());
    }
}

4.2. Chaînes personnalisées

Dans l'exemple ci-dessus, nous avons utilisé l'interfaceProcessor fournie par Spring Cloud, qui n'a qu'une seule entrée et un seul canal de sortie.

Si nous avons besoin de quelque chose de différent, comme une entrée et deux canaux de sortie, nous pouvons créer un processeur personnalisé:

public interface MyProcessor {
    String INPUT = "myInput";

    @Input
    SubscribableChannel myInput();

    @Output("myOutput")
    MessageChannel anOutput();

    @Output
    MessageChannel anotherOutput();
}

Spring nous fournira la bonne implémentation de cette interface. Les noms de canaux peuvent être définis à l'aide d'annotations comme dans@Output(“myOutput”).

Sinon, Spring utilisera les noms de méthodes comme noms de canaux. Par conséquent, nous avons trois canaux appelésmyInput,myOutput etanotherOutput.

Maintenant, imaginons que nous souhaitons acheminer les messages vers une sortie si la valeur est inférieure à 10 et vers une autre sortie si la valeur est supérieure ou égale à 10:

@Autowired
private MyProcessor processor;

@StreamListener(MyProcessor.INPUT)
public void routeValues(Integer val) {
    if (val < 10) {
        processor.anOutput().send(message(val));
    } else {
        processor.anotherOutput().send(message(val));
    }
}

private static final  Message message(T val) {
    return MessageBuilder.withPayload(val).build();
}

4.3. Distribution conditionnelle

En utilisant l'annotation@StreamListener, nous pouvons égalementfilter the messages we expect in the consumer en utilisant n'importe quelle condition que nous définissons avecSpEL expressions.

Par exemple, nous pourrions utiliser la distribution conditionnelle comme une autre approche pour router les messages vers différentes sorties:

@Autowired
private MyProcessor processor;

@StreamListener(
  target = MyProcessor.INPUT,
  condition = "payload < 10")
public void routeValuesToAnOutput(Integer val) {
    processor.anOutput().send(message(val));
}

@StreamListener(
  target = MyProcessor.INPUT,
  condition = "payload >= 10")
public void routeValuesToAnotherOutput(Integer val) {
    processor.anotherOutput().send(message(val));
}

Les seulslimitation of this approach is that these methods must not return a value.

5. Installer

Configurons l'application qui traitera le message du courtier RabbitMQ.

5.1. Configuration du classeur

Nous pouvons configurer notre application pour utiliser l'implémentation par défaut du classeur viaMETA-INF/spring.binders:

rabbit:\
org.springframework.cloud.stream.binder.rabbit.config.RabbitMessageChannelBinderConfiguration

Ou nous pouvons ajouter la bibliothèque de classeurs pour RabbitMQ au chemin de classe en incluantthis dependency:


    org.springframework.cloud
    spring-cloud-stream-binder-rabbit
    1.3.0.RELEASE

Si aucune implémentation de classeur n'est fournie, Spring utilisera une communication par message direct entre les canaux.

5.2. Configuration de RabbitMQ

Pour configurer l'exemple de la section 3.1 pour utiliser le classeur RabbitMQ, nous devons mettre à jour lesapplication.yml situés àsrc/main/resources:

spring:
  cloud:
    stream:
      bindings:
        input:
          destination: queue.log.messages
          binder: local_rabbit
        output:
          destination: queue.pretty.log.messages
          binder: local_rabbit
      binders:
        local_rabbit:
          type: rabbit
          environment:
            spring:
              rabbitmq:
                host: 
                port: 5672
                username: 
                password: 
                virtual-host: /

La liaisoninput utilisera l'échange appeléqueue.log.messages, et la liaisonoutput utilisera l'échangequeue.pretty.log.messages. Les deux liaisons utiliseront le classeur appelélocal_rabbit.

Notez que nous n'avons pas besoin de créer les échanges ou les files d'attente RabbitMQ à l'avance. Lors de l'exécution de l'application,both exchanges are automatically created.

Pour tester l'application, nous pouvons utiliser le site de gestion RabbitMQ pour publier un message. Dans le panneauPublish Message de l'échangequeue.log.messages, nous devons saisir la requête au format JSON.

5.3. Personnalisation de la conversion des messages

Spring Cloud Stream nous permet d’appliquer la conversion de messages pour des types de contenu spécifiques. Dans l'exemple ci-dessus, au lieu d'utiliser le format JSON, nous souhaitons fournir du texte brut.

Pour ce faire, nous allons àapply a custom transformation to LogMessage using a MessageConverter:

@SpringBootApplication
@EnableBinding(Processor.class)
public class MyLoggerServiceApplication {
    //...

    @Bean
    public MessageConverter providesTextPlainMessageConverter() {
        return new TextPlainMessageConverter();
    }

    //...
}
public class TextPlainMessageConverter extends AbstractMessageConverter {

    public TextPlainMessageConverter() {
        super(new MimeType("text", "plain"));
    }

    @Override
    protected boolean supports(Class clazz) {
        return (LogMessage.class == clazz);
    }

    @Override
    protected Object convertFromInternal(Message message,
        Class targetClass, Object conversionHint) {
        Object payload = message.getPayload();
        String text = payload instanceof String
          ? (String) payload
          : new String((byte[]) payload);
        return new LogMessage(text);
    }
}

Après avoir appliqué ces modifications, en revenant au panneauPublish Message, si nous définissons l'en-tête «contentTypes» sur «text/plain» et la charge utile sur «Hello World», il devrait travailler comme avant.

5.4. Groupes de consommateurs

Lors de l'exécution de plusieurs instances de notre application,every time there is a new message in an input channel, all subscribers will be notified.

La plupart du temps, le message doit être traité une seule fois. Spring Cloud Stream implémente ce comportement via des groupes de consommateurs.

Pour activer ce comportement, chaque liaison de consommateur peut utiliser la propriétéspring.cloud.stream.bindings.<CHANNEL>.group pour spécifier un nom de groupe:

spring:
  cloud:
    stream:
      bindings:
        input:
          destination: queue.log.messages
          binder: local_rabbit
          group: logMessageConsumers
          ...

6. Microservices basés sur les messages

Dans cette section, nous présentons toutes les fonctionnalités requises pour exécuter nos applications Spring Cloud Stream dans un contexte de microservices.

6.1. Augmenter

Lorsque plusieurs applications sont en cours d'exécution, il est important de s'assurer que les données sont correctement réparties entre les consommateurs. Pour ce faire, Spring Cloud Stream fournit deux propriétés:

  • spring.cloud.stream.instanceCount - nombre d'applications en cours d'exécution

  • spring.cloud.stream.instanceIndex - index de l'application courante

Par exemple, si nous avons déployé deux instances de l’applicationMyLoggerServiceApplication ci-dessus, la propriétéspring.cloud.stream.instanceCount doit être égale à 2 pour les deux applications et la propriétéspring.cloud.stream.instanceIndex doit être respectivement 0 et 1.

Ces propriétés sont automatiquement définies si nous déployons les applications Spring Cloud Stream à l'aide de Spring Data Flow comme décrit dansthis article.

6.2. Partitionnement

Les événements du domaine peuvent être des messagesPartitioned. Cela aide lorsque nous sommesscaling up the storage and improving application performance.

L'événement de domaine a généralement une clé de partition de sorte qu'il se termine dans la même partition avec les messages associés.

Disons que nous voulons que les messages du journal soient partitionnés par la première lettre du message, qui serait la clé de partition, et regroupés en deux partitions.

Il y aurait une partition pour les messages du journal commençant parA-M et une autre partition pourN-Z. Cela peut être configuré à l'aide de deux propriétés:

  • spring.cloud.stream.bindings.output.producer.partitionKeyExpression - l'expression pour partitionner les charges utiles

  • spring.cloud.stream.bindings.output.producer.partitionCount - le nombre de groupes

Sometimes the expression to partition is too complex to write it in only one line. Dans ces cas, nous pouvons écrire notre stratégie de partition personnalisée en utilisant la propriétéspring.cloud.stream.bindings.output.producer.partitionKeyExtractorClass.

6.3. Indicateur de santé

Dans un contexte de microservices, nous avons également besoin dedetect when a service is down or starts failing. Spring Cloud Stream fournit la propriétémanagement.health.binders.enabled pour activer les indicateurs de santé pour les classeurs.

Lors de l'exécution de l'application, nous pouvons interroger l'état de santé àhttp://<host>:<port>/health.

7. Conclusion

Dans ce didacticiel, nous avons présenté les principaux concepts de Spring Cloud Stream et montré comment l'utiliser au moyen de simples exemples utilisant RabbitMQ. Vous trouverez plus d'informations sur Spring Cloud Streamhere.

Le code source de cet article peut être trouvéover on GitHub.