GH-2941: Kafka Streams Component beans composition
Resolves https://github.com/spring-cloud/spring-cloud-stream/issues/2941 * Introduce function composition for Kafka Streams Component beans
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-2023 the original author or authors.
|
||||
* Copyright 2019-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -254,8 +254,19 @@ public class KafkaStreamsFunctionProcessor extends AbstractKafkaStreamsBinderPro
|
||||
KafkaStreamsBindableProxyFactory kafkaStreamsBindableProxyFactory, Method method,
|
||||
ResolvableType outputResolvableType,
|
||||
String... composedFunctionNames) {
|
||||
final Map<String, ResolvableType> resolvableTypes = buildTypeMap(resolvableType,
|
||||
Map<String, ResolvableType> resolvableTypes;
|
||||
|
||||
if (method != null && composedFunctionNames.length > 0) { // composed component methods
|
||||
resolvableTypes = buildTypeMap(resolvableType,
|
||||
kafkaStreamsBindableProxyFactory, method, composedFunctionNames[0]);
|
||||
}
|
||||
else if (method != null) { // non-composed component beans
|
||||
resolvableTypes = buildTypeMap(resolvableType,
|
||||
kafkaStreamsBindableProxyFactory, method, functionName);
|
||||
}
|
||||
else { // all other cases
|
||||
resolvableTypes = buildTypeMap(resolvableType, kafkaStreamsBindableProxyFactory, null, functionName);
|
||||
}
|
||||
|
||||
ResolvableType outboundResolvableType;
|
||||
if (outputResolvableType != null) {
|
||||
@@ -277,7 +288,8 @@ public class KafkaStreamsFunctionProcessor extends AbstractKafkaStreamsBinderPro
|
||||
biConsumer.accept(adaptedInboundArguments[0], adaptedInboundArguments[1]);
|
||||
}
|
||||
else if (method != null) { // Handling component functional beans
|
||||
final Object bean = beanFactory.getBean(functionName);
|
||||
final Object bean = composedFunctionNames.length > 0 ? beanFactory.getBean(composedFunctionNames[0])
|
||||
: beanFactory.getBean(functionName);
|
||||
if (Consumer.class.isAssignableFrom(bean.getClass())) {
|
||||
((Consumer) bean).accept(adaptedInboundArguments[0]);
|
||||
}
|
||||
@@ -293,6 +305,9 @@ public class KafkaStreamsFunctionProcessor extends AbstractKafkaStreamsBinderPro
|
||||
result = ((Function) bean).apply(adaptedInboundArguments[0]);
|
||||
}
|
||||
result = handleCurriedFunctions(adaptedInboundArguments, result);
|
||||
if (composedFunctionNames.length > 0) {
|
||||
result = handleComposedFunctions(adaptedInboundArguments, result, composedFunctionNames);
|
||||
}
|
||||
if (result != null) {
|
||||
final Set<String> outputs = new TreeSet<>(kafkaStreamsBindableProxyFactory.getOutputs());
|
||||
final Iterator<String> outboundDefinitionIterator = outputs.iterator();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-2023 the original author or authors.
|
||||
* Copyright 2019-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -20,6 +20,7 @@ import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
@@ -61,8 +62,10 @@ public class KafkaStreamsFunctionProcessorInvoker {
|
||||
if (functionUnits.length == 0) {
|
||||
resolvableTypeMap.forEach((key, value) -> {
|
||||
Optional<KafkaStreamsBindableProxyFactory> proxyFactory =
|
||||
Arrays.stream(kafkaStreamsBindableProxyFactories).filter(p -> p.getFunctionName().equals(key)).findFirst();
|
||||
this.kafkaStreamsFunctionProcessor.setupFunctionInvokerForKafkaStreams(value, key, proxyFactory.get(), methods.get(key), null);
|
||||
Arrays.stream(kafkaStreamsBindableProxyFactories).filter(p -> p.getFunctionName().equals(key)).findFirst();
|
||||
proxyFactory.ifPresent(kafkaStreamsBindableProxyFactory ->
|
||||
this.kafkaStreamsFunctionProcessor.setupFunctionInvokerForKafkaStreams(value, key,
|
||||
kafkaStreamsBindableProxyFactory, methods.get(key), null));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,10 +77,16 @@ public class KafkaStreamsFunctionProcessorInvoker {
|
||||
derivedNameFromComposed[0] = derivedNameFromComposed[0].concat(split);
|
||||
}
|
||||
Optional<KafkaStreamsBindableProxyFactory> proxyFactory =
|
||||
Arrays.stream(kafkaStreamsBindableProxyFactories).filter(p -> p.getFunctionName().equals(derivedNameFromComposed[0])).findFirst();
|
||||
proxyFactory.ifPresent(kafkaStreamsBindableProxyFactory ->
|
||||
this.kafkaStreamsFunctionProcessor.setupFunctionInvokerForKafkaStreams(resolvableTypeMap.get(composedFunctions[0]),
|
||||
derivedNameFromComposed[0], kafkaStreamsBindableProxyFactory, methods.get(derivedNameFromComposed[0]), resolvableTypeMap.get(composedFunctions[composedFunctions.length - 1]), composedFunctions));
|
||||
Arrays.stream(kafkaStreamsBindableProxyFactories).filter(p ->
|
||||
p.getFunctionName().equals(derivedNameFromComposed[0])).findFirst();
|
||||
|
||||
Method method = methods.isEmpty() ? null : methods.get(composedFunctions[0]);
|
||||
Consumer<KafkaStreamsBindableProxyFactory> kafkaStreamsBindableProxyFactoryConsumer = kafkaStreamsBindableProxyFactory ->
|
||||
this.kafkaStreamsFunctionProcessor.setupFunctionInvokerForKafkaStreams(resolvableTypeMap.get(composedFunctions[0]),
|
||||
derivedNameFromComposed[0], kafkaStreamsBindableProxyFactory, method,
|
||||
resolvableTypeMap.get(composedFunctions[composedFunctions.length - 1]), composedFunctions);
|
||||
|
||||
proxyFactory.ifPresent(kafkaStreamsBindableProxyFactoryConsumer);
|
||||
}
|
||||
else {
|
||||
Optional<KafkaStreamsBindableProxyFactory> proxyFactory =
|
||||
@@ -88,4 +97,5 @@ public class KafkaStreamsFunctionProcessorInvoker {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2021-2023 the original author or authors.
|
||||
* Copyright 2021-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -57,7 +57,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
@EmbeddedKafka(topics = {"testFunctionComponent-out-0", "testFunctionComponent-out-1", "testBiFunctionComponent-out", "testCurriedFunctionWithFunctionTerminal-out"})
|
||||
class KafkaStreamsComponentBeansTests {
|
||||
|
||||
private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker();
|
||||
private static EmbeddedKafkaBroker embeddedKafka;
|
||||
|
||||
private static Consumer<String, String> consumer1;
|
||||
private static Consumer<String, String> consumer2;
|
||||
@@ -70,6 +70,7 @@ class KafkaStreamsComponentBeansTests {
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() {
|
||||
embeddedKafka = EmbeddedKafkaCondition.getBroker();
|
||||
Map<String, Object> consumerProps = KafkaTestUtils.consumerProps("group", "false",
|
||||
embeddedKafka);
|
||||
consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
|
||||
@@ -105,10 +106,15 @@ class KafkaStreamsComponentBeansTests {
|
||||
|
||||
@AfterAll
|
||||
public static void tearDown() {
|
||||
consumer1.close();
|
||||
consumer2.close();
|
||||
consumer3.close();
|
||||
consumer4.close();
|
||||
closeConsumers(consumer1, consumer2, consumer3, consumer4);
|
||||
}
|
||||
|
||||
static void closeConsumers(Consumer<?, ?>... consumers) {
|
||||
for (Consumer<?, ?> consumer : consumers) {
|
||||
if (consumer != null) {
|
||||
consumer.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -137,6 +143,31 @@ class KafkaStreamsComponentBeansTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void functionComponentBeanComposedWithConsumer() throws InterruptedException {
|
||||
SpringApplication app = new SpringApplication(FunctionAsComponent.class, StringConsumer.class);
|
||||
app.setWebApplicationType(WebApplicationType.NONE);
|
||||
try (ConfigurableApplicationContext ignored = app.run(
|
||||
"--spring.cloud.function.definition=foo|stringConsumer",
|
||||
"--server.port=0",
|
||||
"--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.bindings.foostringConsumer-in-0.destination=fooStringConsumer-in",
|
||||
"--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000",
|
||||
"--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) {
|
||||
Map<String, Object> senderProps = KafkaTestUtils.producerProps(embeddedKafka);
|
||||
DefaultKafkaProducerFactory<Integer, String> pf = new DefaultKafkaProducerFactory<>(senderProps);
|
||||
try {
|
||||
KafkaTemplate<Integer, String> template = new KafkaTemplate<>(pf, true);
|
||||
template.setDefaultTopic("fooStringConsumer-in");
|
||||
template.sendDefault("foobar");
|
||||
Assert.isTrue(LATCH_1.await(10, TimeUnit.SECONDS), "foobar");
|
||||
}
|
||||
finally {
|
||||
pf.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void functionComponentWithBranching() {
|
||||
SpringApplication app = new SpringApplication(FunctionAsComponentWithBranching.class);
|
||||
@@ -349,6 +380,16 @@ class KafkaStreamsComponentBeansTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Component("stringConsumer")
|
||||
@EnableAutoConfiguration
|
||||
public static class StringConsumer implements java.util.function.Consumer<KStream<String, String>> {
|
||||
|
||||
@Override
|
||||
public void accept(KStream<String, String> integerStringKStream) {
|
||||
integerStringKStream.foreach((s1, s2) -> LATCH_1.countDown());
|
||||
}
|
||||
}
|
||||
|
||||
@Component("bazz")
|
||||
@EnableAutoConfiguration
|
||||
public static class BiFunctionAsComponent implements BiFunction<KStream<String, String>, KStream<String, String>, KStream<String, String>> {
|
||||
|
||||
Reference in New Issue
Block a user