Apache Kafka 支持
概述
Spring Integration for Apache Kafka 基于 Spring for Apache Kafka 项目。
此依赖项为项目所需:
- Maven
- Gradle
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-kafka</artifactId>
<version>7.0.2</version>
</dependency>
compile "org.springframework.integration:spring-integration-kafka:7.0.2"
它提供以下组件:
出站通道适配器
出站通道适配器用于将消息从 Spring Integration 通道发布到 Apache Kafka 主题。该通道在应用程序上下文中定义,随后被连接到向 Apache Kafka 发送消息的应用程序中。发送方应用程序可以通过使用 Spring Integration 消息来发布到 Apache Kafka,这些消息在内部由出站通道适配器转换为 Kafka 记录,具体如下:
-
Spring Integration 消息的有效负载用于填充 Kafka 记录的有效负载。
-
默认情况下,Spring Integration 消息的
kafka_messageKey头用于填充 Kafka 记录的键。
您可以通过 kafka_topic 和 kafka_partitionId 头部分别自定义消息发布的目标主题和分区。
此外,<int-kafka:outbound-channel-adapter> 还支持通过对出站消息应用 SpEL 表达式来提取键、目标主题和目标分区。为此,它支持三组互斥的属性:
-
topic和topic-expression -
message-key和message-key-expression -
partition-id和partition-id-expression
这些功能允许您分别指定 topic、message-key 和 partition-id,既可以在适配器上设置为静态值,也可以在运行时根据请求消息动态计算它们的值。
KafkaHeaders 接口(由 spring-kafka 提供)包含了用于与消息头交互的常量。messageKey 和 topic 这两个默认消息头现在需要加上 kafka_ 前缀。当从使用旧消息头的早期版本迁移时,你需要在 <int-kafka:outbound-channel-adapter> 上指定 message-key-expression="headers['messageKey']" 和 topic-expression="headers['topic']"。或者,你也可以通过使用 <header-enricher> 或 MessageBuilder 在上游将消息头更改为来自 KafkaHeaders 的新消息头。如果你使用常量值,也可以通过 topic 和 message-key 在适配器上进行配置。
注意:如果适配器配置了主题或消息键(无论是常量还是表达式),则会使用这些配置,并忽略相应的标头。如果您希望标头覆盖配置,则需要在表达式中进行配置,例如:
topic-expression="headers['topic'] != null ? headers['topic'] : 'myTopic'"
适配器需要一个 KafkaTemplate,而该模板又需要一个配置得当的 KafkaProducerFactory。
如果提供了 send-failure-channel(sendFailureChannel)并且接收到 send() 失败(同步或异步),则会向该通道发送一条 ErrorMessage。其有效载荷是一个 KafkaSendFailureException,包含 failedMessage、record(即 ProducerRecord)和 cause 属性。你可以通过设置 error-message-strategy 属性来覆盖 DefaultErrorMessageStrategy。
如果提供了 send-success-channel (sendSuccessChannel),则在成功发送后,会发送一条负载类型为 org.apache.kafka.clients.producer.RecordMetadata 的消息。
如果你的应用程序使用了事务,并且同一个通道适配器既用于在监听器容器启动的事务中发布消息,也用于在没有现有事务的情况下发布消息,你必须在 KafkaTemplate 上配置一个 transactionIdPrefix 来覆盖容器或事务管理器使用的前缀。由容器启动的事务(生产者工厂或事务管理器属性)使用的前缀必须在所有应用程序实例上保持一致。而仅用于生产者的事务前缀必须在所有应用程序实例上保持唯一。
你可以配置一个 flushExpression,它必须解析为布尔值。如果你正在使用 linger.ms 和 batch.size 这些 Kafka 生产者属性,在发送多条消息后进行刷新可能会很有用;该表达式应在最后一条消息上评估为 Boolean.TRUE,未完成的批次将立即发送。默认情况下,该表达式会在 KafkaIntegrationHeaders.FLUSH 标头(kafka_flush)中查找一个 Boolean 值。如果该值为 true,则执行刷新;如果为 false 或标头不存在,则不执行刷新。
KafkaProducerMessageHandler.sendTimeoutExpression 的默认值已从 10 秒更改为 Kafka 生产者属性 delivery.timeout.ms + 5000,以便将超时后实际的 Kafka 错误传播到应用程序,而不是由该框架生成的超时。进行此更改是为了保持一致性,因为您可能会遇到意外行为(Spring 可能会超时发送,而实际上最终发送成功)。重要提示:该超时时间默认为 120 秒,因此您可能希望减少它以获得更及时的失败反馈。
配置
以下示例展示了如何为 Apache Kafka 配置出站通道适配器:
- Java DSL
- Java
- XML
@Bean
public ProducerFactory<Integer, String> producerFactory() {
return new DefaultKafkaProducerFactory<>(KafkaTestUtils.producerProps(embeddedKafka));
}
@Bean
public IntegrationFlow sendToKafkaFlow() {
return f -> f
.splitWith(s -> s.<String>function(p -> Stream.generate(() -> p).limit(101).iterator()))
.publishSubscribeChannel(c -> c
.subscribe(sf -> sf.handle(
kafkaMessageHandler(producerFactory(), TEST_TOPIC1)
.timestampExpression("T(Long).valueOf('1487694048633')"),
e -> e.id("kafkaProducer1")))
.subscribe(sf -> sf.handle(
kafkaMessageHandler(producerFactory(), TEST_TOPIC2)
.timestamp(m -> 1487694048644L),
e -> e.id("kafkaProducer2")))
);
}
@Bean
public DefaultKafkaHeaderMapper mapper() {
return new DefaultKafkaHeaderMapper();
}
private KafkaProducerMessageHandlerSpec<Integer, String, ?> kafkaMessageHandler(
ProducerFactory<Integer, String> producerFactory, String topic) {
return Kafka
.outboundChannelAdapter(producerFactory)
.messageKey(m -> m
.getHeaders()
.get(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER))
.headerMapper(mapper())
.partitionId(m -> 10)
.topicExpression("headers[kafka_topic] ?: '" + topic + "'")
.configureKafkaTemplate(t -> t.id("kafkaTemplate:" + topic));
}
@Bean
@ServiceActivator(inputChannel = "toKafka")
public MessageHandler handler() throws Exception {
KafkaProducerMessageHandler<String, String> handler =
new KafkaProducerMessageHandler<>(kafkaTemplate());
handler.setTopicExpression(new LiteralExpression("someTopic"));
handler.setMessageKeyExpression(new LiteralExpression("someKey"));
handler.setSuccessChannel(successes());
handler.setFailureChannel(failures());
return handler;
}
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, this.brokerAddress);
// set more properties
return new DefaultKafkaProducerFactory<>(props);
}
<int-kafka:outbound-channel-adapter id="kafkaOutboundChannelAdapter"
kafka-template="template"
auto-startup="false"
channel="inputToKafka"
topic="foo"
sync="false"
message-key-expression="'bar'"
send-failure-channel="failures"
send-success-channel="successes"
error-message-strategy="ems"
partition-id-expression="2">
</int-kafka:outbound-channel-adapter>
<bean id="template" class="org.springframework.kafka.core.KafkaTemplate">
<constructor-arg>
<bean class="org.springframework.kafka.core.DefaultKafkaProducerFactory">
<constructor-arg>
<map>
<entry key="bootstrap.servers" value="localhost:9092" />
... <!-- more producer properties -->
</map>
</constructor-arg>
</bean>
</constructor-arg>
</bean>
消息驱动通道适配器
KafkaMessageDrivenChannelAdapter (<int-kafka:message-driven-channel-adapter>) 使用一个 spring-kafka 的 KafkaMessageListenerContainer 或 ConcurrentListenerContainer。
此外,mode 属性也是可用的。它可以接受 record 或 batch 值(默认:record)。对于 record 模式,每个消息负载是从单个 ConsumerRecord 转换而来的。对于 batch 模式,负载是一个对象列表,这些对象是从消费者轮询返回的所有 ConsumerRecord 实例转换而来的。与批处理的 @KafkaListener 一样,KafkaHeaders.RECEIVED_KEY、KafkaHeaders.RECEIVED_PARTITION、KafkaHeaders.RECEIVED_TOPIC 和 KafkaHeaders.OFFSET 头部也是列表,其位置与负载中的位置相对应。
接收到的消息会填充特定的头部信息。更多详情请参阅 KafkaHeaders 类。
Consumer 对象(位于 kafka_consumer 头文件中)不是线程安全的。您必须仅在适配器内调用监听器的线程上调用其方法。如果将消息传递给另一个线程,则不得调用其方法。
当提供了 retry-template 时,投递失败将根据其重试策略进行重试。如果同时提供了 error-channel,在重试耗尽后将使用默认的 ErrorMessageSendingRecoverer 作为恢复回调。你也可以使用 recovery-callback 来指定在该情况下采取的其他操作,或将其设置为 null 以将最终异常抛给监听器容器,从而在那里进行处理。
在构建 ErrorMessage(用于 error-channel 或 recovery-callback)时,你可以通过设置 error-message-strategy 属性来自定义错误消息。默认情况下,会使用 RawRecordHeaderErrorMessageStrategy,以提供对转换后的消息以及原始 ConsumerRecord 的访问。
这种重试形式是阻塞式的,如果所有轮询记录的总重试延迟可能超过消费者属性 max.poll.interval.ms,则可能导致重新平衡。相反,请考虑向监听器容器添加一个 DefaultErrorHandler,并配置一个 KafkaErrorSendingMessageRecoverer。
配置
以下示例展示了如何配置消息驱动通道适配器:
- Java DSL
- Java
- XML
@Bean
public IntegrationFlow topic1ListenerFromKafkaFlow() {
return IntegrationFlow
.from(Kafka.messageDrivenChannelAdapter(consumerFactory(),
KafkaMessageDrivenChannelAdapter.ListenerMode.record, TEST_TOPIC1)
.configureListenerContainer(c ->
c.ackMode(AbstractMessageListenerContainer.AckMode.MANUAL)
.id("topic1ListenerContainer"))
.recoveryCallback(new ErrorMessageSendingRecoverer(errorChannel(),
new RawRecordHeaderErrorMessageStrategy()))
.retryTemplate(new RetryTemplate())
.filterInRetry(true))
.filter(Message.class, m ->
m.getHeaders().get(KafkaHeaders.RECEIVED_MESSAGE_KEY, Integer.class) < 101,
f -> f.throwExceptionOnRejection(true))
.<String, String>transform(String::toUpperCase)
.channel(c -> c.queue("listeningFromKafkaResults1"))
.get();
}
@Bean
public KafkaMessageDrivenChannelAdapter<String, String>
adapter(KafkaMessageListenerContainer<String, String> container) {
KafkaMessageDrivenChannelAdapter<String, String> kafkaMessageDrivenChannelAdapter =
new KafkaMessageDrivenChannelAdapter<>(container, ListenerMode.record);
kafkaMessageDrivenChannelAdapter.setOutputChannel(received());
return kafkaMessageDrivenChannelAdapter;
}
@Bean
public KafkaMessageListenerContainer<String, String> container() throws Exception {
ContainerProperties properties = new ContainerProperties(this.topic);
// set more properties
return new KafkaMessageListenerContainer<>(consumerFactory(), properties);
}
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, this.brokerAddress);
// set more properties
return new DefaultKafkaConsumerFactory<>(props);
}
<int-kafka:message-driven-channel-adapter
id="kafkaListener"
listener-container="container1"
auto-startup="false"
phase="100"
send-timeout="5000"
mode="record"
retry-template="template"
recovery-callback="callback"
error-message-strategy="ems"
channel="someChannel"
error-channel="errorChannel" />
<bean id="container1" class="org.springframework.kafka.listener.KafkaMessageListenerContainer">
<constructor-arg>
<bean class="org.springframework.kafka.core.DefaultKafkaConsumerFactory">
<constructor-arg>
<map>
<entry key="bootstrap.servers" value="localhost:9092" />
...
</map>
</constructor-arg>
</bean>
</constructor-arg>
<constructor-arg>
<bean class="org.springframework.kafka.listener.config.ContainerProperties">
<constructor-arg name="topics" value="foo" />
</bean>
</constructor-arg>
</bean>
您也可以使用用于 @KafkaListener 注解的容器工厂来创建用于其他目的的 ConcurrentMessageListenerContainer 实例。有关示例,请参阅 Spring for Apache Kafka 文档。
使用 Java DSL 时,容器不必配置为 @Bean,因为 DSL 会将容器注册为 bean。以下示例展示了如何实现:
@Bean
public IntegrationFlow topic2ListenerFromKafkaFlow() {
return IntegrationFlow
.from(Kafka.messageDrivenChannelAdapter(kafkaListenerContainerFactory().createContainer(TEST_TOPIC2),
KafkaMessageDrivenChannelAdapter.ListenerMode.record)
.id("topic2Adapter"))
...
get();
}
请注意,在这种情况下,适配器被赋予了一个 id(topic2Adapter)。该容器在应用程序上下文中注册时使用的名称是 topic2Adapter.container。如果适配器没有 id 属性,容器的 bean 名称将是容器的完全限定类名加上 #n,其中 n 会为每个容器递增。
入站通道适配器
KafkaMessageSource 提供了一个可轮询的通道适配器实现。
配置
- Java DSL
- Kotlin
- Java
- XML
@Bean
public IntegrationFlow flow(ConsumerFactory<String, String> cf) {
return IntegrationFlow.from(Kafka.inboundChannelAdapter(cf, new ConsumerProperties("myTopic")),
e -> e.poller(Pollers.fixedDelay(5000)))
.handle(System.out::println)
.get();
}
@Bean
fun sourceFlow(cf: ConsumerFactory<String, String>) =
integrationFlow(Kafka.inboundChannelAdapter(cf,
ConsumerProperties(TEST_TOPIC3).also {
it.groupId = "kotlinMessageSourceGroup"
}),
{ poller(Pollers.fixedDelay(100)) }) {
handle { m ->
}
}
@InboundChannelAdapter(channel = "fromKafka", poller = @Poller(fixedDelay = "5000"))
@Bean
public KafkaMessageSource<String, String> source(ConsumerFactory<String, String> cf) {
ConsumerProperties consumerProperties = new ConsumerProperties("myTopic");
consumerProperties.setGroupId("myGroupId");
consumerProperties.setClientId("myClientId");
retunr new KafkaMessageSource<>(cf, consumerProperties);
}
<int-kafka:inbound-channel-adapter
id="adapter1"
consumer-factory="consumerFactory"
consumer-properties="consumerProperties1"
ack-factory="ackFactory"
channel="inbound"
message-converter="converter"
payload-type="java.lang.String"
raw-header="true"
auto-startup="false">
<int:poller fixed-delay="5000"/>
</int-kafka:inbound-channel-adapter>
<bean id="consumerFactory" class="org.springframework.kafka.core.DefaultKafkaConsumerFactory">
<constructor-arg>
<map>
<entry key="max.poll.records" value="1"/>
</map>
</constructor-arg>
</bean>
<bean id="consumerProperties1" class="org.springframework.kafka.listener.ConsumerProperties">
<constructor-arg name="topics" value="topic1"/>
<property name="groupId" value="group"/>
<property name="clientId" value="client"/>
</bean>
请参考 Javadocs 以了解可用的属性。
默认情况下,max.poll.records 必须在消费者工厂中显式设置,否则如果消费者工厂是 DefaultKafkaConsumerFactory,该值将被强制设为 1。您可以将属性 allowMultiFetch 设置为 true 来覆盖此行为。
必须在 max.poll.interval.ms 内轮询消费者以避免发生再均衡。如果将 allowMultiFetch 设置为 true,则必须在 max.poll.interval.ms 内处理完所有获取的记录并再次进行轮询。
此适配器发出的消息包含一个标头 kafka_remainingRecords,其中记录了前一次轮询中剩余的记录数量。
从版本 6.2 开始,KafkaMessageSource 支持在消费者属性中配置 ErrorHandlingDeserializer。DeserializationException 会从记录头中提取并抛出给调用者。对于 SourcePollingChannelAdapter,此异常会被包装成 ErrorMessage 并发布到其 errorChannel。更多信息请参阅 ErrorHandlingDeserializer 文档。
出站网关
出站网关用于请求/回复操作。它与大多数Spring Integration网关的不同之处在于,发送线程不会在网关中阻塞,回复会在回复监听器容器线程上进行处理。如果你的代码在同步Messaging Gateway后调用网关,用户线程将在该处阻塞,直到收到回复(或发生超时)。
KafkaProducerMessageHandler 的 sendTimeoutExpression 默认值为 Kafka 生产者属性 delivery.timeout.ms + 5000,这样超时后实际的 Kafka 错误会传播到应用程序,而不是由本框架生成的超时。为了保持一致性,这一点已作更改,因为您可能会遇到意外行为(Spring 可能会使 send() 超时,而实际上它最终是成功的)。重要提示:该超时时间默认为 120 秒,因此您可能希望减少它以获得更及时的失败反馈。
配置
以下示例展示了如何配置网关:
- Java DSL
- Java
- XML
@Bean
public IntegrationFlow outboundGateFlow(
ReplyingKafkaTemplate<String, String, String> kafkaTemplate) {
return IntegrationFlow.from("kafkaRequests")
.handle(Kafka.outboundGateway(kafkaTemplate))
.channel("kafkaReplies")
.get();
}
@Bean
@ServiceActivator(inputChannel = "kafkaRequests", outputChannel = "kafkaReplies")
public KafkaProducerMessageHandler<String, String> outGateway(
ReplyingKafkaTemplate<String, String, String> kafkaTemplate) {
return new KafkaProducerMessageHandler<>(kafkaTemplate);
}
<int-kafka:outbound-gateway
id="allProps"
error-message-strategy="ems"
kafka-template="template"
message-key-expression="'key'"
order="23"
partition-id-expression="2"
reply-channel="replies"
reply-timeout="43"
request-channel="requests"
requires-reply="false"
send-success-channel="successes"
send-failure-channel="failures"
send-timeout-expression="44"
sync="true"
timestamp-expression="T(System).currentTimeMillis()"
topic-expression="'topic'"/>
请参考 Javadocs 以了解可用属性。
请注意,这里使用了与出站通道适配器相同的类,唯一的区别在于传递给构造函数的 KafkaTemplate 是一个 ReplyingKafkaTemplate。更多信息请参阅Spring for Apache Kafka 文档。
出站主题、分区、键等内容的确定方式与出站适配器相同。回复主题的确定方式如下:
-
名为
KafkaHeaders.REPLY_TOPIC的消息头(如果存在,其值必须为String或byte[]类型)将根据模板的回复容器的订阅主题进行验证。 -
如果模板的
replyContainer仅订阅了一个主题,则使用该主题。
您还可以指定 KafkaHeaders.REPLY_PARTITION 标头,以确定用于回复的特定分区。同样,这也会根据模板的回复容器的订阅进行验证。
或者,你也可以使用类似以下 bean 的配置:
@Bean
public IntegrationFlow outboundGateFlow() {
return IntegrationFlow.from("kafkaRequests")
.handle(Kafka.outboundGateway(producerFactory(), replyContainer())
.configureKafkaTemplate(t -> t.replyTimeout(30_000)))
.channel("kafkaReplies")
.get();
}
入站网关
入站网关用于请求/回复操作。
配置
以下示例展示了如何配置入站网关:
- Java DSL
- Java
- XML
@Bean
public IntegrationFlow serverGateway(
ConcurrentMessageListenerContainer<Integer, String> container,
KafkaTemplate<Integer, String> replyTemplate) {
return IntegrationFlow
.from(Kafka.inboundGateway(container, replyTemplate)
.replyTimeout(30_000))
.<String, String>transform(String::toUpperCase)
.get();
}
@Bean
public KafkaInboundGateway<Integer, String, String> inboundGateway(
AbstractMessageListenerContainer<Integer, String>container,
KafkaTemplate<Integer, String> replyTemplate) {
KafkaInboundGateway<Integer, String, String> gateway =
new KafkaInboundGateway<>(container, replyTemplate);
gateway.setRequestChannel(requests);
gateway.setReplyChannel(replies);
gateway.setReplyTimeout(30_000);
return gateway;
}
<int-kafka:inbound-gateway
id="gateway1"
listener-container="container1"
kafka-template="template"
auto-startup="false"
phase="100"
request-timeout="5000"
request-channel="nullChannel"
reply-channel="errorChannel"
reply-timeout="43"
message-converter="messageConverter"
payload-type="java.lang.String"
error-message-strategy="ems"
retry-template="retryTemplate"
recovery-callback="recoveryCallback"/>
请参考 Javadocs 以了解可用属性。
当提供 RetryTemplate 时,将根据其重试策略对投递失败进行重试。如果同时配置了 error-channel,在重试耗尽后将使用默认的 ErrorMessageSendingRecoverer 作为恢复回调。您也可以通过 recovery-callback 指定在该情况下执行其他操作,或将其设置为 null 以将最终异常抛给监听器容器,由容器处理该异常。
在构建 ErrorMessage(用于 error-channel 或 recovery-callback)时,您可以通过设置 error-message-strategy 属性来自定义错误消息。默认情况下,系统会使用 RawRecordHeaderErrorMessageStrategy,以便提供对转换后的消息以及原始 ConsumerRecord 的访问。
这种重试形式是阻塞式的,如果所有轮询记录的总重试延迟可能超过消费者属性 max.poll.interval.ms,则可能导致重新平衡。建议改为在监听器容器中添加 DefaultErrorHandler,并配置 KafkaErrorSendingMessageRecoverer。
以下示例展示了如何使用 Java DSL 配置一个简单的大写转换器:
或者,您也可以通过以下类似的代码来配置一个大写转换器:
@Bean
public IntegrationFlow serverGateway() {
return IntegrationFlow
.from(Kafka.inboundGateway(consumerFactory(), containerProperties(),
producerFactory())
.replyTimeout(30_000))
.<String, String>transform(String::toUpperCase)
.get();
}
您也可以使用用于 @KafkaListener 注解的容器工厂来创建用于其他目的的 ConcurrentMessageListenerContainer 实例。有关示例,请参阅 Spring for Apache Kafka 文档 和 消息驱动通道适配器。
基于 Apache Kafka 主题的通道
Spring Integration 提供了由 Apache Kafka 主题支持的 MessageChannel 实现,用于持久化。
每个通道在发送端都需要一个 KafkaTemplate,对于可订阅通道则需要一个监听器容器工厂,而对于可轮询通道则需要一个 KafkaMessageSource。
Java DSL 配置
- Java DSL
- Java
- XML
@Bean
public IntegrationFlow flowWithSubscribable(KafkaTemplate<Integer, String> template,
ConcurrentKafkaListenerContainerFactory<Integer, String> containerFactory) {
return IntegrationFlow.from(...)
...
.channel(Kafka.channel(template, containerFactory, "someTopic1").groupId("group1"))
...
.get();
}
@Bean
public IntegrationFlow flowWithPubSub(KafkaTemplate<Integer, String> template,
ConcurrentKafkaListenerContainerFactory<Integer, String> containerFactory) {
return IntegrationFlow.from(...)
...
.publishSubscribeChannel(pubSub(template, containerFactory),
pubsub -> pubsub
.subscribe(subflow -> ...)
.subscribe(subflow -> ...))
.get();
}
@Bean
public BroadcastCapableChannel pubSub(KafkaTemplate<Integer, String> template,
ConcurrentKafkaListenerContainerFactory<Integer, String> containerFactory) {
return Kafka.publishSubscribeChannel(template, containerFactory, "someTopic2")
.groupId("group2")
.get();
}
@Bean
public IntegrationFlow flowWithPollable(KafkaTemplate<Integer, String> template,
KafkaMessageSource<Integer, String> source) {
return IntegrationFlow.from(...)
...
.channel(Kafka.pollableChannel(template, source, "someTopic3").groupId("group3"))
.handle(..., e -> e.poller(...))
...
.get();
}
/**
* Channel for a single subscriber.
**/
@Bean
SubscribableKafkaChannel pointToPoint(KafkaTemplate<String, String> template,
KafkaListenerContainerFactory<String, String> factory)
SubscribableKafkaChannel channel =
new SubscribableKafkaChannel(template, factory, "topicA");
channel.setGroupId("group1");
return channel;
}
/**
* Channel for multiple subscribers.
**/
@Bean
SubscribableKafkaChannel pubsub(KafkaTemplate<String, String> template,
KafkaListenerContainerFactory<String, String> factory)
SubscribableKafkaChannel channel =
new SubscribableKafkaChannel(template, factory, "topicB", true);
channel.setGroupId("group2");
return channel;
}
/**
* Pollable channel (topic is configured on the source)
**/
@Bean
PollableKafkaChannel pollable(KafkaTemplate<String, String> template,
KafkaMessageSource<String, String> source)
PollableKafkaChannel channel =
new PollableKafkaChannel(template, source);
channel.setGroupId("group3");
return channel;
}
<int-kafka:channel kafka-template="template" id="ptp" topic="ptpTopic" group-id="ptpGroup"
container-factory="containerFactory" />
<int-kafka:pollable-channel kafka-template="template" id="pollable" message-source="source"
group-id = "pollableGroup"/>
<int-kafka:publish-subscribe-channel kafka-template="template" id="pubSub" topic="pubSubTopic"
group-id="pubSubGroup" container-factory="containerFactory" />
消息转换
提供了一个 StringJsonMessageConverter。更多信息请参阅 Spring for Apache Kafka 文档。
当将此转换器与消息驱动通道适配器一起使用时,您可以指定希望将传入的有效负载转换为何种类型。这可以通过在适配器上设置 payload-type 属性(payloadType 属性)来实现。以下示例展示了如何在 XML 配置中实现此操作:
<int-kafka:message-driven-channel-adapter
id="kafkaListener"
listener-container="container1"
auto-startup="false"
phase="100"
send-timeout="5000"
channel="nullChannel"
message-converter="messageConverter"
payload-type="com.example.Thing"
error-channel="errorChannel" />
<bean id="messageConverter"
class="org.springframework.kafka.support.converter.MessagingMessageConverter"/>
以下示例展示了如何在 Java 配置中为适配器设置 payload-type 属性(payloadType 属性):
@Bean
public KafkaMessageDrivenChannelAdapter<String, String>
adapter(KafkaMessageListenerContainer<String, String> container) {
KafkaMessageDrivenChannelAdapter<String, String> kafkaMessageDrivenChannelAdapter =
new KafkaMessageDrivenChannelAdapter<>(container, ListenerMode.record);
kafkaMessageDrivenChannelAdapter.setOutputChannel(received());
kafkaMessageDrivenChannelAdapter.setMessageConverter(converter());
kafkaMessageDrivenChannelAdapter.setPayloadType(Thing.class);
return kafkaMessageDrivenChannelAdapter;
}
空负载与日志压缩“墓碑”记录
Spring Messaging Message<?> 对象不能包含 null 负载。当您使用 Apache Kafka 端点时,null 负载(也称为墓碑记录)由 KafkaNull 类型的负载表示。更多信息请参阅 Spring for Apache Kafka 文档。
Spring Integration 端点的 POJO 方法可以使用真正的 null 值,而非 KafkaNull。为此,请使用 @Payload(required = false) 标记参数。以下示例展示了如何实现:
@ServiceActivator(inputChannel = "fromSomeKafkaInboundEndpoint")
public void in(@Header(KafkaHeaders.RECEIVED_KEY) String key,
@Payload(required = false) Customer customer) {
// customer is null if a tombstone record
...
}
从 KStream 调用 Spring Integration 流程
您可以使用 MessagingTransformer 从 KStream 调用集成流:
@Bean
public KStream<byte[], byte[]> kStream(StreamsBuilder kStreamBuilder,
MessagingTransformer<byte[], byte[], byte[]> transformer) transformer) {
KStream<byte[], byte[]> stream = kStreamBuilder.stream(STREAMING_TOPIC1);
stream.mapValues((ValueMapper<byte[], byte[]>) String::toUpperCase)
...
.transform(() -> transformer)
.to(streamingTopic2);
stream.print(Printed.toSysOut());
return stream;
}
@Bean
@DependsOn("flow")
public MessagingTransformer<byte[], byte[], String> transformer(
MessagingFunction function) {
MessagingMessageConverter converter = new MessagingMessageConverter();
converter.setHeaderMapper(new SimpleKafkaHeaderMapper("*"));
return new MessagingTransformer<>(function, converter);
}
@Bean
public IntegrationFlow flow() {
return IntegrationFlow.from(MessagingFunction.class)
...
.get();
}
当集成流以接口开始时,创建的代理将使用流Bean的名称,并附加".gateway",因此如果需要,该Bean名称可以用作@Qualifier。
读写处理场景的性能考量
许多应用程序会从一个主题消费消息,进行一些处理,然后写入另一个主题。在大多数情况下,如果 write 操作失败,应用程序会希望抛出异常,以便重试传入的请求和/或将其发送到死信主题。这一功能由底层的消息监听容器与适当配置的错误处理器共同支持。然而,为了实现这一功能,我们需要阻塞监听线程,直到写操作成功(或失败),以便任何异常都能被抛给容器。在消费单条记录时,可以通过在出站适配器上设置 sync 属性来实现。但是,在消费批次时,使用 sync 会导致性能显著下降,因为应用程序在生成下一条消息之前会等待每次发送的结果。你也可以执行多次发送,然后等待这些发送的结果。这可以通过向消息处理器添加 futuresChannel 来实现。要启用此功能,请将 KafkaIntegrationHeaders.FUTURE_TOKEN 添加到出站消息中;然后可以使用它来将 Future 与特定的已发送消息关联起来。以下是如何使用此功能的示例:
@SpringBootApplication
public class FuturesChannelApplication {
public static void main(String[] args) {
SpringApplication.run(FuturesChannelApplication.class, args);
}
@Bean
IntegrationFlow inbound(ConsumerFactory<String, String> consumerFactory, Handler handler) {
return IntegrationFlow.from(Kafka.messageDrivenChannelAdapter(consumerFactory,
ListenerMode.batch, "inTopic"))
.handle(handler)
.get();
}
@Bean
IntegrationFlow outbound(KafkaTemplate<String, String> kafkaTemplate) {
return IntegrationFlow.from(Gate.class)
.enrichHeaders(h -> h
.header(KafkaHeaders.TOPIC, "outTopic")
.headerExpression(KafkaIntegrationHeaders.FUTURE_TOKEN, "headers[id]"))
.handle(Kafka.outboundChannelAdapter(kafkaTemplate)
.futuresChannel("futures"))
.get();
}
@Bean
PollableChannel futures() {
return new QueueChannel();
}
}
@Component
@DependsOn("outbound")
class Handler {
@Autowired
Gate gate;
@Autowired
PollableChannel futures;
public void handle(List<String> input) throws Exception {
System.out.println(input);
input.forEach(str -> this.gate.send(str.toUpperCase()));
for (int i = 0; i < input.size(); i++) {
Message<?> future = this.futures.receive(10000);
((Future<?>) future.getPayload()).get(10, TimeUnit.SECONDS);
}
}
}
interface Gate {
void send(String out);
}