GH-107: Make Splitter Function as Flux-based
Fixes: https://github.com/spring-cloud/spring-functions-catalog/issues/107 When we have a composition like this: ``` spring.cloud.function.definition = fileSupplier|splitterFunction ``` Then final "function" signature is like this `Supplier<Flux<Message<List<Message<?>>>>>`. And that is exactly what we don't expected from the splitter in the end of the composition. While Spring Cloud Stream supports de-batching, it works for a `List` output only if function is bound by itself. In case of composition we got just a `Supplier`. * Rework `SplitterFunctionConfiguration` for `splitterFunction` from `Function<Message<?>, List<Message<?>>>` to `Function<Flux<Message<?>>, Flux<Message<?>>>` signature to support every possible simple and composed bindings in Spring Cloud Stream * Rework `SplitterFunctionApplicationTests` for new expected `Function<Flux<Message<?>>, Flux<Message<?>>>` signature * Rework `zip-split-rabbit-binder` sample to not use a `flattenFunction` workaround and fully rely on whatever is new for the `splitterFunction` * Fix `ZipSplitRabbitBinderApplicationTests` moving the `@RabbitListener` into a `@TestConfiguration`. Apparently in a new Spring Boot version the test class is registered as a bean much later than normal application context startup. Therefore, even if the `@RabbitListener` parsed and registered properly, the `RabbitAdmin` bean has been already started to see our extra bean definition for the `@QueueBinding` Changing signature for the splitterFunction to reactive types would make it working even with a Supplier composition. Fix JDBC & MongoDB suppliers to deal with a new version of Splitter function Fix Checkstyle violations Use `IntegrationReactiveUtils.messageSourceToFlux()` API The `IntegrationReactiveUtils.messageSourceToFlux()` provides convenient API to represent a `MessageSource` as a `Flux` to poll this source. The API has an error handling logic and delay when no data emitted by the source * Remove `org.springframework.cloud` dependencies from the project since we don't use `@PollableBean` anymore, which comes from the `spring-cloud-function-context` * Simplify `JdbcSupplierConfiguration` and `MongodbSupplierConfiguration` code more: more injections to the respective bean method. * Use `(__) ->` lambda syntax for unused argument * Remove unused `ThreadLocalFluxSinkMessageChannel` internal class * Update Copyrights of the classes in this change Upgrade to Gradle `8.12`
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2011-2024 the original author or authors.
|
||||
* Copyright 2011-2025 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.
|
||||
@@ -17,11 +17,9 @@
|
||||
package org.springframework.cloud.fn.splitter;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
@@ -32,13 +30,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.integration.channel.ReactiveStreamsSubscribableChannel;
|
||||
import org.springframework.integration.channel.FluxMessageChannel;
|
||||
import org.springframework.integration.file.splitter.FileSplitter;
|
||||
import org.springframework.integration.splitter.AbstractMessageSplitter;
|
||||
import org.springframework.integration.splitter.DefaultMessageSplitter;
|
||||
import org.springframework.integration.splitter.ExpressionEvaluatingSplitter;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
|
||||
/**
|
||||
* Auto-configuration for Splitter function.
|
||||
@@ -51,7 +48,7 @@ import org.springframework.messaging.MessageChannel;
|
||||
public class SplitterFunctionConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<Message<?>, List<Message<?>>> splitterFunction(
|
||||
public Function<Flux<Message<?>>, Flux<Message<?>>> splitterFunction(
|
||||
@Qualifier("expressionSplitter") Optional<AbstractMessageSplitter> expressionSplitter,
|
||||
@Qualifier("fileSplitter") Optional<AbstractMessageSplitter> fileSplitter,
|
||||
@Qualifier("defaultSplitter") Optional<AbstractMessageSplitter> defaultSplitter,
|
||||
@@ -60,13 +57,13 @@ public class SplitterFunctionConfiguration {
|
||||
AbstractMessageSplitter messageSplitter = expressionSplitter.or(() -> fileSplitter)
|
||||
.or(() -> defaultSplitter)
|
||||
.get();
|
||||
|
||||
messageSplitter.setApplySequence(splitterFunctionProperties.isApplySequence());
|
||||
ThreadLocalFluxSinkMessageChannel outputChannel = new ThreadLocalFluxSinkMessageChannel();
|
||||
FluxMessageChannel inputChannel = new FluxMessageChannel();
|
||||
inputChannel.subscribe(messageSplitter);
|
||||
FluxMessageChannel outputChannel = new FluxMessageChannel();
|
||||
messageSplitter.setOutputChannel(outputChannel);
|
||||
return (message) -> {
|
||||
messageSplitter.handleMessage(message);
|
||||
return outputChannel.publisherThreadLocal.get();
|
||||
};
|
||||
return (messageFlux) -> Flux.from(outputChannel).doOnRequest((__) -> inputChannel.subscribeTo(messageFlux));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -117,22 +114,4 @@ public class SplitterFunctionConfiguration {
|
||||
|
||||
}
|
||||
|
||||
private static final class ThreadLocalFluxSinkMessageChannel
|
||||
implements MessageChannel, ReactiveStreamsSubscribableChannel {
|
||||
|
||||
private final ThreadLocal<List<Message<?>>> publisherThreadLocal = new ThreadLocal<>();
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void subscribeTo(Publisher<? extends Message<?>> publisher) {
|
||||
this.publisherThreadLocal.set(Flux.from(publisher).collectList().cast(List.class).block());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean send(Message<?> message, long l) {
|
||||
throw new UnsupportedOperationException("This channel only supports a reactive 'subscribeTo()' ");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2011-2024 the original author or authors.
|
||||
* Copyright 2011-2025 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.
|
||||
@@ -16,10 +16,12 @@
|
||||
|
||||
package org.springframework.cloud.fn.splitter;
|
||||
|
||||
import java.util.List;
|
||||
import java.time.Duration;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
@@ -28,19 +30,18 @@ import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest(properties = "splitter.expression=payload.split(',')")
|
||||
@DirtiesContext
|
||||
public class SplitterFunctionApplicationTests {
|
||||
|
||||
@Autowired
|
||||
Function<Message<?>, List<Message<?>>> splitter;
|
||||
Function<Flux<Message<?>>, Flux<Message<?>>> splitter;
|
||||
|
||||
@Test
|
||||
public void testExpressionSplitter() {
|
||||
List<Message<?>> messageList = this.splitter.apply(new GenericMessage<>("hello,world"));
|
||||
assertThat(messageList).extracting((m) -> m.getPayload().toString()).contains("hello", "world");
|
||||
Flux<Message<?>> messageFlux = this.splitter.apply(Flux.just(new GenericMessage<>("hello,world")));
|
||||
Flux<String> payloads = messageFlux.map(Message::getPayload).map(Object::toString);
|
||||
StepVerifier.create(payloads).expectNext("hello", "world").thenCancel().verify(Duration.ofSeconds(30));
|
||||
}
|
||||
|
||||
@SpringBootApplication
|
||||
|
||||
Reference in New Issue
Block a user