65-Stream

1. Stream 引入#

https://spring.io/projects/spring-cloud-stream

1.1 要解决的问题#

MQ 消息中间件⼴泛应⽤在应⽤解耦合、异步消息处理、流量削峰等场景中。

不同的 MQ 消息中间件内部机制包括使⽤⽅式都会有所不同,⽐如 RabbitMQ 中有 Exchange(交换机/交换器)这⼀概念,Kafka 有 Topic、Partition 分区这些概念,MQ 消息中间件的差异性不利于我们上层的开发应⽤,当我们的系统希望从原有的 RabbitMQ 切换到 Kafka 时,我们会发现⽐较困难,很多要操作可能重来(因为应⽤程序和具体的某⼀款 MQ 消息中间件耦合在⼀起了)。

Spring Cloud Stream 进⾏了很好的上层抽象,可以让我们与具体消息中间件解耦合,屏蔽掉了底层具体 MQ 消息中间件的细节差异,就像 Hibernate 屏蔽掉了具体数据库(MySQL/Oracle)⼀样。如此⼀来,我们学习、开发、维护 MQ 都会变得轻松。

Spring Cloud Stream 消息驱动组件帮助我们更快速、更⽅便、更友好的去构建消息驱动微服务的。

⽬前 Spring Cloud Stream ⽀持 RabbitMQ 和 Kafka。

【本质】屏蔽底层不同消息中间件的差异,统⼀ MQ 编程模型,降低学习、开发、维护 MQ 的成本。

1.2 重要概念#

Spring Cloud Stream 是⼀个构建消息驱动微服务的框架。应⽤程序通过 Inputs(相当于消息消费者)或者 Outputs(相当于消息⽣产者)来与 Spring Cloud Stream 中的 Binder 对象交互,⽽ Binder 对象是⽤来屏蔽底层 MQ 细节的,它负责与具体的消息中间件交互。

「Binder 绑定器」是 Spring Cloud Stream 中⾮常核⼼的概念,就是通过它来屏蔽底层不同 MQ 消息中间件的细节差异,当需要更换为其他消息中间件时,我们需要做的就是更换对应的 Binder 绑定器⽽不需要修改任何应⽤逻辑(Binder 绑定器的实现是框架内置的)。

说⽩了:对于我们来说,只需要知道如何使⽤ Spring Cloud Stream 与 Binder 对象交互即可。

Binder 是应用与消息中间件之间的封装,目前实现了 Kafka 和 RabbitMQ 的 Binder,通过 Binder 可以很方便的与中间件进行连接。通过定义绑定器 Binder 作为中间层,实现了应用程序与消息中间件细节之间的隔离。

1.3 编程模型#

Spring Cloud Stream 编程模型:

名词 说明
Destination Binder(目标绑定器) 负责集成外部消息系统的组件
Destination Bindings(目标绑定) Binding 是连接外部消息系统、消息发送者和消息消费者的桥梁,由 Binder 创建。Binding 用来绑定消息容器的生产者和消费者,它有两种类型:INPUT 和 OUTPUT,INPUT 对应于消费者,OUTPUT 对应于生产者。
Message(消息) /

上图所示,微服务集成了 Stream,Stream 的 Destination Binder 创建了两个 Binding,左边的 Binding 连接 RabbitMQ,右边连接 Kafka。左边的 Binding 从 RabbitMQ 处消费消息,然后经过 Application 处代码的处理,把处理结果传输给 Kafka(从 RabbitMQ 处消费消息,然后经过处理,生产到 Kafka)。

1.4 通信方式#

在 Spring Cloud Stream 中的消息通信⽅式遵循了「发布-订阅模式」,当⼀条消息被投递到消息中间件之后,它会通过共享的 Topic 主题进⾏⼴播,消息消费者在订阅的主题中收到它并触发⾃身的业务逻辑处理。这⾥所提到的 Topic 主题是 Spring Cloud Stream 中的⼀个抽象概念,⽤来代表发布共享消息给消费者的地⽅。

在不同的消息中间件中, Topic 可能对应着不同的概念,⽐如:在 RabbitMQ 中的它对应了 Exchange、在 Kakfa 中则对应了 Kafka 中的 Topic。

2. 使用说明#

2.1 相关依赖#

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

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

2.2 标准流程#

名词 解释
Binder 绑定器,与消息中间件通信的组件;
Channel 通道,是队列的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过 Channel 对队列进行配置;
Source & Sink Source 源,即发送者 / Sink 水槽,即接收者;可理解为参照对象是 Spring Cloud Stream 自身,从 Stream 发布消息就是输出,接收消息就是输入。

3. 注解式编程#

3.1 编程注解#

注解完全是基于官方给的模型而定的!通过 Stream 使用消息中间件也是非常简单的,直接使用以下注解就可以使用。

如下的注解无非在做⼀件事,把我们结构图中那些组成部分上下关联起来,打通通道(这样的话生产者的 Msg 数据才能进入 MQ,MQ 中数据才能进入消费者工程)。

注意:官方明确表示注解已经被弃用,但弃用并不是不能用,而是用了会画横杠不建议用。但是功能是没有问题的,低版本的 cloud 是没有被弃用的。

案例设计:创建三个工程,消息中间件使用 RabbitMQ。

  • cloud-stream-producer-9090, 作为生产者端发消息
  • cloud-stream-consumer-9091,作为消费者端接收消息
  • cloud-stream-consumer-9092,作为消费者端接收消息

3.2 生产端开发#

(1)pom.xml 引入依赖

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

(2)application.yml 添加配置

server:
  port: 9090
spring:
  application:
    name: cloud-stream-producer
  cloud:
    stream:
      binders:                       # 绑定MQ服务信息(此处我们是RabbitMQ)
         treeRabbitBinder:           # 给Binder定义的名称,⽤于后⾯的关联
            type: rabbit             # MQ类型,如果是Kafka的话,此处配置'kafka'
             environment:            # MQ环境配置(⽤户名、密码等)
               spring:
                 rabbitmq:
                   host: localhost
                   port: 5672
                   username: guest
                   password: guest
      bindings:                      # 关联整合通道和binder对象
        output:                      # 'output'是我们定义的通道名称,此处不能乱改
          destination: studyExchange # 要使⽤的Exchange名称(消息队列主题名称)
          content-type: text/plain   # application/json # 消息类型设置,⽐如json
          binder: treeRabbitBinder   # 设置要绑定的消息服务的具体设置

(3)启动类

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

(4)业务类开发

发送消息接⼝:

public interface IMessageProducer {
  public void sendMessage(String content);
}

发送消息接口实现:

// Source.class⾥⾯就是对输出通道的定义(这是 Stream 内置的通道封装)
@EnableBinding(Source.class) // 可以理解为是一个消息的发送管道的定义
public class MessageProducerImpl implements IMessageProducer {
    
    @Autowired // 将MessageChannel的封装对象Source注⼊到这⾥使⽤
    private Source source;
  
    @Override
    public void sendMessage(String content) {
        // 使⽤通道向外发出消息(指的是Source⾥⾯的output通道)
        source.output().send(MessageBuilder.withPayload(content).build());
    }
}

// 内置通道
public interface Source {
  String OUTPUT = "output";
  
  @Output(Source.OUTPUT)
  MessageChannel output();
}

测试类:

@SpringBootTest(classes = { StreamProducerApplication9090.class })
@RunWith(SpringJUnit4ClassRunner.class)
public class MessageProducerTest {
    @Autowired
    private IMessageProducer iMessageProducer;
  
    @Test
    public void testSendMessage() {
        iMessageProducer.sendMessage("Hello Stream!");
    }
}

启动后会创建交换机,名称就是 application.yml 当中的 destination 属性设置的。

注意:停止服务后并没有删除交换机!

3.3 消费端开发#

此处我们记录 cloud-stream-consumer-9091 编写过程:

(1)application.yml

(2)消息消费者监听

@EnableBinding(Sink.class)
public class MessageConsumerService {
    @StreamListener(Sink.INPUT)
    public void recevieMessages(Message<String> message) {
        System.out.println("接收到的消息:" + message);
    }
}

// 内置通道
public interface Sink {
  String INPUT = "input";
  
  @Input(Source.INPUT)
  SubscribableChannel input();
}

补充:⾃定义消息通道

Stream 内置了两种接⼝ Source 和 Sink,分别定义了 binding 为 “input” 的输⼊流和“output” 的输出流,我们也可以⾃定义各种输⼊输出流(通道),但实际我们可以在我们的服务中使⽤多个 binder、多个输⼊通道和输出通道,然⽽默认就带了⼀个 input 的输⼊通道和⼀个 output 的输出通道,怎么办?

我们是可以⾃定义消息通道的,学着 Source 和 Sink 的样⼦,给你的通道定义个⾃⼰的名字,多个输⼊通道和输出通道是可以写在⼀个类中的。

(1)定义接口

interface CustomChannel {
    String INPUT_LOG = "inputLog";
    String OUTPUT_LOG = "outputLog";
  
    @Input(INPUT_LOG)
    SubscribableChannel inputLog();
  
    @Output(OUTPUT_LOG)
    MessageChannel outputLog();
}

(2)在 @EnableBinding 中绑定⾃定义的接⼝

(3)使⽤ @StreamListener 做监听的时候,需要指定 CustomChannel.INPUT_LOG

(4)写入配置文件

bindings:
  inputLog:
    destination: lagouExchange
  outputLog:
    destination: eduExchange

补充:消息分组

消费者端有两个(消费同⼀个 MQ 的同⼀个 Topic),但是我们的业务场景中希望这个 Topic 的⼀个 Message 只能被⼀个消费者端消费处理,此时我们就可以使⽤消息分组。

解决的问题:能解决消息重复消费问题我们仅仅需要在服务消费者端设置 spring.cloud.stream.bindings.input.group 属性,多个消费者实例配置为同⼀个 group 名称(在同⼀个 group 中的多个消费者只有⼀个可以获取到消息并消费)。

4. 函数式编程#

https://docs.spring.io/spring-cloud-stream/docs/current/reference/html/spring-cloud-stream.html#spring_cloud_function

自 Spring Cloud Stream v2.1 以来,定义流处理程序和源的另一种方法是使用对 Spring Cloud Function 的内置支持,其中它们可以表示为类型的 bean java.util.function.[Supplier/Function/Consumer]

要指定将哪个功能 bean 绑定到绑定公开的外部目标,您必须提供 spring.cloud.function.definition 属性。

如果您只有 java.util.function.[Supplier/Function/Consumer] 类型的单个 bean,则可以跳过 spring.cloud.function.definition 属性,因为将自动发现此类功能 bean。但是,使用此类属性以避免任何混淆被认为是最佳实践。有时,这种自动发现可能会造成阻碍,因为 java.util.function.[Supplier/Function/Consumer] 类型的单个 bean 可能会用于处理消息之外的其他目的,但由于是单个 bean,因此它是自动发现的并且是自动的-边界。对于这些罕见的情况,您可以通过提供 spring.cloud.stream.function.autodetect 属性并将值设置为 false 来禁用自动发现。

下面是应用程序将消息处理程序公开为 java.util.function.Function 的示例,该应用程序通过充当数据的使用者和生产者来有效地支持直通语义。

@SpringBootApplication
public class MyFunctionBootApp {

	public static void main(String[] args) {
		SpringApplication.run(MyFunctionBootApp.class);
	}

	@Bean
	public Function<String, String> toUpperCase() {
		return s -> s.toUpperCase();
	}
}

在前面的示例中,我们定义了一个名为 toUpperCase 的 java.util.function.Function 类型的 bean,充当消息处理程序,其“输入”和“输出”必须绑定到由所提供的目标绑定器公开的外部目标。默认情况下,“输入”和“输出”绑定名称将为 toUpperCase-in-0 和 toUpperCase-out-0。

有关用于建立绑定名称的命名约定的详细信息,请参阅功能绑定名称部分:https://docs.spring.io/spring-cloud-stream/docs/current/reference/html/spring-cloud-stream.html#_functional_binding_names

以下是支持其他语义的简单功能应用程序的示例:

以下是 java.util.function.Supplier 的 source 语义示例:

@SpringBootApplication
public static class SourceFromSupplier {

	@Bean
	public Supplier<Date> date() {
		return () -> new Date(12345L);
	}
}

以下是 java.util.function.Consumer 的 sink 语义的示例:

@SpringBootApplication
public static class SinkFromConsumer {

	@Bean
	public Consumer<String> sink() {
		return System.out::println;
	}
}

4.1 命名约定#

与以前版本的 spring-cloud-stream 中使用的基于注释的支持(遗留)所需的显式命名不同,函数式编程模型在绑定名称时默认采用简单的约定,从而大大简化了应用程序配置。我们看第一个例子:

@SpringBootApplication
public class SampleApplication {

	@Bean
	public Function<String, String> uppercase() {
	    return value -> value.toUpperCase();
	}
}

在前面的示例中,我们有一个具有单个函数的应用程序,该函数充当消息处理程序。作为一个 Function,它有一个输入和输出。用于命名输入和输出绑定的命名约定如下:

  • [input] <functionName> + -in- + <index>
  • [output] <functionName> + -out- + <index>

in 和 out 对应于绑定的类型(例如输入或输出)。索引是输入或输出绑定的索引。对于典型的单输入/输出函数,它始终为 0,因此它仅与具有多个输入和输出参数的函数相关。

因此,如果您想要将此函数的输入映射到名为“my-topic”的远程目的地(例如主题、队列等),您可以使用以下属性来实现:

spring.cloud.stream.bindings.uppercase-in-0.destination=my-topic

请注意 uppercase-in-0 是如何用作属性名称中的段的。对于 uppercase-out-0 也是如此。

描述性绑定名称

有时为了提高可读性,您可能需要为绑定指定一个更具描述性的名称(例如 'account', 'orders' 等)。另一种看待它的方法是,您可以将隐式绑定名称映射到显式绑定名称。您可以使用 spring.cloud.stream.function.bindings.<binding-name> 属性来完成此操作。此属性还为依赖于需要显式名称的基于自定义接口的绑定的现有应用程序提供迁移路径。

比如说:

spring.cloud.stream.function.bindings.uppercase-in-0=input

在前面的示例中,您将 uppercase-in-0 绑定名称映射并有效重命名为 input。现在,所有配置属性都可以引用输入绑定名称(e.g. spring.cloud.stream.bindings.input.destination=my-topic)。

注意:

虽然描述性绑定名称可以增强配置的可读性,但它们也会通过将隐式绑定名称映射到显式绑定名称而产生另一级别的误导。由于所有后续配置属性都将使用显式绑定名称,因此您必须始终引用此 bindings 属性来关联它实际对应的函数。我们认为,对于大多数情况(功能组合除外),这可能是一种矫枉过正,因此,我们建议完全避免使用它,特别是因为不使用它可以在活页夹目标和绑定名称之间提供清晰的路径,例如 spring.cloud.stream.bindings.uppercase-in-0.destination=sample-topic,您可以清楚地将 uppercase[Function] 的输入与 sample-topic 相关联。

4.2 发布数据#

在 Spring Cloud Stream 中,发布数据的方式主要有两种,一种是通过 Supplier 自动触发,一种是通过 StreamBridge 通过外部数据源触发。

a. Supplier#

使用 Supplier<T> 作为 stream 的发布者,需要一种触发机制,来发起发布操作。Spring Cloud Stream 提供了如下几种自动触发机制。

@Bean
Supplier<String> stringSupplier() {
    return () -> {
        String value = "Hello World!";System.out.println("sent: " + value);
        return value;
    };
}

我们可以通过两种方式来调整触发频率:

  • 【全局属性】spring.integration.poller.xxx 设置所有函数的拉取频率
  • 【每个绑定特定的属性】spring.cloud.stream.bindings.<binding-name>.producer.poller.xxx 为某个特定的绑定设置拉取频率

反应式(Reactive)编程模式的触发

使用反应式编程模式时,Supplier<T> 默认只会触发一次,这对于无限流是合适的。对于有限流,如果想多次触发,可以借助于 @PollableBean 注解。

(1)无限流

@Bean
Supplier<Flux<String>> stringSupplier() {
    return () -> Flux.fromStream(Stream.generate(() -> {
        try {
            TimeUnit.SECONDS.sleep(1);
            String value = "Hello World!!";
            log.info(">>> {}", value);
            return value;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    })).subscribeOn(Schedulers.boundedElastic()).share();
}

(2)有限流

@PollableBean
Supplier<Flux<String>> stringSupplier() {
    return () -> {
        System.out.println("sending...");
        return Flux.just("Hello", "World", "!");
    };
}

b. StreamBridge#

如果要发布的数据来自于 REST 请求,或者其他的外部源系统,则可以使用 StreamBridge 这个 bean 来将外部源的数据发布到 Spring Cloud Stream 中。

基本使用

比如,下面是通过 GET 请求发布数据:

@GetMapping("/send")
public String send() {
    if (streamBridge.send("my-binding", "Hello World!")) {
        return "sent";
    } else {
        return "NOT sent";
    }
}

上述 send 方法的第一个参数,就是 bindingName,这里是 my-binding,可以是提前通过属性指定的,如果没有指定,那么会自动创建一个 binding。

spring:
  cloud:
    stream:
      output-bindings: my-binding

建议提前配置好 bindingName,否则可能会造成内存溢出。为了防止内存溢出,可以通过以下属性来限制动态创建 binding 的数量。

spring.cloud.stream.dynamic-destination-cache-size=5

使用 Interceptor

因为 StreamBridge 使用 MessageChannel 来建立发布的 binding,所以可以利用ChannelInterceptor 来拦截发布过程。比如:

@Component 
@GlobalChannelInterceptor(patterns = "*") 
public class MyInterceptor implements ChannelInterceptor {

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        System.out.println(message.getHeaders());
        Object payload = message.getPayload();
        if (payload instanceof byte[] data) {
            System.out.println(new String(data));
        } else {
            System.out.println(message.getPayload());
        }
        return message;
    }
}

ChannleInterceptor 的实现类需要注册到 Spring 容器中。使用 GlobalChannelIntercptor 来标注这个拦截器,参数 patterns 的值可以控制该拦截器的硬性范围

  • * 表示拦截所有 binding
  • foo-* 表示只拦截那些以 foo- 开头的 binding

4.3 消费数据#

可以使用多种方式来消费 Stream 中的数据,最常见的方式是通过 Consumer<T> 函数来消费数据。除此之外,也可以通过所谓的“拉取式消费者”来消费数据。

a. 反应式的 Consumer#

对于命令式编程模式,使用 Consumer<T> 很直观,不赘述。

对于反应式的 Consumer,由于它没有返回值,导致 Spring Framework 无法自动 subscribe 它,所以需要一些特殊处理。有两种方式:

(1)使用 Function<Flux<T>,Mono<Void>

建议的方式,是不用 Consumer<Flux<T>>,而是使用 Function<Flux<T>,Mono<Void>> 来保证有一个返回值,让 Spring Framework可以订阅。

@Bean
Function<Flux<String>, Mono<Void>> printText() {
    return flux -> flux.map(value -> {
        log.info("received: " + value);
        return value;
    }).then();
}

具体的操作,可以放在 map 等的算子中。

(2)使用 Consumer<Flux<T>> 主动订阅

如果必须使用 Consumer<Flux<T>>,则需要自己编写订阅代码,如下:

@Bean
Consumer<Flux<String>>> printText() {
    return flux -> {
        flux.subscribe(System.out::println);
    };
}

b. 函数组合#

利用 Spring Cloud Function 的函数组合功能,可以将多个 Function<T,R>Consumer<T> 组合在一起(当然 Consumer<T> 只能在最后),形成一个运行时的整体函数。比如:

spring:
  cloud:
    function:
      definition: uppercase|quote|printText
    stream:
      function:
        bindings:
          uppercase|quote|printText-in-0: convert
      bindings:
        convert:
          destination: my-binding
  • spring.cloud.function.definition 指定了应用程序中定义的功能 Bean 名称,这里是两个 Function(uppercase 和 quote)和一个 Consumer(printText),他们共同组成一个运行时函数;
  • spring.cloud.stream.function.bindings 指定功能之间的绑定关系。在这个例子中,uppercase, quote, printText 的输入通道 (in-0) 绑定到了名为 convert 的通道。
  • spring.cloud.stream.bindings.convert: 指定了 convert 通道绑定的目的地(destination),这里是 my-binding

使用函数组合,可以很方便地执行切面操作,比如上述案例中的 quote 其实可以看作一个切面,它对输入做了 quote 的增强。

c. 多输入/输出参数#

在需要合并/分流 Stream 的场景下,会涉及到多个输入/输出参数。可以利用 Project Reactor 提供的 Tuple 来处理。

@Bean
public Function<Tuple2<Flux<String>, Flux<Integer>>, Flux<String>> gather() {
    return tuple -> {
        Flux <String> stringStream = tuple.getT1();
        Flux <String> intStream = tuple.getT2().map(i -> String.valueOf(i));
        return Flux.merge(stringStream, intStream);
    };
}

对应的绑定器命名如下:

  • gather-in-0:第一个输入流的绑定器
  • gather-in-1:第二个输入流的绑定器
  • gather-out-0:第一个输出流的绑定器

d. 使用 PollableMessageSource#

如果不想使用函数式编程,那么 Spring Framework 提供了 PollableMessageSource 来“拉取”Stream 中的数据。

@Bean ApplicationRunner applicationRunner(PollableMessageSource source) {
    return args -> {
        while (true) {
            if (!source.poll(m -> {
                    if (m.getPayload() instanceof byte[] data) {
                        System.out.println(new String(data));
                    } else {
                        System.out.println(m.getPayload());
                    }
                })) {
                TimeUnit.SECONDS.sleep(1);
            }
        }
    };
}

4.4 完整案例#

使用 Spring Cloud Stream 的函数式编程模式,可以使用很少的代码实现数据流的发布、流转和消费。下面给个完整案例。

在使用函数式编程的时候一定不要用之前的注解,不然函数式编程会失效的!这个一定要注意!

a. 快速开始#

(1)生产者

spring:
  rabbitmq:
    host: localhost
    username: guest
    password: guest
    port: 5672
  cloud:
    function:
      # 代表的是使用哪些函数(函数bean名称),如果bindings有多个则这块名称也可以写多个(通过'分号'拼起来即可)
      definition: source
    stream:
      bindings:
        source-out-0:           # 绑定名称
          destination: transfer # 通道,如果用的是RabbitMQ就是交换机名称

启动类中添加即可:

@Bean // 供应商函数,就是发送消息者,供应消息的
public Supplier<String> source() {
    return () -> {
        String message = "from source";
        System.out.println("------------------from source--------------");
        return message;
    };
}

(2)消费者

消费者配置如下:binding 名称为 sink-in-0 代表的是,他要将这个 binding 绑定到 Spring 容器当中名称为 sink 的对象,in 代表的就是输入的意思。而输入可以理解为就是接收消息。后面那个 index 一般用不到,设置为 0 即可。当然在下面的合流场景,我们就用到了。

spring:
  rabbitmq:
    host: localhost
    username: guest
    password: guest
    port: 5672
  cloud:
    function:
      # 代表的是使用哪些函数
      definition: sink
    stream:
      bindings:
        sink-in-0:              # binding的名称
          destination: transfer # 通道,如果用的是RabbitMQ就是交换机名称
          group: sink           # 分组

启动类中添加即可:

// Consumer消费函数,在stream当中用来当中接受消息者
@Bean
public Consumer<String> sink() {
  return message -> {
    System.out.println("----------------sink:" + message + "---------------");
  };
}
  • 通过 @Bean 注解注入到容器里面,默认的 Bean 名称就是方法名。
  • Consumer 函数是传入一个对象,没有返回值,所以 SCS 就利用这个来绑定我们的消息。首先通过配置文件的 binding 名称 sink-in-0 找到了容器当中的 Bean 名称为 sink 的 Consumer 函数,然后监听到 transfer 通道有消息后,直接发消息给 Consumer 函数,由它进行消费。

(3)启动测试

启动之后生产者会一直发送消息,而消费者也会一直消费消息。

启动后会发现创建了 transfer 交换机,然后绑定了一个如下队列,通过这个队列一直保持发送和消费。

为什么会一直发送消息呢?

b. 手动发消息#

通过上面示例我们发现,他是每秒自动发送消息的,实际开发当中很少会有这种场景,一般都是手动发送消息,那么使用函数式编程,怎么发送消息呢?

首先明确一点,函数式编程的生产者不需要函数来发送消息,它是通过 StreamBridge 来发送消息的。

@Autowired
private StreamBridge streamBridge;

@GetMapping("send2")
public String send2() {
    String serial = UUID.randomUUID().toString();
    // 创建并发送消息
    System.out.println("***serial: " + serial);
    // 第一个参数:绑定名称,就是application当中配置的通道名称
    // 第二个参数:消息
    streamBridge.send("output", serial);
    return serial;
}

其次还需要修改生产者配置文件:这样发送消息,消费者就可以通过那个函数来进行监听了。

# 正常的发送,然后消费者通过函数式编程来进行监听
spring:
  rabbitmq:
    host: localhost
    username: guest
    password: guest
    port: 5672
  cloud:
    stream:
      bindings:
        output:                 # 绑定名称
          destination: transfer # 通道,如果用的是RabbitMQ就是交换机名称

c. Binding 的多端合流#

所谓合流就是将两个队列当中的消息合为一个。

(1)生产者

# Binding的多端合流
spring:
  rabbitmq:
    host: localhost
    username: guest
    password: guest
    port: 5672
  cloud:
    stream:
      bindings:
        gather1:              # 绑定名称
          destination: input1 # 通道,如果用的是RabbitMQ就是交换机名称
        gather2:              # 绑定名称
          destination: input2 # 通道,如果用的是RabbitMQ就是交换机名称

生产者发送消息:

@Autowired
private StreamBridge streamBridge;

//  Binding的多端合流测试
@GetMapping("send3")
public String send3(String message,String message2) {
    streamBridge.send("gather1", message);
    streamBridge.send("gather2", message2);
    return "Message sended";
}

(2)消费者

# Binding的多端合流
spring:
  rabbitmq:
    host: localhost
    username: guest
    password: guest
    port: 5672
  cloud:
    function:
      #代表的是使用哪些函数
      definition: gather;sink
    stream:
      bindings:
      	# 通过定义名称gather-in-0和gather-in-1来监听两个交换机的消息,gather函数就尤其重要了
        gather-in-0:
          destination: input1
          group: gather1
        gather-in-1:
          destination: input2
          group: gather2
        # gather合并流之后转发到transfer
        gather-out-0:
          destination: transfer
        # 监听transfer,然后并使用sink函数输出
        sink-in-0:
          destination: transfer
          group: sink

启动类当中将如下两个函数注入到容器当中。

// Consumer消费函数,在stream当中用来当中接受消息者
@Bean
public Consumer<String> sink() {
  return message -> {
    System.out.println("----------------sink:" + message + "---------------");
  };
}

// 通过这个函数进行将流拼接到一块,并输出出去
@Bean
public Function<Tuple2<Flux<String>, Flux<String>>, Flux<String>> gather() {
  return tuple -> {
    Flux<String> f1 = tuple.getT1();
    Flux<String> f2 = tuple.getT2();
    // return Flux.merge(f1, f2);
    return Flux.combineLatest(f1, f2, (str1, str2) -> str1 + ":" + str2);
  };
}

(3)测试

请求 http://localhost:8801/send3?message=1&message2=aaa 后消费者监听到消息,并进行合流输出!

【小结】只要消息进入到中间件当中,就通过配置 binding 名称和函数的搭配 任意的转发消息、监听消息并处理消息,而消息它就跟流一样可以被任意改动。消息可扩展性极高。只需要通过一个简单的配置就可以更改消息(消息就可以当作是流)的去向。上面消息消费者的配置当中就体现出来了这一点!合流之后将流转发到 transfer 通道,再由 binding 名称为 sink-in-0 的来监听 transfer 通道!

posted @   tree6x7  阅读(20)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示
主题色彩