Spring Cloud Stream for communication between services

Keywords: Java Spring RabbitMQ JSON kafka

Spring Cloud Stream

The underlying implementation of Srping cloud Bus is Spring Cloud Stream, which is designed to build a message-driven (or event-driven) micro-service architecture. Spring Cloud Stream itself encapsulates (integrates) and extends modules such as Spring Messaging, Spring Integration, Spring Boot Actuator, Spring Boot Externalized Configuration and so on. Next, we implement communication between two services to demonstrate the use of Spring Cloud Stream.

Overall overview


If a service wants to communicate with other services to define channels, it usually defines the output channel and the input channel. The output channel is used to send messages and the input channel is used to receive messages. Each channel will have a name (input and output are only channel types, many channels can be defined by different names). The names of different channels cannot be the same, otherwise they will report errors (input channel). Binders are abstract layers for operating RabbitMQ or Kafka. To shield the complexity and inconsistency of operating these message middleware, binders define topics in message middleware with channel names. Message producers within a topic come from multiple services, and consumers of messages within a topic also have multiple clothes. Business, that is to say, the publication and consumption of messages are defined and organized by topics. The name of channels is the name of topics. In Rabbit MQ, topics are implemented by Exchanges, and in Kafka, topics are implemented by Topic.

Preparation environment

Create two projects spring-cloud-stream-a and spring-cloud-stream-b, spring-cloud-stream-a we use Spring Cloud Stream to achieve communication, spring-cloud-stream-b we use Spring Cloud Stream's underlying module Spring Integration to achieve communication.
The POM file dependencies for both projects are:

<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream-test-support</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

spring-cloud-stream-binder-rabbit refers to the implementation of the binder using RabbitMQ.

Project configuration content application.properties:

spring.application.name=spring-cloud-stream-a
server.port=9010

#Setting the default binder
spring.cloud.stream.defaultBinder = rabbit

spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.application.name=spring-cloud-stream-b
server.port=9011

#Setting the default binder
spring.cloud.stream.defaultBinder = rabbit

spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

Start a rabbitmq:

docker pull rabbitmq:3-management

docker run -d --hostname my-rabbit --name rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3-management

Write project A code

In project A, an input channel and an output channel are defined. The channel is defined with @Input and @Output annotations in the interface. Spring Cloud Stream automatically injects classes according to the interface definition when the program starts (Spring Cloud Stream automatically implements the interface without writing code).
A service input channel, channel name ChatExchanges.A.Input, interface definition input channel must return to Subscribable Channel:

public interface ChatInput {

    String INPUT = "ChatExchanges.A.Input";

    @Input(ChatInput.INPUT)
    SubscribableChannel input();
}

A service output channel, channel name ChatExchanges.A.Output, output channel must return to MessageChannel:

public interface ChatOutput {

    String OUTPUT = "ChatExchanges.A.Output";

    @Output(ChatOutput.OUTPUT)
    MessageChannel output();
}

Define message entity classes:

public class ChatMessage implements Serializable {

    private String name;
    private String message;
    private Date chatDate;

    //Parallelization of constructors without parameters can lead to errors
    private ChatMessage(){}

    public ChatMessage(String name,String message,Date chatDate){
        this.name = name;
        this.message = message;
        this.chatDate = chatDate;
    }

    public String getName(){
        return this.name;
    }

    public String getMessage(){
        return this.message;
    }

    public Date getChatDate() { return this.chatDate; }

    public String ShowMessage(){
        return String.format("Chat message:%s At that time,%s say%s. ",this.chatDate,this.name,this.message);
    }
}

On the business processing class, the @EnableBinding annotation is used to bind the input channel and output channel. This binding action is actually to create and register the implementation class of the input and output channel into the Bean, so it can be injected directly with @Autowired. In addition, the serialization of messages defaults to use the application/json format (com.fastexml.jackson), and finally with the @StreamListener annotation. Specify channel message listening:

//The input channel of ChatInput.class is not bound here, and the AClient class reference cannot be found when listening for data.
//Input and Output channel definitions cannot be the same, otherwise program startup throws an exception.
@EnableBinding({ChatOutput.class,ChatInput.class})
public class AClient {

    private static Logger logger = LoggerFactory.getLogger(AClient.class);

    @Autowired
    private ChatOutput chatOutput;

    //StreamListener brings Json's ability to transfer objects, and receives B's message to print and reply to B's new message.
    @StreamListener(ChatInput.INPUT)
    public void PrintInput(ChatMessage message) {

        logger.info(message.ShowMessage());

        ChatMessage replyMessage = new ChatMessage("ClientA","A To B Message.", new Date());

        chatOutput.output().send(MessageBuilder.withPayload(replyMessage).build());
    }
}

To this A project code is completed.

Write project B code

Project B uses Spring Integration to publish and consume messages. When defining channels, we exchange the names of input and output channels:

public interface ChatProcessor {

    String OUTPUT = "ChatExchanges.A.Input";
    String INPUT  = "ChatExchanges.A.Output";

    @Input(ChatProcessor.INPUT)
    SubscribableChannel input();

    @Output(ChatProcessor.OUTPUT)
    MessageChannel output();
}

Message Entity Class:

public class ChatMessage {
    private String name;
    private String message;
    private Date chatDate;

    //Parallelization of constructors without parameters can lead to errors
    private ChatMessage(){}

    public ChatMessage(String name,String message,Date chatDate){
        this.name = name;
        this.message = message;
        this.chatDate = chatDate;
    }

    public String getName(){
        return this.name;
    }

    public String getMessage(){
        return this.message;
    }

    public Date getChatDate() { return this.chatDate; }

    public String ShowMessage(){
        return String.format("Chat message:%s At that time,%s say%s. ",this.chatDate,this.name,this.message);
    }
}

The business processing class replaces @StreamListener with @Service Activator annotation and publishes messages with @InboundChannelAdapter annotation:

@EnableBinding(ChatProcessor.class)
public class BClient {

    private static Logger logger = LoggerFactory.getLogger(BClient.class);

    //@ ServiceActivator does not have Json's ability to transfer objects and needs the help of @Transformer annotation
    @ServiceActivator(inputChannel=ChatProcessor.INPUT)
    public void PrintInput(ChatMessage message) {

        logger.info(message.ShowMessage());
    }

    @Transformer(inputChannel = ChatProcessor.INPUT,outputChannel = ChatProcessor.INPUT)
    public ChatMessage transform(String message) throws Exception{
        ObjectMapper objectMapper = new ObjectMapper();
        return objectMapper.readValue(message,ChatMessage.class);
    }

    //Send a message to A every second.
    @Bean
    @InboundChannelAdapter(value = ChatProcessor.OUTPUT,poller = @Poller(fixedDelay="1000"))
    public GenericMessage<ChatMessage> SendChatMessage(){
        ChatMessage message = new ChatMessage("ClientB","B To A Message.", new Date());
        GenericMessage<ChatMessage> gm = new GenericMessage<>(message);
        return gm;
    }
}

Operation procedure

Start Project A and Project B:

Source code

Github warehouse: https://github.com/sunweisheng/spring-cloud-example

Posted by offnordberg on Mon, 14 Oct 2019 13:21:14 -0700