Einführung in Spring Cloud Stream

Einführung in Spring Cloud Stream

1. Überblick

Spring Cloud Stream ist ein Framework, das auf Spring Boot und Spring Integrationhelps in creating event-driven or message-driven microservices basiert.

In diesem Artikel werden Konzepte und Konstrukte von Spring Cloud Stream anhand einiger einfacher Beispiele vorgestellt.

2. Maven-Abhängigkeiten

Um zu beginnen, müssen wir dieSpring Cloud Starter Stream with the broker RabbitMQ Maven-Abhängigkeit als Messaging-Middleware zu unserenpom.xml hinzufügen:


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

Und wir werden diemodule dependency from Maven Centralhinzufügen, um auch die JUnit-Unterstützung zu aktivieren:


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

3. Hauptkonzepte

Die Microservices-Architektur folgt dem Prinzip „https://martinfowler.com/articles/microservices.html#SmartEndpointsAndDumbPipes[smart endpoints and dumb pipes]“. Die Kommunikation zwischen Endpunkten wird von Messaging-Middleware-Parteien wie RabbitMQ oder Apache Kafka gesteuert. Services communicate by publishing domain events via these endpoints or channels.

Lassen Sie uns die Konzepte des Spring Cloud Stream-Frameworks zusammen mit den wesentlichen Paradigmen durchgehen, die wir kennen müssen, um nachrichtengesteuerte Dienste zu erstellen.

3.1. Konstruiert

Schauen wir uns einen einfachen Dienst in Spring Cloud Stream an, der die Bindung voninputabhört und eine Antwort auf die Bindung vonoutputendet:

@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()));
    }
}

Die Annotation@EnableBinding konfiguriert die Anwendung so, dass die in der SchnittstelleProcessor definierten KanäleINPUT undOUTPUT gebunden werden. Both channels are bindings that can be configured to use a concrete messaging-middleware or binder.

Werfen wir einen Blick auf die Definition all dieser Konzepte:

  • Bindings - eine Sammlung von Schnittstellen, die die Eingangs- und Ausgangskanäle deklarativ identifizieren

  • Binder - Implementierung von Messaging-Middleware wie Kafka oder RabbitMQ

  • Channel - repräsentiert die Kommunikationsleitung zwischen Messaging-Middleware und der Anwendung

  • StreamListeners - Nachrichtenbehandlungsmethoden in Beans, die automatisch für eine Nachricht aus dem Kanal aufgerufen werden, nachdemMessageConverter die Serialisierung / Deserialisierung zwischen Middleware-spezifischen Ereignissen und Domänenobjekttypen / POJOs durchgeführt hat

  • MessageSchemas - Diese Schemas werden für die Serialisierung und Deserialisierung von Nachrichten verwendet. Sie können statisch von einem Speicherort gelesen oder dynamisch geladen werden, um die Entwicklung von Domänenobjekttypen zu unterstützen

3.2. Kommunikationsmuster

Messages designated to destinations are delivered by the Publish-Subscribe messaging pattern. Publisher kategorisieren Nachrichten in Themen, die jeweils durch einen Namen gekennzeichnet sind. Abonnenten zeigen Interesse an einem oder mehreren Themen. Die Middleware filtert die Nachrichten und übermittelt diese zu den interessanten Themen an die Abonnenten.

Jetzt konnten die Abonnenten gruppiert werden. Einconsumer group ist eine Gruppe von Abonnenten oder Verbrauchern, die durch eingroup id gekennzeichnet sind und in denen Nachrichten von einem Thema oder einer Themenpartition auf eine lastausgeglichene Weise übermittelt werden.

4. Programmiermodell

In diesem Abschnitt werden die Grundlagen zum Erstellen von Spring Cloud Stream-Anwendungen beschrieben.

4.1. Funktionsprüfung

Die Testunterstützung ist eine Binder-Implementierung, die die Interaktion mit den Kanälen und das Überprüfen von Nachrichten ermöglicht.

Senden wir eine Nachricht an den oben genanntenenrichLogMessage-Dienst und prüfen, ob die Antwort den Text“[1]: “ am Anfang der Nachricht enthält:

@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. Benutzerdefinierte Kanäle

Im obigen Beispiel haben wir die von Spring Cloud bereitgestellteProcessor-Schnittstelle verwendet, die nur einen Eingangs- und einen Ausgangskanal hat.

Wenn wir etwas anderes benötigen, wie einen Ein- und zwei Ausgangskanäle, können wir einen benutzerdefinierten Prozessor erstellen:

public interface MyProcessor {
    String INPUT = "myInput";

    @Input
    SubscribableChannel myInput();

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

    @Output
    MessageChannel anotherOutput();
}

Spring wird die ordnungsgemäße Implementierung dieser Schnittstelle für uns bereitstellen. Die Kanalnamen können mit Anmerkungen wie in@Output(“myOutput”) festgelegt werden.

Andernfalls verwendet Spring die Methodennamen als Kanalnamen. Daher haben wir drei Kanäle mit den NamenmyInput,myOutput undanotherOutput.

Stellen wir uns nun vor, wir möchten die Nachrichten an eine Ausgabe weiterleiten, wenn der Wert kleiner als 10 ist, und an eine andere Ausgabe, wenn der Wert größer oder gleich 10 ist:

@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. Bedingter Versand

Mit der Annotation@StreamListener können wir auchfilter the messages we expect in the consumer mit jeder Bedingung definieren, die wir mitSpEL expressions definieren.

Als Beispiel könnten wir das bedingte Dispatching als einen anderen Ansatz verwenden, um Nachrichten an verschiedene Ausgaben weiterzuleiten:

@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));
}

Die einzigenlimitation of this approach is that these methods must not return a value.

5. Konfiguration

Richten Sie die Anwendung ein, die die Nachricht vom RabbitMQ-Broker verarbeitet.

5.1. Binder-Konfiguration

Wir können unsere Anwendung so konfigurieren, dass die Standard-Binder-Implementierung überMETA-INF/spring.binders verwendet wird:

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

Oder wir können die Binder-Bibliothek für RabbitMQ zum Klassenpfad hinzufügen, indem wirthis dependency einschließen:


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

Wenn keine Binder-Implementierung bereitgestellt wird, verwendet Spring die direkte Nachrichtenkommunikation zwischen den Kanälen.

5.2. RabbitMQ-Konfiguration

Um das Beispiel in Abschnitt 3.1 für die Verwendung des RabbitMQ-Binders zu konfigurieren, müssen Sie dieapplication.yml aktualisieren, die sich beisrc/main/resources befinden:

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: /

Dieinput-Bindung verwendet den Austauschqueue.log.messages, und dieoutput-Bindung verwendet den Austauschqueue.pretty.log.messages. Beide Bindungen verwenden das Bindemittellocal_rabbit.

Beachten Sie, dass wir die RabbitMQ-Börsen oder -Warteschlangen nicht im Voraus erstellen müssen. Beim Ausführen der Anwendungboth exchanges are automatically created.

Zum Testen der Anwendung können wir die RabbitMQ-Verwaltungssite verwenden, um eine Nachricht zu veröffentlichen. ImPublish Message-Bereich des Exchangequeue.log.messages müssen wir die Anforderung im JSON-Format eingeben.

5.3. Anpassen der Nachrichtenkonvertierung

Mit Spring Cloud Stream können wir die Nachrichtenkonvertierung für bestimmte Inhaltstypen anwenden. Im obigen Beispiel möchten wir anstelle des JSON-Formats einfachen Text bereitstellen.

Dazu gehen wir zuapply 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);
    }
}

Wenn Sie nach dem Anwenden dieser Änderungen zum BereichPublish Message zurückkehren und den Header "contentTypes" auf "text/plain" und die Nutzlast auf "Hello World" setzen, sollte dies der Fall sein arbeite wie zuvor.

5.4. Verbrauchergruppen

Wenn Sie mehrere Instanzen unserer Anwendung ausführen,every time there is a new message in an input channel, all subscribers will be notified.

In den meisten Fällen muss die Nachricht nur einmal verarbeitet werden. Spring Cloud Stream implementiert dieses Verhalten über Konsumentengruppen.

Um dieses Verhalten zu aktivieren, kann jede Verbraucherbindung die Eigenschaftspring.cloud.stream.bindings.<CHANNEL>.groupverwenden, um einen Gruppennamen anzugeben:

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

6. Nachrichtengesteuerte Microservices

In diesem Abschnitt werden alle erforderlichen Funktionen zum Ausführen unserer Spring Cloud Stream-Anwendungen in einem Microservices-Kontext vorgestellt.

6.1. Hochskalieren

Wenn mehrere Anwendungen ausgeführt werden, ist es wichtig sicherzustellen, dass die Daten ordnungsgemäß auf die Verbraucher aufgeteilt werden. Zu diesem Zweck bietet Spring Cloud Stream zwei Eigenschaften:

  • spring.cloud.stream.instanceCount - Anzahl der laufenden Anwendungen

  • spring.cloud.stream.instanceIndex - Index der aktuellen Anwendung

Wenn wir beispielsweise zwei Instanzen der obigen AnwendungMyLoggerServiceApplication bereitgestellt haben, sollte die Eigenschaftspring.cloud.stream.instanceCount für beide Anwendungen 2 und die Eigenschaftspring.cloud.stream.instanceIndex 0 bzw. 1 sein.

Diese Eigenschaften werden automatisch festgelegt, wenn wir die Spring Cloud Stream-Anwendungen mithilfe von Spring Data Flow bereitstellen, wie inthis article beschrieben.

6.2. Partitionierung

Die Domänenereignisse könnenPartitioned Nachrichten sein. Dies hilft, wenn wirscaling up the storage and improving application performance sind.

Das Domänenereignis verfügt normalerweise über einen Partitionsschlüssel, sodass es in derselben Partition mit zugehörigen Nachrichten endet.

Angenommen, wir möchten, dass die Protokollnachrichten nach dem ersten Buchstaben in der Nachricht, dem Partitionsschlüssel, partitioniert und in zwei Partitionen gruppiert werden.

Es würde eine Partition für die Protokollnachrichten geben, die mitA-M beginnen, und eine andere Partition fürN-Z.. Dies kann mithilfe von zwei Eigenschaften konfiguriert werden:

  • spring.cloud.stream.bindings.output.producer.partitionKeyExpression - der Ausdruck zum Partitionieren der Nutzdaten

  • spring.cloud.stream.bindings.output.producer.partitionCount - die Anzahl der Gruppen

Sometimes the expression to partition is too complex to write it in only one line. In diesen Fällen können wir unsere benutzerdefinierte Partitionsstrategie mit der Eigenschaftspring.cloud.stream.bindings.output.producer.partitionKeyExtractorClass schreiben.

6.3. Gesundheitsindikator

In einem Microservices-Kontext müssen wir auchdetect when a service is down or starts failing. Spring Cloud Stream stellt die Eigenschaftmanagement.health.binders.enabled bereit, um die Integritätsindikatoren für Bindemittel zu aktivieren.

Beim Ausführen der Anwendung können wir den Integritätsstatus beihttp://<host>:<port>/health abfragen.

7. Fazit

In diesem Tutorial haben wir die Hauptkonzepte von Spring Cloud Stream vorgestellt und anhand einiger einfacher Beispiele über RabbitMQ gezeigt, wie man es verwendet. Weitere Informationen zu Spring Cloud Stream finden Sie unterhere.

Der Quellcode für diesen Artikel istover on GitHub.