Split test-binder from core spring-cloud-stream
* Remove all SI test-binder based components from core spring-cloud-stream-module * Create a new module - spring-cloud-stream-test-binder that contains the test-binder and all it's related components * Migrate tests from core module that use the test-binder into a separate module called spring-cloud-stream-integration-tests * Remove the test-jar dependency using the classifier approach * Update Spring Cloud Stream BOM with the new test-binder dependency * Update Schema-Registry tests that use the old approach (using the test-jar with the classifier) with the new test-binder dependency Resolves https://github.com/spring-cloud/spring-cloud-stream/issues/2565
This commit is contained in:
committed by
Oleg Zhurakousky
parent
58ef5b0479
commit
bc094e0ec4
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2017-2019 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.cloud.stream.provisioning.ProvisioningProvider;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.Lifecycle;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.core.MessageProducer;
|
||||
import org.springframework.integration.handler.BridgeHandler;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.SubscribableChannel;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
* @author Gary Russell
|
||||
* @author Oleg Zhurakousky
|
||||
* @since 1.2.2
|
||||
*/
|
||||
@Disabled("This test has issues that needs to be looked into")
|
||||
public class BasicAbstractMessageChannelBinderTests {
|
||||
|
||||
private static ApplicationContext context;
|
||||
|
||||
@BeforeAll
|
||||
public static void prepare() {
|
||||
context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration())
|
||||
.web(WebApplicationType.NONE).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testEndpointLifecycle() throws Exception {
|
||||
// @checkstyle:off
|
||||
AbstractMessageChannelBinder<ConsumerProperties, ProducerProperties, ProvisioningProvider<ConsumerProperties, ProducerProperties>> binder = context
|
||||
.getBean(AbstractMessageChannelBinder.class);
|
||||
// @checkstyle:on
|
||||
|
||||
ConsumerProperties consumerProperties = new ConsumerProperties();
|
||||
consumerProperties.setMaxAttempts(1); // to force error infrastructure creation
|
||||
|
||||
Binding<MessageChannel> consumerBinding = binder.bindConsumer("foo", "fooGroup",
|
||||
new DirectChannel(), consumerProperties);
|
||||
DirectFieldAccessor consumerBindingAccessor = new DirectFieldAccessor(
|
||||
consumerBinding);
|
||||
MessageProducer messageProducer = (MessageProducer) consumerBindingAccessor
|
||||
.getPropertyValue("lifecycle");
|
||||
assertThat(((Lifecycle) messageProducer).isRunning()).isTrue();
|
||||
assertThat(messageProducer.getOutputChannel()).isNotNull();
|
||||
|
||||
SubscribableChannel errorChannel = (SubscribableChannel) consumerBindingAccessor
|
||||
.getPropertyValue("lifecycle.errorChannel");
|
||||
assertThat(errorChannel).isNotNull();
|
||||
Set<MessageHandler> handlers = TestUtils.getPropertyValue(errorChannel,
|
||||
"dispatcher.handlers", Set.class);
|
||||
assertThat(handlers.size()).isEqualTo(2);
|
||||
Iterator<MessageHandler> iterator = handlers.iterator();
|
||||
assertThat(iterator.next()).isInstanceOf(BridgeHandler.class);
|
||||
assertThat(iterator.next()).isInstanceOf(LastSubscriberMessageHandler.class);
|
||||
assertThat(this.context.containsBean("foo.fooGroup.errors")).isTrue();
|
||||
assertThat(this.context.containsBean("foo.fooGroup.errors.recoverer")).isTrue();
|
||||
assertThat(this.context.containsBean("foo.fooGroup.errors.handler")).isTrue();
|
||||
assertThat(this.context.containsBean("foo.fooGroup.errors.bridge")).isTrue();
|
||||
consumerBinding.unbind();
|
||||
assertThat(this.context.containsBean("foo.fooGroup.errors")).isFalse();
|
||||
assertThat(this.context.containsBean("foo.fooGroup.errors.recoverer")).isFalse();
|
||||
assertThat(this.context.containsBean("foo.fooGroup.errors.handler")).isFalse();
|
||||
assertThat(this.context.containsBean("foo.fooGroup.errors.bridge")).isFalse();
|
||||
|
||||
assertThat(((Lifecycle) messageProducer).isRunning()).isFalse();
|
||||
|
||||
ProducerProperties producerProps = new ProducerProperties();
|
||||
producerProps.setErrorChannelEnabled(true);
|
||||
Binding<MessageChannel> producerBinding = binder.bindProducer("bar",
|
||||
new DirectChannel(), producerProps);
|
||||
assertThat(this.context.containsBean("bar.errors")).isTrue();
|
||||
assertThat(this.context.containsBean("bar.errors.bridge")).isTrue();
|
||||
producerBinding.unbind();
|
||||
assertThat(this.context.containsBean("bar.errors")).isFalse();
|
||||
assertThat(this.context.containsBean("bar.errors.bridge")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testEndpointBinderHasRecoverer() throws Exception {
|
||||
// @checkstyle:off
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration())
|
||||
.web(WebApplicationType.NONE).run();
|
||||
|
||||
AbstractMessageChannelBinder<ConsumerProperties, ProducerProperties, ProvisioningProvider<ConsumerProperties, ProducerProperties>> binder = context
|
||||
.getBean(AbstractMessageChannelBinder.class);
|
||||
// @checkstyle:on
|
||||
|
||||
Binding<MessageChannel> consumerBinding = binder.bindConsumer("foo", "fooGroup",
|
||||
new DirectChannel(), new ConsumerProperties());
|
||||
DirectFieldAccessor consumerBindingAccessor = new DirectFieldAccessor(
|
||||
consumerBinding);
|
||||
SubscribableChannel errorChannel = (SubscribableChannel) consumerBindingAccessor
|
||||
.getPropertyValue("lifecycle.errorChannel");
|
||||
assertThat(errorChannel).isNull();
|
||||
errorChannel = (SubscribableChannel) consumerBindingAccessor
|
||||
.getPropertyValue("lifecycle.recoveryCallback.channel");
|
||||
assertThat(errorChannel).isNotNull();
|
||||
Set<MessageHandler> handlers = TestUtils.getPropertyValue(errorChannel,
|
||||
"dispatcher.handlers", Set.class);
|
||||
assertThat(handlers.size()).isEqualTo(2);
|
||||
Iterator<MessageHandler> iterator = handlers.iterator();
|
||||
assertThat(iterator.next()).isInstanceOf(BridgeHandler.class);
|
||||
assertThat(iterator.next()).isInstanceOf(LastSubscriberMessageHandler.class);
|
||||
assertThat(context.containsBean("foo.fooGroup.errors")).isTrue();
|
||||
assertThat(context.containsBean("foo.fooGroup.errors.recoverer")).isTrue();
|
||||
assertThat(context.containsBean("foo.fooGroup.errors.handler")).isTrue();
|
||||
assertThat(context.containsBean("foo.fooGroup.errors.bridge")).isTrue();
|
||||
consumerBinding.unbind();
|
||||
assertThat(context.containsBean("foo.fooGroup.errors")).isFalse();
|
||||
assertThat(context.containsBean("foo.fooGroup.errors.recoverer")).isFalse();
|
||||
assertThat(context.containsBean("foo.fooGroup.errors.handler")).isFalse();
|
||||
assertThat(context.containsBean("foo.fooGroup.errors.bridge")).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2017-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.springframework.integration.dispatcher.AbstractDispatcher;
|
||||
import org.springframework.messaging.MessageDeliveryException;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Anshul Mehra
|
||||
*/
|
||||
public class BinderErrorChannelTests {
|
||||
|
||||
private static final LastSubscriberMessageHandler FINAL_HANDLER = message -> {
|
||||
|
||||
};
|
||||
|
||||
private static final MessageHandler FIRST_HANDLER = message -> {
|
||||
|
||||
};
|
||||
|
||||
private static final MessageHandler SECOND_HANDLER = message -> {
|
||||
|
||||
};
|
||||
|
||||
@Test
|
||||
void testExceptionIsThrownWhenNoSubscribers() {
|
||||
BinderErrorChannel channel = new BinderErrorChannel();
|
||||
Assertions.assertThrows(MessageDeliveryException.class, () -> {
|
||||
channel.send(new GenericMessage<String>("hello"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscribeUnsubscribe() {
|
||||
BinderErrorChannel channel = new BinderErrorChannel();
|
||||
DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(channel);
|
||||
AbstractDispatcher dispatcher = (AbstractDispatcher) fieldAccessor
|
||||
.getPropertyValue("dispatcher");
|
||||
|
||||
assertThat(dispatcher).isNotNull();
|
||||
assertThat(channel.subscribers()).isEqualTo(0);
|
||||
assertThat(dispatcher.getHandlerCount()).isEqualTo(0);
|
||||
|
||||
channel.subscribe(FINAL_HANDLER);
|
||||
|
||||
assertThat(channel.subscribers()).isEqualTo(1);
|
||||
assertThat(dispatcher.getHandlerCount()).isEqualTo(1);
|
||||
|
||||
channel.subscribe(FIRST_HANDLER);
|
||||
|
||||
assertThat(channel.subscribers()).isEqualTo(2);
|
||||
assertThat(dispatcher.getHandlerCount()).isEqualTo(2);
|
||||
|
||||
channel.subscribe(SECOND_HANDLER);
|
||||
|
||||
assertThat(channel.subscribers()).isEqualTo(3);
|
||||
assertThat(dispatcher.getHandlerCount()).isEqualTo(3);
|
||||
|
||||
channel.unsubscribe(FIRST_HANDLER);
|
||||
|
||||
assertThat(channel.subscribers()).isEqualTo(2);
|
||||
assertThat(dispatcher.getHandlerCount()).isEqualTo(2);
|
||||
|
||||
channel.unsubscribe(FINAL_HANDLER);
|
||||
|
||||
assertThat(channel.subscribers()).isEqualTo(1);
|
||||
assertThat(dispatcher.getHandlerCount()).isEqualTo(1);
|
||||
|
||||
channel.unsubscribe(SECOND_HANDLER);
|
||||
|
||||
assertThat(channel.subscribers()).isEqualTo(0);
|
||||
assertThat(dispatcher.getHandlerCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2016-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.binder.test.InputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
* @author Oleg Zhurakousky
|
||||
*/
|
||||
public class ErrorBindingTests {
|
||||
|
||||
@Test
|
||||
public void testConfigurationWithDefaultErrorHandler() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
ErrorBindingTests.ErrorConfigurationDefault.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.cloud.stream.bindings.handle-in-0.consumer.max-attempts=1",
|
||||
"--spring.cloud.function.definition=handle",
|
||||
"--spring.cloud.stream.default.error-handler-definition=errorHandler",
|
||||
"--spring.jmx.enabled=false");
|
||||
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
source.send(new GenericMessage<byte[]>("Hello".getBytes()));
|
||||
source.send(new GenericMessage<byte[]>("Hello".getBytes()));
|
||||
source.send(new GenericMessage<byte[]>("Hello".getBytes()));
|
||||
|
||||
ErrorConfigurationDefault errorConfiguration = context
|
||||
.getBean(ErrorConfigurationDefault.class);
|
||||
assertThat(errorConfiguration.counter).isEqualTo(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConfigurationWithBindingSpecificErrorHandler() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
ErrorBindingTests.ErrorConfigurationWithCustomErrorHandler.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.cloud.stream.bindings.handle-in-0.consumer.max-attempts=1",
|
||||
"--spring.cloud.function.definition=handle",
|
||||
"--spring.cloud.stream.bindings.handle-in-0.error-handler-definition=errorHandler",
|
||||
"--spring.jmx.enabled=false");
|
||||
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
source.send(new GenericMessage<byte[]>("Hello".getBytes()));
|
||||
source.send(new GenericMessage<byte[]>("Hello".getBytes()));
|
||||
source.send(new GenericMessage<byte[]>("Hello".getBytes()));
|
||||
|
||||
ErrorConfigurationWithCustomErrorHandler errorConfiguration = context
|
||||
.getBean(ErrorConfigurationWithCustomErrorHandler.class);
|
||||
assertThat(errorConfiguration.counter).isEqualTo(6);
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class TestProcessor {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> processor() {
|
||||
return s -> s;
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class ErrorConfigurationDefault {
|
||||
|
||||
private int counter;
|
||||
|
||||
@Bean
|
||||
public Function<String, String> handle() {
|
||||
return v -> {
|
||||
this.counter++;
|
||||
throw new RuntimeException("Intentional");
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Consumer<Object> errorHandler() {
|
||||
return v -> {
|
||||
this.counter++;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class ErrorConfigurationWithCustomErrorHandler {
|
||||
|
||||
private int counter;
|
||||
|
||||
@Bean
|
||||
public Function<String, String> handle() {
|
||||
return v -> {
|
||||
this.counter++;
|
||||
throw new RuntimeException("Intentional");
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Consumer<Object> errorHandler() {
|
||||
return v -> {
|
||||
this.counter++;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
/*
|
||||
* Copyright 2018-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinder;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.cloud.stream.binding.MessageConverterConfigurer;
|
||||
import org.springframework.cloud.stream.config.BindingProperties;
|
||||
import org.springframework.cloud.stream.config.BindingServiceProperties;
|
||||
import org.springframework.cloud.stream.converter.CompositeMessageConverterFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.Lifecycle;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.integration.acks.AcknowledgmentCallback;
|
||||
import org.springframework.integration.acks.AcknowledgmentCallback.Status;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.context.IntegrationContextUtils;
|
||||
import org.springframework.integration.core.MessageSource;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.SubscribableChannel;
|
||||
import org.springframework.messaging.converter.SmartMessageConverter;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @author Oleg Zhurakousky
|
||||
* @author David Turanski
|
||||
* @since 2.0
|
||||
*
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
public class PollableConsumerTests {
|
||||
|
||||
private ApplicationContext context;
|
||||
|
||||
private SmartMessageConverter messageConverter;
|
||||
|
||||
@BeforeAll
|
||||
public void before() {
|
||||
this.messageConverter = new CompositeMessageConverterFactory()
|
||||
.getMessageConverterForAllRegistered();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultMessageSource() {
|
||||
TestChannelBinder binder = createBinder();
|
||||
MessageConverterConfigurer configurer = this.context
|
||||
.getBean(MessageConverterConfigurer.class);
|
||||
|
||||
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
|
||||
this.messageConverter);
|
||||
configurer.configurePolledMessageSource(pollableSource, "foo");
|
||||
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
|
||||
null);
|
||||
properties.setMaxAttempts(2);
|
||||
properties.setBackOffInitialInterval(0);
|
||||
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
|
||||
AtomicInteger count = new AtomicInteger();
|
||||
assertThat(pollableSource.poll(message -> {
|
||||
assertThat(message).isNotNull();
|
||||
count.incrementAndGet();
|
||||
})).isTrue();
|
||||
assertThat(count.get()).isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSimple() {
|
||||
TestChannelBinder binder = createBinder();
|
||||
MessageConverterConfigurer configurer = this.context
|
||||
.getBean(MessageConverterConfigurer.class);
|
||||
|
||||
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
|
||||
this.messageConverter);
|
||||
configurer.configurePolledMessageSource(pollableSource, "foo");
|
||||
pollableSource.addInterceptor(new ChannelInterceptor() {
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
return MessageBuilder
|
||||
.withPayload(((String) message.getPayload()).toUpperCase())
|
||||
.copyHeaders(message.getHeaders()).build();
|
||||
}
|
||||
|
||||
});
|
||||
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
|
||||
null);
|
||||
properties.setMaxAttempts(2);
|
||||
properties.setBackOffInitialInterval(0);
|
||||
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
|
||||
final AtomicInteger count = new AtomicInteger();
|
||||
assertThat(pollableSource.poll(received -> {
|
||||
assertThat(received.getPayload()).isEqualTo("POLLED DATA");
|
||||
assertThat(received.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeType.valueOf("text/plain"));
|
||||
if (count.incrementAndGet() == 1) {
|
||||
throw new RuntimeException("test retry");
|
||||
}
|
||||
})).isTrue();
|
||||
assertThat(count.get()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertSimple() {
|
||||
TestChannelBinder binder = createBinder();
|
||||
MessageConverterConfigurer configurer = this.context
|
||||
.getBean(MessageConverterConfigurer.class);
|
||||
|
||||
binder.setMessageSourceDelegate(
|
||||
() -> new GenericMessage<>("{\"foo\":\"bar\"}".getBytes()));
|
||||
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
|
||||
this.messageConverter);
|
||||
configurer.configurePolledMessageSource(pollableSource, "foo");
|
||||
|
||||
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
|
||||
null);
|
||||
properties.setMaxAttempts(1);
|
||||
properties.setBackOffInitialInterval(0);
|
||||
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
|
||||
final AtomicReference<Object> payload = new AtomicReference<>();
|
||||
assertThat(pollableSource.poll(received -> {
|
||||
payload.set(received.getPayload());
|
||||
}, new ParameterizedTypeReference<Foo>() {
|
||||
})).isTrue();
|
||||
assertThat(payload.get()).isInstanceOf(Foo.class);
|
||||
assertThat(((Foo) payload.get()).getFoo()).isEqualTo("bar");
|
||||
// test the cache for coverage
|
||||
assertThat(pollableSource.poll(received -> {
|
||||
payload.set(received.getPayload());
|
||||
}, new ParameterizedTypeReference<Foo>() {
|
||||
})).isTrue();
|
||||
assertThat(payload.get()).isInstanceOf(Foo.class);
|
||||
assertThat(((Foo) payload.get()).getFoo()).isEqualTo("bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertSimpler() {
|
||||
TestChannelBinder binder = createBinder();
|
||||
MessageConverterConfigurer configurer = this.context
|
||||
.getBean(MessageConverterConfigurer.class);
|
||||
BindingServiceProperties bsps = this.context
|
||||
.getBean(BindingServiceProperties.class);
|
||||
BindingProperties props = new BindingProperties();
|
||||
props.setContentType("text/plain");
|
||||
bsps.setBindings(Collections.singletonMap("foo", props));
|
||||
|
||||
binder.setMessageSourceDelegate(() -> new GenericMessage<>("foo".getBytes()));
|
||||
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
|
||||
this.messageConverter);
|
||||
configurer.configurePolledMessageSource(pollableSource, "foo");
|
||||
|
||||
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
|
||||
null);
|
||||
properties.setMaxAttempts(1);
|
||||
properties.setBackOffInitialInterval(0);
|
||||
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
|
||||
final AtomicReference<Object> payload = new AtomicReference<>();
|
||||
assertThat(pollableSource.poll(received -> {
|
||||
payload.set(received.getPayload());
|
||||
}, new ParameterizedTypeReference<String>() {
|
||||
})).isTrue();
|
||||
assertThat(payload.get()).isInstanceOf(String.class);
|
||||
assertThat(payload.get()).isEqualTo("foo");
|
||||
// test the cache for coverage
|
||||
assertThat(pollableSource.poll(received -> {
|
||||
payload.set(received.getPayload());
|
||||
}, new ParameterizedTypeReference<String>() {
|
||||
})).isTrue();
|
||||
assertThat(payload.get()).isInstanceOf(String.class);
|
||||
assertThat(payload.get()).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertList() {
|
||||
TestChannelBinder binder = createBinder();
|
||||
MessageConverterConfigurer configurer = this.context
|
||||
.getBean(MessageConverterConfigurer.class);
|
||||
|
||||
binder.setMessageSourceDelegate(() -> new GenericMessage<>(
|
||||
"[{\"foo\":\"bar\"},{\"foo\":\"baz\"}]".getBytes()));
|
||||
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
|
||||
this.messageConverter);
|
||||
configurer.configurePolledMessageSource(pollableSource, "foo");
|
||||
|
||||
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
|
||||
null);
|
||||
properties.setMaxAttempts(1);
|
||||
properties.setBackOffInitialInterval(0);
|
||||
|
||||
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
|
||||
|
||||
final AtomicReference<Object> payload = new AtomicReference<>();
|
||||
assertThat(pollableSource.poll(received -> {
|
||||
payload.set(received.getPayload());
|
||||
}, new ParameterizedTypeReference<List<Foo>>() {
|
||||
})).isTrue();
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Foo> list = (List<Foo>) payload.get();
|
||||
assertThat(list.size()).isEqualTo(2);
|
||||
assertThat(list.get(0).getFoo()).isEqualTo("bar");
|
||||
assertThat(list.get(1).getFoo()).isEqualTo("baz");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertMap() {
|
||||
TestChannelBinder binder = createBinder();
|
||||
MessageConverterConfigurer configurer = this.context
|
||||
.getBean(MessageConverterConfigurer.class);
|
||||
|
||||
binder.setMessageSourceDelegate(
|
||||
() -> new GenericMessage<>("{\"qux\":{\"foo\":\"bar\"}}".getBytes()));
|
||||
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
|
||||
this.messageConverter);
|
||||
configurer.configurePolledMessageSource(pollableSource, "foo");
|
||||
|
||||
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
|
||||
null);
|
||||
properties.setMaxAttempts(1);
|
||||
properties.setBackOffInitialInterval(0);
|
||||
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
|
||||
final AtomicReference<Object> payload = new AtomicReference<>();
|
||||
assertThat(pollableSource.poll(received -> {
|
||||
payload.set(received.getPayload());
|
||||
}, new ParameterizedTypeReference<Map<String, Foo>>() {
|
||||
})).isTrue();
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Foo> map = (Map<String, Foo>) payload.get();
|
||||
assertThat(map.size()).isEqualTo(1);
|
||||
assertThat(map.get("qux").getFoo()).isEqualTo("bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmbedded() {
|
||||
TestChannelBinder binder = createBinder();
|
||||
MessageConverterConfigurer configurer = this.context
|
||||
.getBean(MessageConverterConfigurer.class);
|
||||
|
||||
binder.setMessageSourceDelegate(() -> {
|
||||
MessageValues original = new MessageValues("foo".getBytes(),
|
||||
Collections.singletonMap(MessageHeaders.CONTENT_TYPE,
|
||||
"application/octet-stream"));
|
||||
byte[] payload = new byte[0];
|
||||
try {
|
||||
payload = EmbeddedHeaderUtils.embedHeaders(original,
|
||||
MessageHeaders.CONTENT_TYPE);
|
||||
}
|
||||
catch (Exception e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
return new GenericMessage<>(payload);
|
||||
});
|
||||
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
|
||||
null);
|
||||
properties.setHeaderMode(HeaderMode.embeddedHeaders);
|
||||
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
|
||||
this.messageConverter);
|
||||
configurer.configurePolledMessageSource(pollableSource, "foo");
|
||||
pollableSource.addInterceptor(new ChannelInterceptor() {
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
return MessageBuilder
|
||||
.withPayload(
|
||||
new String((byte[]) message.getPayload()).toUpperCase())
|
||||
.copyHeaders(message.getHeaders()).build();
|
||||
}
|
||||
|
||||
});
|
||||
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
|
||||
assertThat(pollableSource.poll(received -> {
|
||||
assertThat(received.getPayload()).isEqualTo("FOO");
|
||||
assertThat(received.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo("application/octet-stream");
|
||||
})).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrors() {
|
||||
TestChannelBinder binder = createBinder();
|
||||
MessageConverterConfigurer configurer = this.context
|
||||
.getBean(MessageConverterConfigurer.class);
|
||||
|
||||
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
|
||||
this.messageConverter);
|
||||
configurer.configurePolledMessageSource(pollableSource, "foo");
|
||||
pollableSource.addInterceptor(new ChannelInterceptor() {
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
return MessageBuilder
|
||||
.withPayload(((String) message.getPayload()).toUpperCase())
|
||||
.copyHeaders(message.getHeaders()).build();
|
||||
}
|
||||
|
||||
});
|
||||
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
|
||||
null);
|
||||
properties.setMaxAttempts(2);
|
||||
properties.setBackOffInitialInterval(0);
|
||||
properties.getRetryableExceptions().put(IllegalStateException.class, false);
|
||||
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
this.context.getBean(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME,
|
||||
SubscribableChannel.class).subscribe(m -> {
|
||||
latch.countDown();
|
||||
});
|
||||
final AtomicInteger count = new AtomicInteger();
|
||||
assertThat(pollableSource.poll(received -> {
|
||||
count.incrementAndGet();
|
||||
throw new RuntimeException("test recoverer");
|
||||
})).isTrue();
|
||||
assertThat(count.get()).isEqualTo(2);
|
||||
Message<?> lastError = binder.getLastError();
|
||||
assertThat(lastError).isNotNull();
|
||||
assertThat(((Exception) lastError.getPayload()).getCause().getMessage())
|
||||
.isEqualTo("test recoverer");
|
||||
assertThat(pollableSource.poll(received -> {
|
||||
count.incrementAndGet();
|
||||
throw new IllegalStateException("no retries");
|
||||
})).isTrue();
|
||||
assertThat(count.get()).isEqualTo(3);
|
||||
lastError = binder.getLastError();
|
||||
assertThat(lastError).isNotNull();
|
||||
assertThat(((Exception) lastError.getPayload()).getCause().getMessage())
|
||||
.isEqualTo("no retries");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorsNoRetry() {
|
||||
TestChannelBinder binder = createBinder();
|
||||
MessageConverterConfigurer configurer = this.context
|
||||
.getBean(MessageConverterConfigurer.class);
|
||||
|
||||
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
|
||||
this.messageConverter);
|
||||
configurer.configurePolledMessageSource(pollableSource, "foo");
|
||||
pollableSource.addInterceptor(new ChannelInterceptor() {
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
return MessageBuilder
|
||||
.withPayload(((String) message.getPayload()).toUpperCase())
|
||||
.copyHeaders(message.getHeaders()).build();
|
||||
}
|
||||
|
||||
});
|
||||
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
|
||||
null);
|
||||
properties.setMaxAttempts(1);
|
||||
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
this.context.getBean(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME,
|
||||
SubscribableChannel.class).subscribe(m -> {
|
||||
latch.countDown();
|
||||
});
|
||||
final AtomicInteger count = new AtomicInteger();
|
||||
assertThat(pollableSource.poll(received -> {
|
||||
count.incrementAndGet();
|
||||
throw new RuntimeException("test recoverer");
|
||||
})).isTrue();
|
||||
assertThat(count.get()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRequeue() {
|
||||
TestChannelBinder binder = createBinder();
|
||||
MessageConverterConfigurer configurer = this.context
|
||||
.getBean(MessageConverterConfigurer.class);
|
||||
|
||||
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
|
||||
this.messageConverter);
|
||||
configurer.configurePolledMessageSource(pollableSource, "foo");
|
||||
AcknowledgmentCallback callback = mock(AcknowledgmentCallback.class);
|
||||
pollableSource.addInterceptor(new ChannelInterceptor() {
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
return MessageBuilder.fromMessage(message)
|
||||
.setHeader(
|
||||
IntegrationMessageHeaderAccessor.ACKNOWLEDGMENT_CALLBACK,
|
||||
callback)
|
||||
.build();
|
||||
}
|
||||
|
||||
});
|
||||
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(null);
|
||||
properties.setMaxAttempts(2);
|
||||
properties.setBackOffInitialInterval(0);
|
||||
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
|
||||
final AtomicInteger count = new AtomicInteger();
|
||||
try {
|
||||
assertThat(pollableSource.poll(received -> {
|
||||
count.incrementAndGet();
|
||||
throw new RequeueCurrentMessageException("test retry");
|
||||
})).isTrue();
|
||||
}
|
||||
catch (Exception e) {
|
||||
// no op
|
||||
}
|
||||
assertThat(count.get()).isEqualTo(2);
|
||||
verify(callback).acknowledge(Status.REQUEUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRequeueWithNoAcknowledgementCallback() {
|
||||
TestChannelBinder binder = createBinder();
|
||||
MessageConverterConfigurer configurer = this.context
|
||||
.getBean(MessageConverterConfigurer.class);
|
||||
|
||||
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
|
||||
this.messageConverter);
|
||||
configurer.configurePolledMessageSource(pollableSource, "foo");
|
||||
pollableSource.addInterceptor(new ChannelInterceptor() {
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
return MessageBuilder.fromMessage(message)
|
||||
.build();
|
||||
}
|
||||
|
||||
});
|
||||
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(null);
|
||||
properties.setMaxAttempts(2);
|
||||
properties.setBackOffInitialInterval(0);
|
||||
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
|
||||
final AtomicInteger count = new AtomicInteger();
|
||||
|
||||
assertThat(pollableSource.poll(received -> {
|
||||
count.incrementAndGet();
|
||||
throw new RequeueCurrentMessageException("test retry");
|
||||
})).isTrue();
|
||||
|
||||
assertThat(count.get()).isEqualTo(2);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRequeueFromErrorFlow() {
|
||||
TestChannelBinder binder = createBinder();
|
||||
MessageConverterConfigurer configurer = this.context
|
||||
.getBean(MessageConverterConfigurer.class);
|
||||
|
||||
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
|
||||
this.messageConverter);
|
||||
configurer.configurePolledMessageSource(pollableSource, "foo");
|
||||
AcknowledgmentCallback callback = mock(AcknowledgmentCallback.class);
|
||||
pollableSource.addInterceptor(new ChannelInterceptor() {
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
return MessageBuilder.fromMessage(message)
|
||||
.setHeader(
|
||||
IntegrationMessageHeaderAccessor.ACKNOWLEDGMENT_CALLBACK,
|
||||
callback)
|
||||
.build();
|
||||
}
|
||||
|
||||
});
|
||||
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(null);
|
||||
properties.setMaxAttempts(1);
|
||||
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
|
||||
SubscribableChannel errorChannel = new DirectChannel();
|
||||
errorChannel.subscribe(msg -> {
|
||||
throw new RequeueCurrentMessageException((Throwable) msg.getPayload());
|
||||
});
|
||||
pollableSource.setErrorChannel(errorChannel);
|
||||
try {
|
||||
pollableSource.poll(received -> {
|
||||
throw new RuntimeException("test requeue from error flow");
|
||||
});
|
||||
}
|
||||
catch (Exception e) {
|
||||
// no op
|
||||
}
|
||||
verify(callback).acknowledge(Status.REQUEUE);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void testAutoStartupOff() {
|
||||
TestChannelBinder binder = createBinder();
|
||||
binder.setMessageSourceDelegate(new LifecycleMessageSource(
|
||||
() -> new GenericMessage<>("{\"foo\":\"bar\"}".getBytes())));
|
||||
MessageConverterConfigurer configurer = this.context
|
||||
.getBean(MessageConverterConfigurer.class);
|
||||
|
||||
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
|
||||
this.messageConverter);
|
||||
configurer.configurePolledMessageSource(pollableSource, "foo");
|
||||
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
|
||||
null);
|
||||
properties.setAutoStartup(false);
|
||||
|
||||
Binding<PollableSource<MessageHandler>> pollableSourceBinding = binder
|
||||
.bindPollableConsumer("foo", "bar", pollableSource, properties);
|
||||
|
||||
assertThat(pollableSourceBinding.isRunning()).isFalse();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void testAutoStartupOn() {
|
||||
TestChannelBinder binder = createBinder();
|
||||
binder.setMessageSourceDelegate(new LifecycleMessageSource(
|
||||
() -> new GenericMessage<>("{\"foo\":\"bar\"}".getBytes())));
|
||||
MessageConverterConfigurer configurer = this.context
|
||||
.getBean(MessageConverterConfigurer.class);
|
||||
|
||||
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
|
||||
this.messageConverter);
|
||||
configurer.configurePolledMessageSource(pollableSource, "foo");
|
||||
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
|
||||
null);
|
||||
properties.setAutoStartup(true);
|
||||
|
||||
Binding<PollableSource<MessageHandler>> pollableSourceBinding = binder
|
||||
.bindPollableConsumer("foo", "bar", pollableSource, properties);
|
||||
|
||||
assertThat(pollableSourceBinding.isRunning()).isTrue();
|
||||
}
|
||||
|
||||
private TestChannelBinder createBinder(String... args) {
|
||||
this.context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration())
|
||||
.web(WebApplicationType.NONE).run(args);
|
||||
TestChannelBinder binder = this.context.getBean(TestChannelBinder.class);
|
||||
return binder;
|
||||
}
|
||||
|
||||
public static class Foo {
|
||||
|
||||
private String foo;
|
||||
|
||||
protected String getFoo() {
|
||||
return this.foo;
|
||||
}
|
||||
|
||||
protected void setFoo(String foo) {
|
||||
this.foo = foo;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class LifecycleMessageSource<T> implements MessageSource<T>, Lifecycle {
|
||||
private final MessageSource<T> delegate;
|
||||
|
||||
private boolean running = false;
|
||||
|
||||
public LifecycleMessageSource(MessageSource<T> delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<T> receive() {
|
||||
return this.delegate.receive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.running = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return this.running;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,830 @@
|
||||
/*
|
||||
* Copyright 2017-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.tck;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.function.json.JsonMapper;
|
||||
import org.springframework.cloud.stream.binder.test.InputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.OutputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinder;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
import org.springframework.messaging.converter.AbstractMessageConverter;
|
||||
import org.springframework.messaging.converter.MessageConversionException;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Sort of a TCK test suite to validate payload conversion is done properly by interacting
|
||||
* with binder's input/output destinations instead of its bridged channels. This means
|
||||
* that all payloads (sent/received) must be expressed in the wire format (byte[])
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Gary Russell
|
||||
*
|
||||
*/
|
||||
public class ContentTypeTckTests {
|
||||
|
||||
@Test
|
||||
void stringToMapMessageStreamListener() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
StringToMapMessageConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(new String(outputMessage.getPayload())).isEqualTo("oleg");
|
||||
}
|
||||
|
||||
@Test
|
||||
void pojoToPojo() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
PojoToPojoConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo(jsonPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pojoToString() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
PojoToStringConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo("oleg");
|
||||
}
|
||||
|
||||
@Test
|
||||
void pojoToStringOutboundContentTypeBinding() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
PojoToStringConfiguration.class).web(WebApplicationType.NONE).run(
|
||||
"--spring.cloud.stream.bindings.echo-out-0.contentType=text/plain",
|
||||
"--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeTypeUtils.TEXT_PLAIN_VALUE);
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo("oleg");
|
||||
}
|
||||
|
||||
@Test
|
||||
void pojoToByteArray() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
PojoToByteArrayConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo("oleg");
|
||||
}
|
||||
|
||||
@Test
|
||||
void pojoToByteArrayOutboundContentTypeBinding() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
PojoToByteArrayConfiguration.class).web(WebApplicationType.NONE).run(
|
||||
"--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo("oleg");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringToPojoInboundContentTypeBinding() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
StringToPojoConfiguration.class).web(WebApplicationType.NONE).run(
|
||||
"--spring.cloud.stream.bindings.echo-on-0.contentType=text/plain",
|
||||
"--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo(jsonPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
void typelessToPojoInboundContentTypeBinding() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
TypelessToPojoConfiguration.class).web(WebApplicationType.NONE).run(
|
||||
"--spring.cloud.stream.bindings.echo-in-0.contentType=text/plain",
|
||||
"--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo(jsonPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
void typelessToPojoInboundContentTypeBindingJson() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
TypelessToPojoConfiguration.class).web(WebApplicationType.NONE).run(
|
||||
"--spring.cloud.stream.bindings.echo-in-0.contentType=application/json",
|
||||
"--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo(jsonPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
void typelessMessageToPojoInboundContentTypeBinding() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
TypelessMessageToPojoConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.cloud.stream.bindings.echo-in-0.contentType=text/plain",
|
||||
"--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo(jsonPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
void typelessMessageToPojoInboundContentTypeBindingJson() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
TypelessMessageToPojoConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.cloud.stream.bindings.echo-in-0.contentType=application/json",
|
||||
"--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo(jsonPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
void typelessToPojoWithTextHeaderContentTypeBinding() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
TypelessToPojoConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(MessageBuilder.withPayload(jsonPayload.getBytes())
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE, MimeType.valueOf("text/plain"))
|
||||
.build());
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo(jsonPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
void typelessToPojoOutboundContentTypeBinding() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
TypelessToMessageConfiguration.class).web(WebApplicationType.NONE).run(
|
||||
"--spring.cloud.stream.bindings.echo-out-0.contentType=text/plain",
|
||||
"--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(MessageBuilder.withPayload(jsonPayload.getBytes())
|
||||
.setHeader("contentType", new MimeType("text", "plain")).build());
|
||||
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
System.out.println(new String(outputMessage.getPayload()));
|
||||
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeTypeUtils.TEXT_PLAIN_VALUE);
|
||||
assertThat(outputMessage.getPayload())
|
||||
.isEqualTo(jsonPayload.getBytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundMessageWithTextContentTypeOnly() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
TypelessToMessageTextOnlyContentTypeConfiguration.class)
|
||||
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(MessageBuilder.withPayload(jsonPayload.getBytes())
|
||||
.setHeader("contentType", new MimeType("text")).build());
|
||||
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE).toString())
|
||||
.startsWith("text/");
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo(jsonPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringToPojoInboundContentTypeHeader() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
StringToPojoConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes(),
|
||||
new MessageHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE,
|
||||
MimeTypeUtils.TEXT_PLAIN))));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo(jsonPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
void byteArrayToPojoInboundContentTypeBinding() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
ByteArrayToPojoConfiguration.class).web(WebApplicationType.NONE).run(
|
||||
"--spring.cloud.stream.bindings.echo-in-0.contentType=text/plain",
|
||||
"--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo(jsonPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
void byteArrayToPojoInboundContentTypeHeader() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
StringToPojoConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes(),
|
||||
new MessageHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE,
|
||||
MimeTypeUtils.TEXT_PLAIN))));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo(jsonPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
void byteArrayToByteArray() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
ByteArrayToByteArrayConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo(jsonPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
void byteArrayToByteArrayInboundOutboundContentTypeBinding() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
ByteArrayToByteArrayConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.cloud.stream.bindings.input.contentType=text/plain",
|
||||
"--spring.cloud.stream.bindings.output.contentType=text/plain",
|
||||
"--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo(jsonPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pojoMessageToStringMessage() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
PojoMessageToStringMessageConfiguration.class)
|
||||
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeTypeUtils.TEXT_PLAIN_VALUE);
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo("oleg");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customMessageConverter_defaultContentTypeBinding() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
StringToStringConfiguration.class, CustomConverters.class)
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.cloud.stream.default.contentType=foo/bar",
|
||||
"--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(outputMessage).isNotNull();
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo("FooBarMessageConverter");
|
||||
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo("foo/bar");
|
||||
}
|
||||
|
||||
// Failure tests
|
||||
|
||||
@Test
|
||||
void _jsonToPojoWrongDefaultContentTypeProperty() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
PojoToPojoConfiguration.class).web(WebApplicationType.NONE).run(
|
||||
"--spring.cloud.stream.default.contentType=text/plain",
|
||||
"--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
TestChannelBinder binder = context.getBean(TestChannelBinder.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
assertThat(binder.getLastError().getPayload() instanceof MessagingException)
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void _toStringDefaultContentTypePropertyUnknownContentType() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
StringToStringConfiguration.class).web(WebApplicationType.NONE).run(
|
||||
"--spring.cloud.stream.default.contentType=foo/bar",
|
||||
"--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
TestChannelBinder binder = context.getBean(TestChannelBinder.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
assertThat(
|
||||
binder.getLastError().getPayload() instanceof MessageConversionException)
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void toCollectionWithParameterizedType() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
CollectionWithParameterizedTypes.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "[{\"person\":{\"name\":\"jon\"},\"id\":123},{\"person\":{\"name\":\"jane\"},\"id\":456}]";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(outputMessage.getPayload()).isEqualTo(jsonPayload.getBytes());
|
||||
}
|
||||
|
||||
// ======
|
||||
@Test
|
||||
void testWithMapInputParameter() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
MapInputConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo(jsonPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithMapPayloadParameter() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
MapInputConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo(jsonPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithListInputParameter() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
ListInputConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false", "--debug");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "[\"foo\",\"bar\"]";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isEqualTo(jsonPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // TODO fix it. We can recognize MessageHeaders and parse it out of the message properly
|
||||
void testWithMessageHeadersInputParameter() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
MessageHeadersInputConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String jsonPayload = "{\"name\":\"oleg\"}";
|
||||
source.send(new GenericMessage<>(jsonPayload.getBytes()));
|
||||
Message<byte[]> outputMessage = target.receive();
|
||||
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
|
||||
.isNotEqualTo(jsonPayload);
|
||||
assertThat(outputMessage.getHeaders().containsKey(MessageHeaders.ID)).isTrue();
|
||||
assertThat(outputMessage.getHeaders().containsKey(MessageHeaders.CONTENT_TYPE))
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class CollectionWithParameterizedTypes {
|
||||
|
||||
@Bean
|
||||
public Function<List<Employee<Person>>, List<Employee<Person>>> echo() {
|
||||
return value -> {
|
||||
assertThat(value.get(0) != null).isTrue();
|
||||
return value;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class PojoToPojoConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<Person, Person> ecgo() {
|
||||
return value -> value;
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class PojoToStringConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<Person, String> echo() {
|
||||
return value -> value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class PojoToByteArrayConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<Person, byte[]> echo() {
|
||||
return value -> value.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class ByteArrayToPojoConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<byte[], Person> echo(JsonMapper mapper) {
|
||||
return value -> mapper.fromJson(value, Person.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class StringToPojoConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<String, Person> echo(JsonMapper mapper) {
|
||||
return value -> mapper.fromJson(value, Person.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class TypelessToPojoConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<Object, Person> echo(JsonMapper mapper) {
|
||||
return value -> value instanceof byte[]
|
||||
? mapper.fromJson((byte[]) value, Person.class)
|
||||
: mapper.fromJson((String) value, Person.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class TypelessMessageToPojoConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<Message<?>, Person> echo(JsonMapper mapper) {
|
||||
return message -> message.getPayload() instanceof byte[]
|
||||
? mapper.fromJson((byte[]) message.getPayload(), Person.class)
|
||||
: mapper.fromJson((String) message.getPayload(), Person.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class TypelessToMessageConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<Object, Message<?>> echo() {
|
||||
return value -> MessageBuilder.withPayload(value.toString())
|
||||
.setHeader("contentType", new MimeType("text", "plain")).build();
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class TypelessToMessageTextOnlyContentTypeConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<?, Message<?>> echo() {
|
||||
return value -> {
|
||||
return MessageBuilder.withPayload(value.toString())
|
||||
.setHeader("expected-content-type", "text/*").build();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class ByteArrayToByteArrayConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<byte[], byte[]> echo() {
|
||||
return value -> value;
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class StringToStringConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> echo() {
|
||||
return v -> v;
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class StringToMapMessageConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<Message<Map<?, ?>>, String> echo() {
|
||||
return value -> {
|
||||
assertThat(value.getPayload() instanceof Map).isTrue();
|
||||
return (String) value.getPayload().get("name");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class PojoMessageToStringMessageConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<Message<Person>, Message<String>> echo() {
|
||||
return value -> MessageBuilder.withPayload(value.getPayload().toString())
|
||||
.setHeader("expected-content-type", MimeTypeUtils.TEXT_PLAIN_VALUE)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Employee<P> {
|
||||
|
||||
private P person;
|
||||
|
||||
private int id;
|
||||
|
||||
public int getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public P getPerson() {
|
||||
return this.person;
|
||||
}
|
||||
|
||||
public void setPerson(P person) {
|
||||
this.person = person;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Person {
|
||||
|
||||
private String name;
|
||||
|
||||
public Person() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public Person(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public static class CustomConverters {
|
||||
|
||||
@Bean
|
||||
public FooBarMessageConverter fooBarMessageConverter() {
|
||||
return new FooBarMessageConverter(MimeType.valueOf("foo/bar"));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AlwaysStringKryoMessageConverter kryoOverrideMessageConverter() {
|
||||
return new AlwaysStringKryoMessageConverter(
|
||||
MimeType.valueOf("application/x-java-object"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Even though this MessageConverter has nothing to do with Kryo it still shows
|
||||
* how Kryo conversion can be customized/overriden since it simply overriding a
|
||||
* converter for contentType 'application/x-java-object'.
|
||||
*
|
||||
*/
|
||||
public static class AlwaysStringKryoMessageConverter
|
||||
extends AbstractMessageConverter {
|
||||
|
||||
public AlwaysStringKryoMessageConverter(MimeType supportedMimeType) {
|
||||
super(supportedMimeType);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supports(Class<?> clazz) {
|
||||
return clazz == null || String.class.isAssignableFrom(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object convertFromInternal(Message<?> message, Class<?> targetClass,
|
||||
@Nullable Object conversionHint) {
|
||||
return this.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object convertToInternal(Object payload,
|
||||
@Nullable MessageHeaders headers, @Nullable Object conversionHint) {
|
||||
return ((String) payload).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class FooBarMessageConverter extends AbstractMessageConverter {
|
||||
|
||||
protected FooBarMessageConverter(MimeType supportedMimeType) {
|
||||
super(supportedMimeType);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supports(Class<?> clazz) {
|
||||
return clazz != null && String.class.isAssignableFrom(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object convertFromInternal(Message<?> message, Class<?> targetClass,
|
||||
@Nullable Object conversionHint) {
|
||||
return this.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object convertToInternal(Object payload,
|
||||
@Nullable MessageHeaders headers, @Nullable Object conversionHint) {
|
||||
return ((String) payload).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class MapInputConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<Map<?, ?>, Map<?, ?>> echo() {
|
||||
return value -> value;
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class ListInputConfiguration {
|
||||
@Bean
|
||||
public Function<List<?>, List<?>> echo() {
|
||||
return v -> v;
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class MessageHeadersInputConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<MessageHeaders, Map<?, ?>> echo() {
|
||||
return v -> v;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.tck;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.binder.test.InputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.integration.annotation.ServiceActivator;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Oleg Zhurakousky
|
||||
*
|
||||
*/
|
||||
@Disabled
|
||||
public class ErrorHandlingTests {
|
||||
|
||||
@Test
|
||||
void testGlobalErrorWithMessage() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(GlobalErrorHandlerWithErrorMessageConfig.class)
|
||||
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
source.send(new GenericMessage<>("foo".getBytes()));
|
||||
GlobalErrorHandlerWithErrorMessageConfig config = context
|
||||
.getBean(GlobalErrorHandlerWithErrorMessageConfig.class);
|
||||
assertThat(config.globalErroInvoked).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGlobalErrorWithThrowable() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(GlobalErrorHandlerWithThrowableConfig.class)
|
||||
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false");
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
source.send(new GenericMessage<>("foo".getBytes()));
|
||||
GlobalErrorHandlerWithThrowableConfig config = context.getBean(GlobalErrorHandlerWithThrowableConfig.class);
|
||||
assertThat(config.globalErroInvoked).isTrue();
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class GlobalErrorHandlerWithErrorMessageConfig {
|
||||
|
||||
private boolean globalErroInvoked;
|
||||
|
||||
@Bean
|
||||
public Function<String, String> func() {
|
||||
return v -> {
|
||||
throw new RuntimeException("test exception");
|
||||
};
|
||||
}
|
||||
|
||||
@ServiceActivator(inputChannel = "errorChannel")
|
||||
public void generalError(Message<?> message) {
|
||||
this.globalErroInvoked = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class GlobalErrorHandlerWithThrowableConfig {
|
||||
|
||||
private boolean globalErroInvoked;
|
||||
|
||||
@Bean
|
||||
public Function<String, String> func() {
|
||||
return v -> {
|
||||
throw new RuntimeException("test exception");
|
||||
};
|
||||
}
|
||||
|
||||
@ServiceActivator(inputChannel = "errorChannel")
|
||||
public void generalError(Throwable exception) {
|
||||
this.globalErroInvoked = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,639 @@
|
||||
/*
|
||||
* Copyright 2015-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binding;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
|
||||
import org.springframework.cloud.stream.binder.Binder;
|
||||
import org.springframework.cloud.stream.binder.BinderChildContextInitializer;
|
||||
import org.springframework.cloud.stream.binder.BinderConfiguration;
|
||||
import org.springframework.cloud.stream.binder.BinderFactory;
|
||||
import org.springframework.cloud.stream.binder.BinderType;
|
||||
import org.springframework.cloud.stream.binder.BinderTypeRegistry;
|
||||
import org.springframework.cloud.stream.binder.Binding;
|
||||
import org.springframework.cloud.stream.binder.ConsumerProperties;
|
||||
import org.springframework.cloud.stream.binder.DefaultBinderFactory;
|
||||
import org.springframework.cloud.stream.binder.DefaultBinderTypeRegistry;
|
||||
import org.springframework.cloud.stream.binder.ProducerProperties;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.cloud.stream.config.BindingProperties;
|
||||
import org.springframework.cloud.stream.config.BindingServiceConfiguration;
|
||||
import org.springframework.cloud.stream.config.BindingServiceProperties;
|
||||
import org.springframework.cloud.stream.reflection.GenericsUtils;
|
||||
import org.springframework.cloud.stream.utils.IntegrationTestsMockBinderConfiguration;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @author Mark Fisher
|
||||
* @author Marius Bogoevici
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @author Janne Valkealahti
|
||||
* @author Soby Chacko
|
||||
* @author Michael Michailidis
|
||||
* @author Chris Bono
|
||||
*/
|
||||
public class BindingServiceTests {
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@Test
|
||||
void testDefaultGroup() throws Exception {
|
||||
BindingServiceProperties properties = new BindingServiceProperties();
|
||||
Map<String, BindingProperties> bindingProperties = new HashMap<>();
|
||||
BindingProperties props = new BindingProperties();
|
||||
props.setDestination("foo");
|
||||
final String inputChannelName = "input";
|
||||
bindingProperties.put(inputChannelName, props);
|
||||
properties.setBindings(bindingProperties);
|
||||
DefaultBinderFactory binderFactory = createMockBinderFactory();
|
||||
Binder binder = binderFactory.getBinder("mock", MessageChannel.class);
|
||||
BindingService service = new BindingService(properties, binderFactory, new ObjectMapper());
|
||||
MessageChannel inputChannel = new DirectChannel();
|
||||
Binding<MessageChannel> mockBinding = Mockito.mock(Binding.class);
|
||||
when(binder.bindConsumer(eq("foo"), isNull(), same(inputChannel),
|
||||
any(ConsumerProperties.class))).thenReturn(mockBinding);
|
||||
Collection<Binding<MessageChannel>> bindings = service.bindConsumer(inputChannel,
|
||||
inputChannelName);
|
||||
assertThat(bindings).hasSize(1);
|
||||
Binding<MessageChannel> binding = bindings.iterator().next();
|
||||
assertThat(binding).isSameAs(mockBinding);
|
||||
service.unbindConsumers(inputChannelName);
|
||||
verify(binder).bindConsumer(eq("foo"), isNull(), same(inputChannel),
|
||||
any(ConsumerProperties.class));
|
||||
verify(binding).unbind();
|
||||
binderFactory.destroy();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@Test
|
||||
void testMultipleConsumerBindings() throws Exception {
|
||||
BindingServiceProperties properties = new BindingServiceProperties();
|
||||
Map<String, BindingProperties> bindingProperties = new HashMap<>();
|
||||
BindingProperties props = new BindingProperties();
|
||||
props.setDestination("foo,bar");
|
||||
final String inputChannelName = "input";
|
||||
bindingProperties.put(inputChannelName, props);
|
||||
|
||||
properties.setBindings(bindingProperties);
|
||||
|
||||
DefaultBinderFactory binderFactory = createMockBinderFactory();
|
||||
|
||||
Binder binder = binderFactory.getBinder("mock", MessageChannel.class);
|
||||
BindingService service = new BindingService(properties, binderFactory, new ObjectMapper());
|
||||
MessageChannel inputChannel = new DirectChannel();
|
||||
|
||||
Binding<MessageChannel> mockBinding1 = Mockito.mock(Binding.class);
|
||||
Binding<MessageChannel> mockBinding2 = Mockito.mock(Binding.class);
|
||||
|
||||
when(binder.bindConsumer(eq("foo"), isNull(), same(inputChannel),
|
||||
any(ConsumerProperties.class))).thenReturn(mockBinding1);
|
||||
when(binder.bindConsumer(eq("bar"), isNull(), same(inputChannel),
|
||||
any(ConsumerProperties.class))).thenReturn(mockBinding2);
|
||||
|
||||
Collection<Binding<MessageChannel>> bindings = service.bindConsumer(inputChannel,
|
||||
"input");
|
||||
assertThat(bindings).hasSize(2);
|
||||
|
||||
Iterator<Binding<MessageChannel>> iterator = bindings.iterator();
|
||||
Binding<MessageChannel> binding1 = iterator.next();
|
||||
Binding<MessageChannel> binding2 = iterator.next();
|
||||
|
||||
assertThat(binding1).isSameAs(mockBinding1);
|
||||
assertThat(binding2).isSameAs(mockBinding2);
|
||||
|
||||
service.unbindConsumers("input");
|
||||
|
||||
verify(binder).bindConsumer(eq("foo"), isNull(), same(inputChannel),
|
||||
any(ConsumerProperties.class));
|
||||
verify(binder).bindConsumer(eq("bar"), isNull(), same(inputChannel),
|
||||
any(ConsumerProperties.class));
|
||||
verify(binding1).unbind();
|
||||
verify(binding2).unbind();
|
||||
|
||||
binderFactory.destroy();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@Test
|
||||
void testMultipleConsumerBindingsFromIndexList() throws Exception {
|
||||
BindingServiceProperties properties = new BindingServiceProperties();
|
||||
Map<String, BindingProperties> bindingProperties = new HashMap<>();
|
||||
BindingProperties props = new BindingProperties();
|
||||
props.setDestination("foo");
|
||||
|
||||
ConsumerProperties consumer = properties.getConsumerProperties("input");
|
||||
consumer.setInstanceIndexList(Arrays.asList(0, 1));
|
||||
consumer.setInstanceCount(2);
|
||||
consumer.setPartitioned(true);
|
||||
props.setConsumer(consumer);
|
||||
|
||||
final String inputChannelName = "input";
|
||||
bindingProperties.put(inputChannelName, props);
|
||||
|
||||
properties.setBindings(bindingProperties);
|
||||
|
||||
DefaultBinderFactory binderFactory = createMockBinderFactory();
|
||||
|
||||
Binder binder = binderFactory.getBinder("mock", MessageChannel.class);
|
||||
BindingService service = new BindingService(properties, binderFactory, new ObjectMapper());
|
||||
MessageChannel inputChannel = new DirectChannel();
|
||||
|
||||
Binding<MessageChannel> mockBinding1 = Mockito.mock(Binding.class, "FirstBinding");
|
||||
Binding<MessageChannel> mockBinding2 = Mockito.mock(Binding.class, "SecondBinding");
|
||||
|
||||
ArgumentCaptor<ConsumerProperties> captor = ArgumentCaptor.forClass(ConsumerProperties.class);
|
||||
|
||||
when(binder.bindConsumer(eq("foo"), isNull(), same(inputChannel),
|
||||
any(ConsumerProperties.class))).thenReturn(mockBinding1).thenReturn(mockBinding2);
|
||||
|
||||
Collection<Binding<MessageChannel>> bindings = service.bindConsumer(inputChannel,
|
||||
"input");
|
||||
assertThat(bindings).hasSize(2);
|
||||
|
||||
Iterator<Binding<MessageChannel>> iterator = bindings.iterator();
|
||||
Binding<MessageChannel> binding1 = iterator.next();
|
||||
Binding<MessageChannel> binding2 = iterator.next();
|
||||
|
||||
assertThat(binding1).isSameAs(mockBinding1);
|
||||
assertThat(binding2).isSameAs(mockBinding2);
|
||||
|
||||
service.unbindConsumers("input");
|
||||
|
||||
verify(binder, times(2)).bindConsumer(eq("foo"), isNull(), same(inputChannel),
|
||||
captor.capture());
|
||||
verify(binding1).unbind();
|
||||
verify(binding2).unbind();
|
||||
|
||||
List<ConsumerProperties> allValues = captor.getAllValues();
|
||||
|
||||
assertThat(allValues.size()).isEqualTo(2);
|
||||
|
||||
assertThat(allValues.get(0).getInstanceIndex()).isEqualTo(0);
|
||||
assertThat(allValues.get(1).getInstanceIndex()).isEqualTo(1);
|
||||
|
||||
binderFactory.destroy();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@Test
|
||||
void testConsumerBindingWhenMultiplexingIsEnabled() throws Exception {
|
||||
BindingServiceProperties properties = new BindingServiceProperties();
|
||||
Map<String, BindingProperties> bindingProperties = new HashMap<>();
|
||||
BindingProperties props = new BindingProperties();
|
||||
props.setDestination("foo,bar");
|
||||
|
||||
ConsumerProperties consumer = properties.getConsumerProperties("input");
|
||||
consumer.setMultiplex(true);
|
||||
props.setConsumer(consumer);
|
||||
|
||||
final String inputChannelName = "input";
|
||||
bindingProperties.put(inputChannelName, props);
|
||||
|
||||
properties.setBindings(bindingProperties);
|
||||
|
||||
DefaultBinderFactory binderFactory = createMockBinderFactory();
|
||||
|
||||
Binder binder = binderFactory.getBinder("mock", MessageChannel.class);
|
||||
BindingService service = new BindingService(properties, binderFactory, new ObjectMapper());
|
||||
MessageChannel inputChannel = new DirectChannel();
|
||||
|
||||
Binding<MessageChannel> mockBinding1 = Mockito.mock(Binding.class);
|
||||
|
||||
when(binder.bindConsumer(eq("foo,bar"), isNull(), same(inputChannel),
|
||||
any(ConsumerProperties.class))).thenReturn(mockBinding1);
|
||||
|
||||
Collection<Binding<MessageChannel>> bindings = service.bindConsumer(inputChannel,
|
||||
"input");
|
||||
assertThat(bindings).hasSize(1);
|
||||
|
||||
Iterator<Binding<MessageChannel>> iterator = bindings.iterator();
|
||||
Binding<MessageChannel> binding1 = iterator.next();
|
||||
|
||||
assertThat(binding1).isSameAs(mockBinding1);
|
||||
|
||||
service.unbindConsumers("input");
|
||||
|
||||
verify(binder).bindConsumer(eq("foo,bar"), isNull(), same(inputChannel),
|
||||
any(ConsumerProperties.class));
|
||||
verify(binding1).unbind();
|
||||
|
||||
binderFactory.destroy();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@Test
|
||||
void testExplicitGroup() throws Exception {
|
||||
BindingServiceProperties properties = new BindingServiceProperties();
|
||||
Map<String, BindingProperties> bindingProperties = new HashMap<>();
|
||||
BindingProperties props = new BindingProperties();
|
||||
props.setDestination("foo");
|
||||
props.setGroup("fooGroup");
|
||||
final String inputChannelName = "input";
|
||||
bindingProperties.put(inputChannelName, props);
|
||||
properties.setBindings(bindingProperties);
|
||||
DefaultBinderFactory binderFactory = createMockBinderFactory();
|
||||
Binder binder = binderFactory.getBinder("mock", MessageChannel.class);
|
||||
BindingService service = new BindingService(properties, binderFactory, new ObjectMapper());
|
||||
MessageChannel inputChannel = new DirectChannel();
|
||||
Binding<MessageChannel> mockBinding = Mockito.mock(Binding.class);
|
||||
when(binder.bindConsumer(eq("foo"), eq("fooGroup"), same(inputChannel),
|
||||
any(ConsumerProperties.class))).thenReturn(mockBinding);
|
||||
Collection<Binding<MessageChannel>> bindings = service.bindConsumer(inputChannel,
|
||||
inputChannelName);
|
||||
assertThat(bindings).hasSize(1);
|
||||
Binding<MessageChannel> binding = bindings.iterator().next();
|
||||
assertThat(binding).isSameAs(mockBinding);
|
||||
|
||||
service.unbindConsumers(inputChannelName);
|
||||
verify(binder).bindConsumer(eq("foo"), eq(props.getGroup()), same(inputChannel),
|
||||
any(ConsumerProperties.class));
|
||||
verify(binding).unbind();
|
||||
binderFactory.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testProducerPropertiesValidation() {
|
||||
BindingServiceProperties serviceProperties = new BindingServiceProperties();
|
||||
Map<String, BindingProperties> bindingProperties = new HashMap<>();
|
||||
BindingProperties props = new BindingProperties();
|
||||
ProducerProperties producerProperties = new ProducerProperties();
|
||||
producerProperties.setPartitionCount(0);
|
||||
props.setDestination("foo");
|
||||
props.setProducer(producerProperties);
|
||||
final String outputChannelName = "output";
|
||||
bindingProperties.put(outputChannelName, props);
|
||||
serviceProperties.setBindings(bindingProperties);
|
||||
DefaultBinderFactory binderFactory = createMockBinderFactory();
|
||||
BindingService service = new BindingService(serviceProperties, binderFactory, new ObjectMapper());
|
||||
MessageChannel outputChannel = new DirectChannel();
|
||||
try {
|
||||
service.bindProducer(outputChannel, outputChannelName);
|
||||
fail("Producer properties should be validated.");
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
assertThat(e)
|
||||
.hasMessageContaining("Partition count should be greater than zero.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultPropertyBehavior() {
|
||||
ConfigurableApplicationContext run = SpringApplication.run(
|
||||
DefaultConsumerPropertiesTestSink.class,
|
||||
"--server.port=0",
|
||||
"--spring.cloud.stream.output-bindings=output1",
|
||||
"--spring.cloud.stream.input-bindings=inputFooBarBuzz",
|
||||
"--spring.cloud.stream.default.contentType=text/plain",
|
||||
"--spring.cloud.stream.bindings.input1.contentType=application/json",
|
||||
"--spring.cloud.stream.default.group=foo",
|
||||
"--spring.cloud.stream.bindings.input2.group=bar",
|
||||
"--spring.cloud.stream.default.consumer.concurrency=5",
|
||||
"--spring.cloud.stream.bindings.input2.consumer.concurrency=1",
|
||||
"--spring.cloud.stream.bindings.input1.consumer.partitioned=true",
|
||||
"--spring.cloud.stream.default.producer.partitionCount=10",
|
||||
"--spring.cloud.stream.bindings.output2.producer.partitionCount=1",
|
||||
"--spring.cloud.stream.bindings.inputXyz.contentType=application/json",
|
||||
"--spring.cloud.stream.bindings.inputFooBar.contentType=application/avro",
|
||||
"--spring.cloud.stream.bindings.input_snake_case.contentType=application/avro");
|
||||
|
||||
BindingServiceProperties bindingServiceProperties = run.getBeanFactory()
|
||||
.getBean(BindingServiceProperties.class);
|
||||
Map<String, BindingProperties> bindings = bindingServiceProperties.getBindings();
|
||||
|
||||
assertThat(bindings.get("input1").getContentType()).isEqualTo("application/json");
|
||||
assertThat(bindings.get("input2").getContentType()).isEqualTo("text/plain");
|
||||
assertThat(bindings.get("input1").getGroup()).isEqualTo("foo");
|
||||
assertThat(bindings.get("input2").getGroup()).isEqualTo("bar");
|
||||
assertThat(bindings.get("input1").getConsumer().getConcurrency()).isEqualTo(5);
|
||||
assertThat(bindings.get("input2").getConsumer().getConcurrency()).isEqualTo(1);
|
||||
assertThat(bindings.get("input1").getConsumer().isPartitioned()).isEqualTo(true);
|
||||
assertThat(bindings.get("input2").getConsumer().isPartitioned()).isEqualTo(false);
|
||||
assertThat(bindings.get("output1-out-0").getProducer().getPartitionCount())
|
||||
.isEqualTo(10);
|
||||
assertThat(bindings.get("output2").getProducer().getPartitionCount())
|
||||
.isEqualTo(1);
|
||||
|
||||
assertThat(bindings.get("inputXyz").getContentType())
|
||||
.isEqualTo("application/json");
|
||||
assertThat(bindings.get("inputFooBar").getContentType())
|
||||
.isEqualTo("application/avro");
|
||||
assertThat(bindings.get("inputFooBarBuzz-in-0").getContentType())
|
||||
.isEqualTo("text/plain");
|
||||
assertThat(bindings.get("input_snake_case").getContentType())
|
||||
.isEqualTo("application/avro");
|
||||
|
||||
run.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testConsumerPropertiesValidation() {
|
||||
BindingServiceProperties serviceProperties = new BindingServiceProperties();
|
||||
Map<String, BindingProperties> bindingProperties = new HashMap<>();
|
||||
BindingProperties props = new BindingProperties();
|
||||
ConsumerProperties consumerProperties = new ConsumerProperties();
|
||||
consumerProperties.setConcurrency(0);
|
||||
props.setDestination("foo");
|
||||
props.setConsumer(consumerProperties);
|
||||
final String inputChannelName = "input";
|
||||
bindingProperties.put(inputChannelName, props);
|
||||
serviceProperties.setBindings(bindingProperties);
|
||||
DefaultBinderFactory binderFactory = createMockBinderFactory();
|
||||
BindingService service = new BindingService(serviceProperties, binderFactory, new ObjectMapper());
|
||||
MessageChannel inputChannel = new DirectChannel();
|
||||
try {
|
||||
service.bindConsumer(inputChannel, inputChannelName);
|
||||
fail("Consumer properties should be validated.");
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
assertThat(e)
|
||||
.hasMessageContaining("Concurrency should be greater than zero.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnknownBinderOnBindingFailure() {
|
||||
HashMap<String, String> properties = new HashMap<>();
|
||||
properties.put("spring.cloud.stream.bindings.input.destination", "fooInput");
|
||||
properties.put("spring.cloud.stream.bindings.input.binder", "mock");
|
||||
properties.put("spring.cloud.stream.bindings.output.destination", "fooOutput");
|
||||
properties.put("spring.cloud.stream.bindings.output.binder", "mockError");
|
||||
BindingServiceProperties bindingServiceProperties = createBindingServiceProperties(
|
||||
properties);
|
||||
BindingService bindingService = new BindingService(bindingServiceProperties,
|
||||
createMockBinderFactory(), new ObjectMapper());
|
||||
bindingService.bindConsumer(new DirectChannel(), "input");
|
||||
try {
|
||||
bindingService.bindProducer(new DirectChannel(), "output");
|
||||
fail("Expected 'Unknown binder configuration'");
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
assertThat(e).hasMessageContaining("Unknown binder configuration: mockError");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void testUnrecognizedBinderAllowedIfNotUsed() {
|
||||
HashMap<String, String> properties = new HashMap<>();
|
||||
properties.put("spring.cloud.stream.bindings.input.destination", "fooInput");
|
||||
properties.put("spring.cloud.stream.bindings.output.destination", "fooOutput");
|
||||
properties.put("spring.cloud.stream.defaultBinder", "mock1");
|
||||
properties.put("spring.cloud.stream.binders.mock1.type", "mock");
|
||||
properties.put("spring.cloud.stream.binders.kafka1.type", "kafka");
|
||||
BindingServiceProperties bindingServiceProperties = createBindingServiceProperties(
|
||||
properties);
|
||||
BinderFactory binderFactory = new BindingServiceConfiguration()
|
||||
.binderFactory(createMockBinderTypeRegistry(), bindingServiceProperties, Mockito.mock(ObjectProvider.class),
|
||||
Mockito.mock(BinderChildContextInitializer.class));
|
||||
BindingService bindingService = new BindingService(bindingServiceProperties,
|
||||
binderFactory, new ObjectMapper());
|
||||
bindingService.bindConsumer(new DirectChannel(), "input");
|
||||
bindingService.bindProducer(new DirectChannel(), "output");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void testUnrecognizedBinderDisallowedIfUsed() {
|
||||
HashMap<String, String> properties = new HashMap<>();
|
||||
properties.put("spring.cloud.stream.bindings.input.destination", "fooInput");
|
||||
properties.put("spring.cloud.stream.bindings.input.binder", "mock1");
|
||||
properties.put("spring.cloud.stream.bindings.output.destination", "fooOutput");
|
||||
properties.put("spring.cloud.stream.bindings.output.type", "kafka1");
|
||||
properties.put("spring.cloud.stream.binders.mock1.type", "mock");
|
||||
properties.put("spring.cloud.stream.binders.kafka1.type", "kafka");
|
||||
BindingServiceProperties bindingServiceProperties = createBindingServiceProperties(
|
||||
properties);
|
||||
BinderFactory binderFactory = new BindingServiceConfiguration()
|
||||
.binderFactory(createMockBinderTypeRegistry(), bindingServiceProperties, Mockito.mock(ObjectProvider.class),
|
||||
Mockito.mock(BinderChildContextInitializer.class));
|
||||
BindingService bindingService = new BindingService(bindingServiceProperties,
|
||||
binderFactory, new ObjectMapper());
|
||||
bindingService.bindConsumer(new DirectChannel(), "input");
|
||||
try {
|
||||
bindingService.bindProducer(new DirectChannel(), "output");
|
||||
fail("Expected 'Unknown binder configuration'");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertThat(e).hasMessageContaining("Binder type kafka is not defined");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResolveBindableType() {
|
||||
Class<?> bindableType = GenericsUtils.getParameterType(FooBinder.class,
|
||||
Binder.class, 0);
|
||||
assertThat(bindableType).isSameAs(SomeBindableType.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Test
|
||||
void testLateBindingProducer() throws Exception {
|
||||
BindingServiceProperties properties = new BindingServiceProperties();
|
||||
properties.setBindingRetryInterval(1);
|
||||
Map<String, BindingProperties> bindingProperties = new HashMap<>();
|
||||
BindingProperties props = new BindingProperties();
|
||||
props.setDestination("foo");
|
||||
final String outputChannelName = "output";
|
||||
bindingProperties.put(outputChannelName, props);
|
||||
properties.setBindings(bindingProperties);
|
||||
DefaultBinderFactory binderFactory = createMockBinderFactory();
|
||||
Binder binder = binderFactory.getBinder("mock", MessageChannel.class);
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.initialize();
|
||||
BindingService service = new BindingService(properties, binderFactory, scheduler, new ObjectMapper());
|
||||
MessageChannel outputChannel = new DirectChannel();
|
||||
final Binding<MessageChannel> mockBinding = Mockito.mock(Binding.class);
|
||||
final CountDownLatch fail = new CountDownLatch(2);
|
||||
doAnswer(i -> {
|
||||
fail.countDown();
|
||||
if (fail.getCount() == 1) {
|
||||
throw new RuntimeException("fail");
|
||||
}
|
||||
return mockBinding;
|
||||
}).when(binder).bindProducer(eq("foo"), same(outputChannel),
|
||||
any(ProducerProperties.class));
|
||||
Binding<MessageChannel> binding = service.bindProducer(outputChannel,
|
||||
outputChannelName);
|
||||
assertThat(fail.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(binding).isNotNull();
|
||||
Binding delegate = TestUtils.getPropertyValue(binding, "delegate", Binding.class);
|
||||
int n = 0;
|
||||
while (n++ < 300 && delegate == null) {
|
||||
Thread.sleep(100);
|
||||
delegate = TestUtils.getPropertyValue(binding, "delegate", Binding.class);
|
||||
}
|
||||
assertThat(delegate).isSameAs(mockBinding);
|
||||
service.unbindProducers(outputChannelName);
|
||||
verify(binder, times(2)).bindProducer(eq("foo"), same(outputChannel),
|
||||
any(ProducerProperties.class));
|
||||
verify(delegate).unbind();
|
||||
binderFactory.destroy();
|
||||
scheduler.destroy();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void testBindingAutostartup() throws Exception {
|
||||
ApplicationContext context = new SpringApplicationBuilder(FooConfiguration.class)
|
||||
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.bindings.input-in-0.consumer.auto-startup=false");
|
||||
BindingService bindingService = context.getBean(BindingService.class);
|
||||
|
||||
Field cbField = ReflectionUtils.findField(BindingService.class,
|
||||
"consumerBindings");
|
||||
cbField.setAccessible(true);
|
||||
Map<String, Object> cbMap = (Map<String, Object>) cbField.get(bindingService);
|
||||
Binding<?> inputBinding = ((List<Binding<?>>) cbMap.get("input-in-0")).get(0);
|
||||
assertThat(inputBinding.isRunning()).isFalse();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void testBindingNameAsTopLevelProperty() throws Exception {
|
||||
ApplicationContext context = new SpringApplicationBuilder(BarConfiguration.class)
|
||||
.web(WebApplicationType.NONE).run();
|
||||
|
||||
final BindingServiceProperties bindingServiceProperties = context.getBean(BindingServiceProperties.class);
|
||||
|
||||
final ConsumerProperties consumerProperties = bindingServiceProperties.getConsumerProperties("myFunction-in-0");
|
||||
assertThat(consumerProperties.getBindingName()).isEqualTo("myFunction-in-0");
|
||||
final ProducerProperties producerProperties = bindingServiceProperties.getProducerProperties("myFunction-out-0");
|
||||
assertThat(producerProperties.getBindingName()).isEqualTo("myFunction-out-0");
|
||||
}
|
||||
|
||||
private DefaultBinderFactory createMockBinderFactory() {
|
||||
BinderTypeRegistry binderTypeRegistry = createMockBinderTypeRegistry();
|
||||
return new DefaultBinderFactory(
|
||||
Collections.singletonMap("mock",
|
||||
new BinderConfiguration("mock", new HashMap<>(), true, true)),
|
||||
binderTypeRegistry, null);
|
||||
}
|
||||
|
||||
private DefaultBinderTypeRegistry createMockBinderTypeRegistry() {
|
||||
return new DefaultBinderTypeRegistry(Collections.singletonMap("mock",
|
||||
new BinderType("mock", new Class[] { IntegrationTestsMockBinderConfiguration.class })));
|
||||
}
|
||||
|
||||
private BindingServiceProperties createBindingServiceProperties(
|
||||
HashMap<String, String> properties) {
|
||||
BindingServiceProperties bindingServiceProperties = new BindingServiceProperties();
|
||||
org.springframework.boot.context.properties.bind.Binder propertiesBinder;
|
||||
propertiesBinder = new org.springframework.boot.context.properties.bind.Binder(
|
||||
new MapConfigurationPropertySource(properties));
|
||||
propertiesBinder.bind("spring.cloud.stream",
|
||||
org.springframework.boot.context.properties.bind.Bindable
|
||||
.ofInstance(bindingServiceProperties));
|
||||
return bindingServiceProperties;
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class DefaultConsumerPropertiesTestSink {
|
||||
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class FooConfiguration {
|
||||
|
||||
@Bean("input")
|
||||
public Consumer<Message<?>> log() {
|
||||
return System.out::println;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class BarConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> myFunction() {
|
||||
return s -> s;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class FooBinder
|
||||
implements Binder<SomeBindableType, ConsumerProperties, ProducerProperties> {
|
||||
|
||||
@Override
|
||||
public Binding<SomeBindableType> bindConsumer(String name, String group,
|
||||
SomeBindableType inboundBindTarget,
|
||||
ConsumerProperties consumerProperties) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Binding<SomeBindableType> bindProducer(String name,
|
||||
SomeBindableType outboundBindTarget,
|
||||
ProducerProperties producerProperties) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class SomeBindableType {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2022-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binding;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.binder.test.OutputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
public class ExplicitBindingTests {
|
||||
|
||||
@Test
|
||||
void testExplicitBindings() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(EmptyConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.input-bindings=fooin;barin",
|
||||
"--spring.cloud.stream.output-bindings=fooout;barout")) {
|
||||
|
||||
assertThat(context.getBean("fooin-in-0", MessageChannel.class)).isNotNull();
|
||||
assertThat(context.getBean("barin-in-0", MessageChannel.class)).isNotNull();
|
||||
assertThat(context.getBean("fooout-out-0", MessageChannel.class)).isNotNull();
|
||||
assertThat(context.getBean("barout-out-0", MessageChannel.class)).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExplicitBindingsWithExistingConsumer() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(ConsumerConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.output-bindings=consume")) {
|
||||
|
||||
assertThat(context.getBean("consume-in-0", MessageChannel.class)).isNotNull();
|
||||
assertThat(context.getBean("consume-out-0", MessageChannel.class)).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Disabled
|
||||
@Test
|
||||
void testExplicitBindingsWithExistingSupplier() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(SupplierConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.input-bindings=supply",
|
||||
"--spring.cloud.stream.bindings.supply-out-0.producer.poller.fixed-delay=3000")) {
|
||||
|
||||
|
||||
assertThat(context.getBean("supply-in-0", MessageChannel.class)).isNotNull();
|
||||
assertThat(context.getBean("supply-out-0", MessageChannel.class)).isNotNull();
|
||||
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
assertThat(output.receive()).isNotNull();
|
||||
assertThat(output.receive(500)).isNull();
|
||||
assertThat(output.receive(500)).isNull();
|
||||
assertThat(output.receive(500)).isNull();
|
||||
assertThat(output.receive(500)).isNull();
|
||||
assertThat(output.receive(500)).isNull();
|
||||
assertThat(output.receive(500)).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@Configuration
|
||||
public static class EmptyConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@Configuration
|
||||
public static class ConsumerConfiguration {
|
||||
|
||||
@Bean
|
||||
public Consumer<String> consume() {
|
||||
return System.out::println;
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@Configuration
|
||||
public static class SupplierConfiguration {
|
||||
|
||||
@Bean
|
||||
public Supplier<String> supply() {
|
||||
return () -> "hello";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2022-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binding;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.integration.channel.FluxMessageChannel;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Soby Chacko
|
||||
*/
|
||||
public class FluxMessageChannelBindingTests {
|
||||
|
||||
@Test
|
||||
void testFluxMessageChannelBindingWhenReactiveOptIn() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(ReactiveFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.function.reactive.uppercase=true")) {
|
||||
assertThat(context.getBean("uppercase-in-0", MessageChannel.class)).isInstanceOf(FluxMessageChannel.class);
|
||||
assertThat(context.getBean("uppercase-out-0", MessageChannel.class)).isInstanceOf(FluxMessageChannel.class);
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@Configuration
|
||||
public static class ReactiveFunctionConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<Flux<String>, Flux<String>> uppercase() {
|
||||
return s -> s.map(String::toUpperCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2018-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.config;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesBindException;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
// see https://github.com/spring-cloud/spring-cloud-stream/issues/1573 for more details
|
||||
/**
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Soby Chacko
|
||||
*
|
||||
*/
|
||||
public class BindingHandlerAdviseTests {
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testFailureWithWrongValue() {
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> new SpringApplicationBuilder(SampleConfiguration.class).web(WebApplicationType.NONE).run("--props.value=-1",
|
||||
"--spring.jmx.enabled=false"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidatedValueValue() {
|
||||
ValidatedProps validatedProps = new SpringApplicationBuilder(SampleConfiguration.class)
|
||||
.web(WebApplicationType.NONE).run("--props.value=2", "--spring.jmx.enabled=false")
|
||||
.getBean(ValidatedProps.class);
|
||||
assertThat(validatedProps.getValue()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonValidatedConfigProperties() {
|
||||
new SpringApplicationBuilder(NonValidatedConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false");
|
||||
// simply should not fail
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void validatedConfigProperties() {
|
||||
assertThatExceptionOfType(ConfigurationPropertiesBindException.class)
|
||||
.isThrownBy(() -> new SpringApplicationBuilder(ValidatedConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false"));
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class NonValidatedConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> processor() {
|
||||
return s -> s;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConfigurationProperties
|
||||
public NonValidatedClass nonValidatedClass() {
|
||||
return new NonValidatedClass();
|
||||
}
|
||||
}
|
||||
|
||||
public static class NonValidatedClass {
|
||||
|
||||
private String id;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class ValidatedConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> processor() {
|
||||
return s -> s;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConfigurationProperties
|
||||
public ValidatedClass nonValidatedClass() {
|
||||
return new ValidatedClass();
|
||||
}
|
||||
}
|
||||
|
||||
@Validated
|
||||
public static class ValidatedClass {
|
||||
|
||||
private String id;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
@EnableConfigurationProperties(ValidatedProps.class)
|
||||
class SampleConfiguration {
|
||||
|
||||
@Bean
|
||||
public Consumer<String> sink() {
|
||||
return System.out::println;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ConfigurationProperties("props")
|
||||
@Validated
|
||||
class ValidatedProps {
|
||||
|
||||
private int value;
|
||||
|
||||
public int getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public void setValue(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2017-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.config;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.integration.annotation.ServiceActivator;
|
||||
import org.springframework.integration.channel.PublishSubscribeChannel;
|
||||
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Oleg Zhurakousky
|
||||
*
|
||||
*/
|
||||
public class BindingServiceConfigurationTests {
|
||||
|
||||
@Test
|
||||
void testErroChannelDistributesMessagesInCaseOfException() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(EmptyConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run()) {
|
||||
AtomicInteger counter = new AtomicInteger();
|
||||
PublishSubscribeChannel channel = context.getBean("errorChannel", PublishSubscribeChannel.class);
|
||||
//channel.setIgnoreFailures(true);
|
||||
channel.subscribe(m -> {
|
||||
counter.incrementAndGet();
|
||||
throw new RuntimeException("one");
|
||||
});
|
||||
channel.subscribe(m -> {
|
||||
counter.incrementAndGet();
|
||||
throw new RuntimeException("two");
|
||||
});
|
||||
channel.subscribe(m -> {
|
||||
counter.incrementAndGet();
|
||||
throw new RuntimeException("three");
|
||||
});
|
||||
channel.send(new GenericMessage<String>("foo"));
|
||||
assertThat(counter.get()).isEqualTo(3);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void valdateImportedConfiguartionHandlerPostProcessing() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(RootConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run();
|
||||
Map<String, AbstractReplyProducingMessageHandler> beansOfType = context
|
||||
.getBeansOfType(AbstractReplyProducingMessageHandler.class);
|
||||
for (AbstractReplyProducingMessageHandler handler : beansOfType.values()) {
|
||||
assertThat(handler.getNotPropagatedHeaders().contains("contentType"))
|
||||
.isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(ImportedConfiguration.class)
|
||||
public static class RootConfiguration {
|
||||
|
||||
@ServiceActivator(inputChannel = "input")
|
||||
public void rootService(String val) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
public static class ImportedConfiguration {
|
||||
|
||||
@ServiceActivator(inputChannel = "input")
|
||||
public void importedService(String val) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
public static class EmptyConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2018-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.config;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.annotation.StreamRetryTemplate;
|
||||
import org.springframework.cloud.stream.binder.AbstractBinder;
|
||||
import org.springframework.cloud.stream.binder.ConsumerProperties;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Soby Chacko
|
||||
*/
|
||||
public class RetryTemplateTests {
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Test
|
||||
void testSingleCustomRetryTemplate() throws Exception {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
SingleCustomRetryTemplateConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false");
|
||||
AbstractBinder binder = context.getBean(AbstractBinder.class);
|
||||
Field f = AbstractBinder.class.getDeclaredField("consumerBindingRetryTemplates");
|
||||
f.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, RetryTemplate> consumerBindingRetryTemplates = (Map<String, RetryTemplate>) f
|
||||
.get(binder);
|
||||
assertThat(consumerBindingRetryTemplates).hasSize(1);
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Test
|
||||
void testSpecificCustomRetryTemplate() throws Exception {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
SpecificCustomRetryTemplateConfiguration.class)
|
||||
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.bindings.processor-in-0.consumer.retry-template-name=retryTemplateTwo");
|
||||
|
||||
RetryTemplate retryTemplateTwo = context.getBean("retryTemplateTwo",
|
||||
RetryTemplate.class);
|
||||
BindingServiceProperties bindingServiceProperties = context
|
||||
.getBean(BindingServiceProperties.class);
|
||||
ConsumerProperties consumerProperties = bindingServiceProperties
|
||||
.getConsumerProperties("processor-in-0");
|
||||
AbstractBinder binder = context.getBean(AbstractBinder.class);
|
||||
|
||||
Method m = AbstractBinder.class.getDeclaredMethod("buildRetryTemplate",
|
||||
ConsumerProperties.class);
|
||||
m.setAccessible(true);
|
||||
RetryTemplate retryTemplate = (RetryTemplate) m.invoke(binder,
|
||||
consumerProperties);
|
||||
assertThat(retryTemplate).isEqualTo(retryTemplateTwo);
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class SpecificCustomRetryTemplateConfiguration {
|
||||
|
||||
@StreamRetryTemplate
|
||||
public RetryTemplate retryTemplate() {
|
||||
return new RetryTemplate();
|
||||
}
|
||||
|
||||
@StreamRetryTemplate
|
||||
public RetryTemplate retryTemplateTwo() {
|
||||
return new RetryTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RetryTemplate otherRetryTemplate() {
|
||||
return new RetryTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> processor() {
|
||||
return s -> s;
|
||||
}
|
||||
}
|
||||
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class SingleCustomRetryTemplateConfiguration {
|
||||
|
||||
@StreamRetryTemplate
|
||||
public RetryTemplate retryTemplate() {
|
||||
return new RetryTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RetryTemplate otherRetryTemplate() {
|
||||
return new RetryTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> processor() {
|
||||
return s -> s;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2018-2021 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.endpoint;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.cloud.stream.binding.BindingsLifecycleController;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ActuatorBindingsTest {
|
||||
|
||||
/*
|
||||
* Even though this test performs some simple assertions, the main purpose for it is to validate that
|
||||
* it does not result in recursive exception described in https://github.com/spring-cloud/spring-cloud-stream/issues/2253
|
||||
*/
|
||||
@Test
|
||||
void test_2253() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(Bindings.class))
|
||||
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=consume",
|
||||
"--spring.jackson.visibility.field=ANY" // see https://github.com/spring-cloud/spring-cloud-stream/issues/2253
|
||||
// we need the above just to verify that such action does not
|
||||
// interfere with instance of ObjectMapper inside of BindingsLifecycleController
|
||||
)) {
|
||||
|
||||
BindingsLifecycleController controller = context
|
||||
.getBean(BindingsLifecycleController.class);
|
||||
List<Map<?, ?>> bindings = controller.queryStates();
|
||||
assertThat(bindings.size()).isEqualTo(1);
|
||||
assertThat(bindings.get(0).get("bindingName")).isEqualTo("consume-in-0");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class Bindings {
|
||||
|
||||
@Bean
|
||||
public Consumer<String> consume() {
|
||||
return message -> System.out.println("Received message " + message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.function;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy;
|
||||
import org.springframework.cloud.stream.binder.test.InputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.OutputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.cloud.stream.config.BindingServiceProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
* @author David Turanski
|
||||
*
|
||||
*
|
||||
* TODO: Need to rewrite this test.
|
||||
*/
|
||||
public class DynamicDestinationFunctionTests {
|
||||
|
||||
@AfterAll
|
||||
public static void after() {
|
||||
System.clearProperty("spring.cloud.stream.function.definition");
|
||||
System.clearProperty("spring.cloud.function.definition");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testEmptyConfiguration() {
|
||||
TestChannelBinderConfiguration.applicationContextRunner(SampleConfiguration.class)
|
||||
.withPropertyValues(
|
||||
"spring.jmx.enabled=false",
|
||||
"spring.cloud.stream.bindings.fooDestination.producer.partitionKeyExtractorName=keyExtractor")
|
||||
.run(context -> {
|
||||
InputDestination input = context.getBean(InputDestination.class);
|
||||
input.send(new GenericMessage<String>("fooDestination"));
|
||||
|
||||
BindingServiceProperties serviceProperties = context.getBean(BindingServiceProperties.class);
|
||||
assertThat("keyExtractor").isEqualTo(
|
||||
serviceProperties.getProducerProperties("fooDestination").getPartitionKeyExtractorName());
|
||||
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
Object result = output.receive(1000).getPayload();
|
||||
assertThat(result).isEqualTo("fooDestination");
|
||||
});
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class SampleConfiguration {
|
||||
|
||||
// @Autowired
|
||||
// private BinderAwareChannelResolver resolver;
|
||||
|
||||
@Bean
|
||||
public PartitionKeyExtractorStrategy keyExtractor() {
|
||||
return new PartitionKeyExtractorStrategy() {
|
||||
|
||||
@Override
|
||||
public Object extractKey(Message<?> message) {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// @Bean
|
||||
// public Consumer<String> cons() {
|
||||
// return value -> {
|
||||
// resolver.resolveDestination(value).send(new GenericMessage<String>(value));
|
||||
// };
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.function;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.assertj.core.util.Arrays;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.cloud.stream.binder.test.InputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.OutputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.kafka.support.KafkaNull;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Gary Russel
|
||||
* @author Oleg Zhurakousky
|
||||
* @author David Turanski
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
public class FunctionBatchingTests {
|
||||
|
||||
@Test
|
||||
void testMessageBatchConfigurationWithKafkaNull() {
|
||||
TestChannelBinderConfiguration.applicationContextRunner(MessageBatchConfiguration.class)
|
||||
.withPropertyValues("spring.jmx.enabled=false",
|
||||
"spring.cloud.stream.function.definition=func",
|
||||
"spring.cloud.stream.bindings.input.consumer.batch-mode=true")
|
||||
.run(context -> {
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context
|
||||
.getBean(OutputDestination.class);
|
||||
|
||||
List<Object> list = new ArrayList<>();
|
||||
list.add("{\"name\":\"bob\"}".getBytes());
|
||||
list.add("{\"name\":\"jill\"}".getBytes());
|
||||
list.add(KafkaNull.INSTANCE);
|
||||
list.add("{\"name\":\"steve\"}".getBytes());
|
||||
Message<List<Object>> inputMessage = MessageBuilder
|
||||
.withPayload(list)
|
||||
.build();
|
||||
inputDestination.send(inputMessage);
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(outputMessage).isNotNull();
|
||||
assertThat(outputMessage.getPayload())
|
||||
.isEqualTo("{\"name\":\"bob\"}".getBytes());
|
||||
|
||||
context.stop();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListPayloadConfiguration() {
|
||||
TestChannelBinderConfiguration.applicationContextRunner(ListPayloadNotBatchConfiguration.class)
|
||||
.withPropertyValues("spring.jmx.enabled=false",
|
||||
"spring.cloud.stream.function.definition=func")
|
||||
.run(context -> {
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context
|
||||
.getBean(OutputDestination.class);
|
||||
|
||||
Message<byte[]> inputMessage = MessageBuilder
|
||||
.withPayload("[{\"name\":\"bob\"},{\"name\":\"jill\"}]".getBytes())
|
||||
.build();
|
||||
inputDestination.send(inputMessage);
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(outputMessage).isNotNull();
|
||||
assertThat(outputMessage.getPayload())
|
||||
.isEqualTo("{\"name\":\"bob\"}".getBytes());
|
||||
|
||||
context.stop();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListStringPayloadConfigurationTextPlain() {
|
||||
TestChannelBinderConfiguration.applicationContextRunner(ListStringPayloadConfiguration.class)
|
||||
.withPropertyValues("spring.jmx.enabled=false",
|
||||
"spring.cloud.stream.function.definition=func",
|
||||
"spring.cloud.stream.bindings.func-in-0.content-type=text/plain")
|
||||
.run(context -> {
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context
|
||||
.getBean(OutputDestination.class);
|
||||
|
||||
List bytes = Arrays.asList(new Object[] {"abc".getBytes(), "xyz".getBytes()});
|
||||
Message inputMessage = MessageBuilder.withPayload(bytes).build();
|
||||
inputDestination.send(inputMessage);
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(new String(outputMessage.getPayload())).isEqualTo("[abc, xyz]");
|
||||
context.stop();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListObjectPayloadObjectConfigurationTextPlain() {
|
||||
TestChannelBinderConfiguration.applicationContextRunner(ListObjectPayloadConfiguration.class)
|
||||
.withPropertyValues("spring.jmx.enabled=false",
|
||||
"spring.cloud.stream.function.definition=func",
|
||||
"spring.cloud.stream.bindings.func-in-0.content-type=text/plain")
|
||||
.run(context -> {
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context
|
||||
.getBean(OutputDestination.class);
|
||||
|
||||
List bytes = Arrays.asList(new Object[] {"abc".getBytes(), "xyz".getBytes()});
|
||||
Message inputMessage = MessageBuilder.withPayload(bytes).build();
|
||||
inputDestination.send(inputMessage);
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(new String(outputMessage.getPayload())).isEqualTo("[abc, xyz]");
|
||||
context.stop();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSimpleBatchConfiguration() {
|
||||
TestChannelBinderConfiguration.applicationContextRunner(SimpleBatchConfiguration.class)
|
||||
.withPropertyValues(
|
||||
"spring.jmx.enabled=false",
|
||||
"spring.cloud.stream.function.definition=func",
|
||||
"spring.cloud.stream.bindings.input.consumer.batch-mode=true")
|
||||
.run(context -> {
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context
|
||||
.getBean(OutputDestination.class);
|
||||
|
||||
List<byte[]> list = new ArrayList<>();
|
||||
list.add("{\"name\":\"bob\"}".getBytes());
|
||||
list.add("{\"name\":\"jill\"}".getBytes());
|
||||
Message<List<byte[]>> inputMessage = MessageBuilder
|
||||
.withPayload(list)
|
||||
.build();
|
||||
inputDestination.send(inputMessage);
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(outputMessage).isNotNull();
|
||||
assertThat(outputMessage.getPayload())
|
||||
.isEqualTo("{\"name\":\"bob\"}".getBytes());
|
||||
context.stop();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNestedBatchConfiguration() {
|
||||
TestChannelBinderConfiguration.applicationContextRunner(NestedBatchConfiguration.class)
|
||||
.withPropertyValues("spring.jmx.enabled=false",
|
||||
"spring.cloud.stream.function.definition=func",
|
||||
"spring.cloud.stream.bindings.input.consumer.batch-mode=true")
|
||||
.run(context -> {
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context
|
||||
.getBean(OutputDestination.class);
|
||||
|
||||
List<byte[]> list = new ArrayList<>();
|
||||
list.add("[{\"name\":\"bob\"},{\"name\":\"jill\"}]".getBytes());
|
||||
Message<List<byte[]>> inputMessage = MessageBuilder
|
||||
.withPayload(list)
|
||||
.build();
|
||||
inputDestination.send(inputMessage);
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(outputMessage).isNotNull();
|
||||
assertThat(outputMessage.getPayload())
|
||||
.isEqualTo("{\"name\":\"bob\"}".getBytes());
|
||||
context.stop();
|
||||
});
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class SimpleBatchConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<List<Person>, Person> func() {
|
||||
return x -> x.get(0);
|
||||
}
|
||||
|
||||
public static class Person {
|
||||
|
||||
private String name;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class ListStringPayloadConfiguration {
|
||||
@Bean
|
||||
public Function<List<String>, String> func() {
|
||||
return x -> x.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class ListObjectPayloadConfiguration {
|
||||
@Bean
|
||||
public Function<List<Object>, String> func() {
|
||||
return x -> x.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class ListPayloadNotBatchConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<List<Person>, Person> func() {
|
||||
return x -> x.get(0);
|
||||
}
|
||||
|
||||
public static class Person {
|
||||
|
||||
private String name;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class NestedBatchConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<List<List<Person>>, Person> func() {
|
||||
return x -> x.get(0).get(0);
|
||||
}
|
||||
|
||||
public static class Person {
|
||||
|
||||
private String name;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class MessageBatchConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<Message<List<Person>>, Person> func() {
|
||||
return x -> {
|
||||
Object o = x.getPayload().get(2);
|
||||
assertThat(o).isNull();
|
||||
return (Person) x.getPayload().get(0);
|
||||
};
|
||||
}
|
||||
|
||||
public static class Person {
|
||||
|
||||
private String name;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2018-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.function;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.binder.test.InputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.OutputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.integration.channel.QueueChannel;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* This test validates proper function binding for applications where EnableBinding is
|
||||
* declared.
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Artem Bilan
|
||||
*/
|
||||
public class GreenfieldFunctionEnableBindingTests {
|
||||
|
||||
@Test
|
||||
void testProcessorFromFunction() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
ProcessorFromFunction.class)).web(WebApplicationType.NONE).run(
|
||||
"--spring.cloud.function.definition=toUpperCase",
|
||||
"--spring.jmx.enabled=false")) {
|
||||
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
source.send(new GenericMessage<byte[]>("John Doe".getBytes()));
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
assertThat(target.receive(10000).getPayload())
|
||||
.isEqualTo("JOHN DOE".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSinkFromConsumer() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(SinkFromConsumer.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.cloud.function.definition=sink",
|
||||
"--spring.jmx.enabled=false")) {
|
||||
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
PollableChannel result = context.getBean("result", PollableChannel.class);
|
||||
source.send(new GenericMessage<byte[]>("John Doe".getBytes()));
|
||||
assertThat(result.receive(10000).getPayload()).isEqualTo("John Doe");
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class SourceFromSupplier {
|
||||
|
||||
@Bean
|
||||
public Supplier<Date> date() {
|
||||
return () -> new Date(12345L);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class ProcessorFromFunction {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> toUpperCase() {
|
||||
return String::toUpperCase;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class SinkFromConsumer {
|
||||
|
||||
@Bean
|
||||
public PollableChannel result() {
|
||||
return new QueueChannel();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Consumer<String> sink(PollableChannel result) {
|
||||
return s -> {
|
||||
result.send(new GenericMessage<String>(s));
|
||||
System.out.println(s);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class Foo {
|
||||
|
||||
String bar;
|
||||
|
||||
public String getBar() {
|
||||
return this.bar;
|
||||
}
|
||||
|
||||
public void setBar(String bar) {
|
||||
this.bar = bar;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,488 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.function;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.UnicastProcessor;
|
||||
import reactor.util.function.Tuple2;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.binder.test.InputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.OutputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.converter.AbstractMessageConverter;
|
||||
import org.springframework.messaging.converter.MessageConverter;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
*
|
||||
*/
|
||||
public class MultipleInputOutputFunctionTests {
|
||||
|
||||
@Test
|
||||
void testFailureWithNonReactiveFunction() {
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
ReactiveFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=multipleInputNonReactive"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailureWithReactiveArrayOutput() {
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
ReactiveFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=multiReactiveInputReactiveArrayOutput"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailureWithReactiveArrayOutputNonGeneric() {
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
ReactiveFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=multiReactiveInputReactiveArrayOutputNoGeneric"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailureWithReactiveArrayInput() {
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
ReactiveFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=genericReactiveArrayInput"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailureWithReactiveArrayInputNonGeneric() {
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
ReactiveFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=nonGenericReactiveArrayInput"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailureWithConsumer() {
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
ReactiveFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=multiInputConsumer"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultiInputSingleOutput() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
ReactiveFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=multiInputSingleOutput")) {
|
||||
context.getBean(InputDestination.class);
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
|
||||
Message<byte[]> stringInputMessage = MessageBuilder.withPayload("one".getBytes()).build();
|
||||
Message<byte[]> integerInputMessage = MessageBuilder.withPayload("1".getBytes()).build();
|
||||
inputDestination.send(stringInputMessage, "multiInputSingleOutput-in-0");
|
||||
inputDestination.send(integerInputMessage, "multiInputSingleOutput-in-1");
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("one".getBytes());
|
||||
outputMessage = outputDestination.receive(0, "multiInputSingleOutput-out-0");
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("1".getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultiInputMessageSingleOutput() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
ReactiveFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=multiInputSingleOutputMessage")) {
|
||||
context.getBean(InputDestination.class);
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
|
||||
Message<byte[]> stringInputMessage = MessageBuilder.withPayload("one".getBytes()).build();
|
||||
Message<byte[]> integerInputMessage = MessageBuilder.withPayload("1".getBytes()).build();
|
||||
inputDestination.send(stringInputMessage, "multiInputSingleOutputMessage-in-0");
|
||||
inputDestination.send(integerInputMessage, "multiInputSingleOutputMessage-in-1");
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("one".getBytes());
|
||||
outputMessage = outputDestination.receive(0, "multiInputSingleOutputMessage-out-0");
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("1".getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingleInputMultiOutput() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
ReactiveFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=singleInputMultipleOutputs")) {
|
||||
context.getBean(InputDestination.class);
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
inputDestination.send(MessageBuilder.withPayload(String.valueOf(i).getBytes()).build(), "singleInputMultipleOutputs-in-0");
|
||||
}
|
||||
|
||||
int counter = 0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
Message<byte[]> even = outputDestination.receive(0, "singleInputMultipleOutputs-out-0");
|
||||
assertThat(even.getPayload()).isEqualTo(("EVEN: " + String.valueOf(counter++)).getBytes());
|
||||
Message<byte[]> odd = outputDestination.receive(0, "singleInputMultipleOutputs-out-1");
|
||||
assertThat(odd.getPayload()).isEqualTo(("ODD: " + String.valueOf(counter++)).getBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleFunctions() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
ReactiveFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=uppercase;reverse")) {
|
||||
context.getBean(InputDestination.class);
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
|
||||
Message<byte[]> inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build();
|
||||
inputDestination.send(inputMessage, "uppercase-in-0");
|
||||
inputDestination.send(inputMessage, "reverse-in-0");
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive(0, "uppercase-out-0");
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("HELLO".getBytes());
|
||||
|
||||
outputMessage = outputDestination.receive(0, "reverse-out-0");
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("olleH".getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleFunctionsWithComposition() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
ReactiveFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=uppercase|reverse;reverse|uppercase")) {
|
||||
context.getBean(InputDestination.class);
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
|
||||
Message<byte[]> inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build();
|
||||
inputDestination.send(inputMessage, "uppercasereverse-in-0");
|
||||
inputDestination.send(inputMessage, "reverseuppercase-in-0");
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive(0, "uppercasereverse-out-0");
|
||||
System.out.println(new String(outputMessage.getPayload()));
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("OLLEH".getBytes());
|
||||
|
||||
outputMessage = outputDestination.receive(0, "reverseuppercase-out-0");
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("OLLEH".getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultiInputSingleOutputWithCustomContentType() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
ContentTypeConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=multiInputSingleOutput",
|
||||
"--spring.cloud.stream.bindings.multiInputSingleOutput-in-0.content-type=string/person",
|
||||
"--spring.cloud.stream.bindings.multiInputSingleOutput-in-1.content-type=string/employee")) {
|
||||
context.getBean(InputDestination.class);
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
|
||||
Message<byte[]> stringInputMessage = MessageBuilder.withPayload("ricky".getBytes()).build();
|
||||
Message<byte[]> integerInputMessage = MessageBuilder.withPayload("bobby".getBytes()).build();
|
||||
inputDestination.send(stringInputMessage, "multiInputSingleOutput-in-0");
|
||||
inputDestination.send(integerInputMessage, "multiInputSingleOutput-in-1");
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive(1000, "multiInputSingleOutput-out-0");
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("RICKY".getBytes());
|
||||
outputMessage = outputDestination.receive(1000, "multiInputSingleOutput-out-0");
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("BOBBY".getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultiInputSingleOutputWithCustomContentType2() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
ContentTypeConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=multiInputSingleOutput2",
|
||||
"--spring.cloud.stream.bindings.multiInputSingleOutput2-in-0.content-type=string/person",
|
||||
"--spring.cloud.stream.bindings.multiInputSingleOutput2-in-1.content-type=string/employee",
|
||||
"--spring.cloud.stream.bindings.multiInputSingleOutput2-out-0.content-type=string/person")) {
|
||||
context.getBean(InputDestination.class);
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
|
||||
Message<byte[]> stringInputMessage = MessageBuilder.withPayload("ricky".getBytes()).build();
|
||||
Message<byte[]> integerInputMessage = MessageBuilder.withPayload("bobby".getBytes()).build();
|
||||
inputDestination.send(stringInputMessage, "multiInputSingleOutput2-in-0");
|
||||
inputDestination.send(integerInputMessage, "multiInputSingleOutput2-in-1");
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive(1000, "multiInputSingleOutput2-out-0");
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("rickybobby".getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class ReactiveFunctionConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return value -> value.toUpperCase();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> reverse() {
|
||||
return value -> new StringBuilder(value).reverse().toString();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Tuple2<String, String>, String> multipleInputNonReactive() { // not supported
|
||||
return tuple -> null;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Tuple2<Flux<String>, Flux<Integer>>, Flux<?>[]> multiReactiveInputReactiveArrayOutput() { // not supported
|
||||
return tuple -> null;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Consumer<Tuple2<Flux<String>, Flux<Integer>>> multiInputConsumer() { // not supported
|
||||
return tuple -> System.out.println();
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Bean
|
||||
public Function<Tuple2<Flux<String>, Flux<Integer>>, Flux[]> multiReactiveInputReactiveArrayOutputNoGeneric() { // not supported
|
||||
return tuple -> null;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Flux<?>[], Tuple2<Flux<String>, Flux<Integer>>> genericReactiveArrayInput() { // not supported
|
||||
return tuple -> null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Bean
|
||||
public Function<Flux[], Tuple2<Flux<String>, Flux<Integer>>> nonGenericReactiveArrayInput() { // not supported
|
||||
return tuple -> null;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Tuple2<Flux<String>, Flux<Integer>>, Flux<String>> multiInputSingleOutput() {
|
||||
return tuple -> {
|
||||
Flux<String> stringStream = tuple.getT1();
|
||||
Flux<String> intStream = tuple.getT2().map(i -> String.valueOf(i));
|
||||
return Flux.merge(stringStream, intStream);
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Tuple2<Flux<Message<String>>, Flux<Message<Integer>>>, Flux<String>> multiInputSingleOutputMessage() {
|
||||
return tuple -> {
|
||||
Flux<String> stringStream = tuple.getT1().map(m -> m.getPayload());
|
||||
Flux<String> intStream = tuple.getT2().map(i -> {
|
||||
int v = i.getPayload();
|
||||
return String.valueOf(v);
|
||||
});
|
||||
return Flux.merge(stringStream, intStream);
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public static Function<Flux<Integer>, Tuple2<Flux<String>, Flux<String>>> singleInputMultipleOutputs() {
|
||||
return flux -> {
|
||||
Flux<Integer> connectedFlux = flux.publish().autoConnect(2);
|
||||
UnicastProcessor even = UnicastProcessor.create();
|
||||
UnicastProcessor odd = UnicastProcessor.create();
|
||||
Flux<Integer> evenFlux = connectedFlux.filter(number -> number % 2 == 0).doOnNext(number -> even.onNext("EVEN: " + number));
|
||||
Flux<Integer> oddFlux = connectedFlux.filter(number -> number % 2 != 0).doOnNext(number -> odd.onNext("ODD: " + number));
|
||||
|
||||
return Tuples.of(Flux.from(even).doOnSubscribe(x -> evenFlux.subscribe()), Flux.from(odd).doOnSubscribe(x -> oddFlux.subscribe()));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class ContentTypeConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<Tuple2<Flux<Person>, Flux<Employee>>, Flux<String>> multiInputSingleOutput() {
|
||||
return tuple -> {
|
||||
Flux<String> stringStream = tuple.getT1().map(p -> p.getName().toUpperCase());
|
||||
Flux<String> intStream = tuple.getT2().map(p -> p.getName().toUpperCase());
|
||||
return Flux.merge(stringStream, intStream);
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Tuple2<Flux<Person>, Flux<Employee>>, Flux<Person>> multiInputSingleOutput2() {
|
||||
return tuple -> {
|
||||
return Flux.merge(tuple.getT1(), tuple.getT2()).buffer(Duration.ofMillis(1000)).map(list -> {
|
||||
String personName = ((Person) list.get(0)).getName();
|
||||
String employeeName = ((Employee) list.get(1)).getName();
|
||||
return new Person(personName + employeeName);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MessageConverter stringToPersonConverter() {
|
||||
return new AbstractMessageConverter(MimeType.valueOf("string/person")) {
|
||||
|
||||
@Override
|
||||
protected boolean supports(Class<?> clazz) {
|
||||
return Person.class.isAssignableFrom(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Object convertFromInternal(
|
||||
Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) {
|
||||
String name = new String(((byte[]) message.getPayload()), StandardCharsets.UTF_8);
|
||||
Person person = new Person(name);
|
||||
return person;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Object convertToInternal(
|
||||
Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) {
|
||||
|
||||
return ((Person) payload).getName().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MessageConverter stringToEmployeeConverter() {
|
||||
return new AbstractMessageConverter(MimeType.valueOf("string/employee")) {
|
||||
|
||||
@Override
|
||||
protected boolean supports(Class<?> clazz) {
|
||||
return Employee.class.isAssignableFrom(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Object convertFromInternal(
|
||||
Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) {
|
||||
String name = new String(((byte[]) message.getPayload()), StandardCharsets.UTF_8);
|
||||
Employee person = new Employee(name);
|
||||
return person;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Object convertToInternal(
|
||||
Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) {
|
||||
|
||||
return ((Employee) payload).getName().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static class Person {
|
||||
private final String name;
|
||||
|
||||
Person(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Employee {
|
||||
private final String name;
|
||||
|
||||
Employee(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2020-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.function;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException;
|
||||
import org.springframework.cloud.stream.binder.DefaultPollableMessageSource;
|
||||
import org.springframework.cloud.stream.binder.PollableMessageSource;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.integration.scheduling.PollerMetadata;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
import org.springframework.scheduling.support.CronTrigger;
|
||||
import org.springframework.scheduling.support.PeriodicTrigger;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Artem Bilan
|
||||
*
|
||||
*/
|
||||
public class PollableSourceTests {
|
||||
|
||||
@Test
|
||||
void testPollableSource() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(PollableAppSampleConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run(
|
||||
"--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.pollable-source=blah",
|
||||
"--spring.integration.poller.cron=*/2 * * * * *",
|
||||
"--spring.cloud.stream.poller.max-messages-per-poll=4")) {
|
||||
|
||||
|
||||
DefaultPollableMessageSource pollableSource = (DefaultPollableMessageSource) context.getBean(PollableMessageSource.class);
|
||||
pollableSource.poll(message -> assertThat(message.getPayload()).isNotNull());
|
||||
|
||||
PollerMetadata pollerMetadata = context.getBean(PollerMetadata.class);
|
||||
assertThat(pollerMetadata.getTrigger()).isInstanceOf(CronTrigger.class);
|
||||
assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "expression.expression"))
|
||||
.isEqualTo("*/2 * * * * *");
|
||||
assertThat(pollerMetadata.getMaxMessagesPerPoll()).isEqualTo(4);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPollerDefaultFixedDelay() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(PollableAppSampleConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run()) {
|
||||
|
||||
PollerMetadata pollerMetadata = context.getBean(PollerMetadata.class);
|
||||
assertThat(pollerMetadata.getTrigger()).isInstanceOf(PeriodicTrigger.class);
|
||||
assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "fixedRate")).isEqualTo(false);
|
||||
assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "period")).isEqualTo(Duration.ofSeconds(1));
|
||||
assertThat(pollerMetadata.getMaxMessagesPerPoll()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPollerProvidedFixedDelayAndMaxMessagesPerPoll() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(PollableAppSampleConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run(
|
||||
"--spring.integration.poller.fixed-delay=7s",
|
||||
"--spring.integration.poller.max-messages-per-poll=13")) {
|
||||
|
||||
PollerMetadata pollerMetadata = context.getBean(PollerMetadata.class);
|
||||
assertThat(pollerMetadata.getTrigger()).isInstanceOf(PeriodicTrigger.class);
|
||||
assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "fixedRate")).isEqualTo(false);
|
||||
assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "period")).isEqualTo(Duration.ofSeconds(7));
|
||||
assertThat(pollerMetadata.getMaxMessagesPerPoll()).isEqualTo(13);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoPollerFixedDelayIfFixedRatePresent() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(PollableAppSampleConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run("--spring.integration.poller.fixed-rate=200")) {
|
||||
|
||||
PollerMetadata pollerMetadata = context.getBean(PollerMetadata.class);
|
||||
assertThat(pollerMetadata.getTrigger()).isInstanceOf(PeriodicTrigger.class);
|
||||
assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "fixedRate")).isEqualTo(true);
|
||||
assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "period")).isEqualTo(Duration.ofMillis(200));
|
||||
assertThat(pollerMetadata.getMaxMessagesPerPoll()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPollerMutualProperties() {
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(PollableAppSampleConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run(
|
||||
"--spring.integration.poller.fixed-rate=200",
|
||||
"--spring.cloud.stream.poller.fixed-delay=300"))
|
||||
.withRootCauseExactlyInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class)
|
||||
.withMessageContaining("are mutually exclusive");
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@Configuration
|
||||
public static class PollableAppSampleConfiguration {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.function;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.function.context.FunctionProperties;
|
||||
import org.springframework.cloud.function.context.config.RoutingFunction;
|
||||
import org.springframework.cloud.stream.binder.test.InputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.OutputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinder;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.integration.channel.AbstractSubscribableChannel;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
* @since 2.2.1
|
||||
*/
|
||||
public class RoutingFunctionTests {
|
||||
|
||||
|
||||
@BeforeAll
|
||||
public static void before() {
|
||||
System.getProperties().remove("spring.cloud.function.routing.enabled");
|
||||
System.getProperties().remove("spring.cloud.stream.function.definition");
|
||||
System.getProperties().remove("spring.cloud.function.definition");
|
||||
System.getProperties().remove("spring.cloud.function.routing-expression");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoutingViaExplicitEnablingAndDefinitionHeader() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
RoutingFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.function.routing.enabled=true")) {
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context
|
||||
.getBean(OutputDestination.class);
|
||||
|
||||
Message<byte[]> inputMessage = MessageBuilder
|
||||
.withPayload("Hello".getBytes())
|
||||
.setHeader(FunctionProperties.PREFIX + ".definition", "echo")
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN)
|
||||
.build();
|
||||
inputDestination.send(inputMessage);
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("Hello".getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoutingViaExplicitEnablingAndRoutingExpressionProperty() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
RoutingFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.routing-expression=headers.contentType.toString().equals('text/plain') ? 'echo' : null",
|
||||
"--spring.cloud.stream.function.routing.enabled=true")) {
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context
|
||||
.getBean(OutputDestination.class);
|
||||
|
||||
Message<byte[]> inputMessage = MessageBuilder
|
||||
.withPayload("Hello".getBytes())
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN)
|
||||
.build();
|
||||
inputDestination.send(inputMessage);
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("Hello".getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoutingViaExplicitEnablingAndRoutingExpressionHeader() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
RoutingFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.function.routing.enabled=true")) {
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context
|
||||
.getBean(OutputDestination.class);
|
||||
|
||||
Message<byte[]> inputMessage = MessageBuilder
|
||||
.withPayload("Hello".getBytes())
|
||||
.setHeader("spring.cloud.function.routing-expression", "'echo'")
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN)
|
||||
.build();
|
||||
inputDestination.send(inputMessage);
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("Hello".getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoutingViaExplicitDefinitionAndDefinitionHeader() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
RoutingFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=" + RoutingFunction.FUNCTION_NAME)) {
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context
|
||||
.getBean(OutputDestination.class);
|
||||
|
||||
Message<byte[]> inputMessage = MessageBuilder
|
||||
.withPayload("Hello".getBytes())
|
||||
.setHeader("spring.cloud.function.definition", "echo|uppercase")
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN)
|
||||
.build();
|
||||
inputDestination.send(inputMessage);
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("HELLO".getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultRoutingFunctionBindingFlux() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
RoutingFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.function.routing.enabled=true")) {
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
|
||||
Message<byte[]> inputMessage = MessageBuilder
|
||||
.withPayload("Hello".getBytes())
|
||||
.setHeader("spring.cloud.function.definition", "echoFlux")
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN)
|
||||
.build();
|
||||
inputDestination.send(inputMessage);
|
||||
|
||||
TestChannelBinder binder = context.getBean(TestChannelBinder.class);
|
||||
Throwable ex = ((Exception) binder.getLastError().getPayload()).getCause();
|
||||
assertThat(ex).isInstanceOf(IllegalStateException.class);
|
||||
assertThat(ex.getMessage()).isEqualTo("Routing to functions that return Publisher is not supported in the context of Spring Cloud Stream.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testPojoFunction() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
RoutingFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.function.definition=" + RoutingFunction.FUNCTION_NAME)) {
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context
|
||||
.getBean(OutputDestination.class);
|
||||
|
||||
Message<byte[]> inputMessage = MessageBuilder
|
||||
.withPayload("{\"name\":\"bob\"}".getBytes())
|
||||
.setHeader("spring.cloud.function.definition", "pojoecho")
|
||||
.build();
|
||||
inputDestination.send(inputMessage);
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("{\"name\":\"bob\"}".getBytes());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExplicitRoutingFunctionBindingWithCompositionAndRoutingEnabledExplicitly() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
RoutingFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.function.definition=enrich|" + RoutingFunction.FUNCTION_NAME,
|
||||
"--spring.cloud.stream.function.routing.enabled=true")) {
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context
|
||||
.getBean(OutputDestination.class);
|
||||
|
||||
Message<byte[]> inputMessage = MessageBuilder
|
||||
.withPayload("Hello".getBytes()).build();
|
||||
inputDestination.send(inputMessage);
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("HELLO".getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExplicitRoutingFunctionBindingWithCompositionAndRoutingEnabledImplicitly() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
RoutingFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.function.definition=enrich|" + RoutingFunction.FUNCTION_NAME)) {
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context
|
||||
.getBean(OutputDestination.class);
|
||||
|
||||
Message<byte[]> inputMessage = MessageBuilder
|
||||
.withPayload("Hello".getBytes()).build();
|
||||
inputDestination.send(inputMessage);
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("HELLO".getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExplicitRoutingFunctionBindingWithCompositionAndRoutingEnabledExplicitlyAndMoreComposition() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
RoutingFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.function.definition=enrich|" + RoutingFunction.FUNCTION_NAME + "|reverse",
|
||||
"--spring.cloud.stream.function.routing.enabled=true")) {
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context
|
||||
.getBean(OutputDestination.class);
|
||||
|
||||
Message<byte[]> inputMessage = MessageBuilder
|
||||
.withPayload("Hello".getBytes())
|
||||
.build();
|
||||
inputDestination.send(inputMessage);
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("OLLEH".getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExplicitRoutingFunctionBindingWithCompositionAndRoutingEnabledImplicitlyAndMoreComposition() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
RoutingFunctionConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.function.definition=enrich|" + RoutingFunction.FUNCTION_NAME + "|reverse")) {
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
OutputDestination outputDestination = context
|
||||
.getBean(OutputDestination.class);
|
||||
|
||||
Message<byte[]> inputMessage = MessageBuilder
|
||||
.withPayload("Hello".getBytes()).build();
|
||||
inputDestination.send(inputMessage);
|
||||
|
||||
Message<byte[]> outputMessage = outputDestination.receive();
|
||||
assertThat(outputMessage.getPayload()).isEqualTo("OLLEH".getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void testRoutingToConsumers() throws Exception {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
RoutingConsumerConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.routing-expression=headers['func_name']")) {
|
||||
|
||||
InputDestination inputDestination = context.getBean(InputDestination.class);
|
||||
Message<byte[]> inputMessage = MessageBuilder
|
||||
.withPayload("foo".getBytes())
|
||||
.setHeader("func_name", "consume")
|
||||
.build();
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
Field chField = ReflectionUtils.findField(outputDestination.getClass(), "channels");
|
||||
chField.setAccessible(true);
|
||||
List<AbstractSubscribableChannel> outputChannels = (List<AbstractSubscribableChannel>) chField.get(outputDestination);
|
||||
assertThat(outputChannels.isEmpty());
|
||||
inputDestination.send(inputMessage);
|
||||
assertThat(outputChannels.isEmpty());
|
||||
inputMessage = MessageBuilder
|
||||
.withPayload("foo".getBytes())
|
||||
.setHeader("func_name", "echo")
|
||||
.build();
|
||||
inputDestination.send(inputMessage);
|
||||
assertThat(outputChannels.size()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class RoutingConsumerConfiguration {
|
||||
@Bean
|
||||
public Consumer<String> consume() {
|
||||
return System.out::println;
|
||||
}
|
||||
@Bean
|
||||
public Function<String, String> echo() {
|
||||
return x -> x;
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class RoutingFunctionConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> echo() {
|
||||
return x -> {
|
||||
System.out.println("===> echo");
|
||||
return x;
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Person, Person> pojoecho() {
|
||||
return x -> {
|
||||
System.out.println("===> pojoecho");
|
||||
return x;
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Flux<String>, Flux<String>> echoFlux() {
|
||||
return flux -> flux.map(x -> {
|
||||
System.out.println("===> echoFlux");
|
||||
return x;
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Message<String>, Message<String>> enrich() {
|
||||
return x -> {
|
||||
System.out.println("===> enrich");
|
||||
return MessageBuilder.withPayload(x.getPayload()).setHeader("spring.cloud.function.definition", "uppercase").build();
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return x -> {
|
||||
System.out.println("===> uppercase");
|
||||
return x.toUpperCase();
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> reverse() {
|
||||
return x -> {
|
||||
System.out.println("===> reverse");
|
||||
return new StringBuilder(x).reverse().toString();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class Person {
|
||||
private String name;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Copyright 2020-2020 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.function;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.binder.test.InputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.OutputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
*
|
||||
*/
|
||||
public class ScenarioTests {
|
||||
|
||||
@Test
|
||||
void test2106() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(ConsumerConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run(
|
||||
"--spring.cloud.function.definition=consume;echo",
|
||||
"--spring.cloud.stream.bindings.consume-in-0.destination=input",
|
||||
"--spring.cloud.stream.bindings.echo-in-0.destination=echoin",
|
||||
"--spring.cloud.stream.bindings.echo-out-0.destination=echoout",
|
||||
"--spring.jmx.enabled=false")) {
|
||||
|
||||
ConsumerConfiguration configuration = context.getBean(ConsumerConfiguration.class);
|
||||
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
bridge.send("input", "destination");
|
||||
bridge.send("input", "destination");
|
||||
bridge.send("input", "destination");
|
||||
|
||||
bridge.send("consume-in-0", "hello");
|
||||
bridge.send("consume-in-0", "hello");
|
||||
bridge.send("consume-in-0", "hello");
|
||||
|
||||
bridge.send("echoin", "hello");
|
||||
bridge.send("echoin", "hello");
|
||||
bridge.send("echoin", "hello");
|
||||
|
||||
assertThat(configuration.destinationCounter).isEqualTo(3);
|
||||
assertThat(configuration.bindingCounter).isEqualTo(3);
|
||||
|
||||
assertThat(output.receive(1000, "echoout")).isNotNull();
|
||||
assertThat(output.receive(1000, "echoout")).isNotNull();
|
||||
assertThat(output.receive(1000, "echoout")).isNotNull();
|
||||
assertThat(output.receive(1000, "echoout")).isNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testComposingSupplierWuthTypelessMessageFunction() throws Exception {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(SupplierConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=messageSupplier|messageFunction")) {
|
||||
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
assertThat(output.receive(1000, "messageSuppliermessageFunction-out-0")).isNotNull();
|
||||
assertThat(output.receive(1200, "messageSuppliermessageFunction-out-0")).isNotNull();
|
||||
assertThat(output.receive(1300, "messageSuppliermessageFunction-out-0")).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void test2107() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(FunctionReturningNullConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=uppercase")) {
|
||||
|
||||
InputDestination input = context.getBean(InputDestination.class);
|
||||
input.send(new GenericMessage<byte[]>("a".getBytes()), "uppercase-in-0");
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
assertThat(new String(output.receive(2000, "uppercase-out-0").getPayload())).isEqualTo("a");
|
||||
input.send(new GenericMessage<byte[]>("b".getBytes()), "uppercase-in-0");
|
||||
assertThat(output.receive(2000, "uppercase-out-0")).isNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void test2113() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(TestConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=genericTypeFunction")) {
|
||||
|
||||
InputDestination input = context.getBean(InputDestination.class);
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
|
||||
input.send(new GenericMessage<byte[]>("hello".getBytes()), "genericTypeFunction-in-0");
|
||||
assertThat(new String(output.receive(1000, "genericTypeFunction-out-0").getPayload())).isEqualTo("hello_hello");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@Configuration
|
||||
public static class TestConfiguration {
|
||||
@SuppressWarnings("unchecked")
|
||||
@Bean
|
||||
public <I, O> Function<I, O> genericTypeFunction() {
|
||||
return v -> {
|
||||
return (O) ("hello_" + new String((byte[]) v));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@Configuration
|
||||
public static class SupplierConfiguration {
|
||||
@Bean
|
||||
public Supplier<Message<?>> messageSupplier() {
|
||||
return () -> new GenericMessage<>("10/27/20 07:20:01");
|
||||
}
|
||||
@Bean
|
||||
public Function<Message<?>, Message<?>> messageFunction() {
|
||||
return message -> {
|
||||
return message;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@Configuration
|
||||
public static class FunctionReturningNullConfiguration {
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return v -> {
|
||||
if ("a".equals(v)) {
|
||||
return v;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class ConsumerConfiguration {
|
||||
|
||||
private int destinationCounter;
|
||||
|
||||
private int bindingCounter;
|
||||
|
||||
@Bean
|
||||
public Consumer<String> consume() {
|
||||
return v -> {
|
||||
if (v.equals("destination")) {
|
||||
destinationCounter++;
|
||||
}
|
||||
else {
|
||||
bindingCounter++;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> echo() {
|
||||
return v -> v;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,810 @@
|
||||
/*
|
||||
* Copyright 2020-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.function;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.function.cloudevent.CloudEventMessageBuilder;
|
||||
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
|
||||
import org.springframework.cloud.function.context.message.MessageUtils;
|
||||
import org.springframework.cloud.stream.binder.test.InputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.OutputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.cloud.stream.binding.BindingService;
|
||||
import org.springframework.cloud.stream.binding.NewDestinationBindingCallback;
|
||||
import org.springframework.cloud.stream.config.BindingServiceProperties;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.integration.channel.AbstractMessageChannel;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.config.GlobalChannelInterceptor;
|
||||
import org.springframework.integration.dsl.IntegrationFlow;
|
||||
import org.springframework.integration.dsl.IntegrationFlows;
|
||||
import org.springframework.integration.handler.LoggingHandler;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.converter.AbstractMessageConverter;
|
||||
import org.springframework.messaging.converter.MessageConverter;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Soby Chacko
|
||||
*
|
||||
*/
|
||||
public class StreamBridgeTests {
|
||||
|
||||
@BeforeAll
|
||||
public static void before() {
|
||||
System.clearProperty("spring.cloud.function.definition");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_SCF_856() throws Exception {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(EmptyConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) {
|
||||
StreamBridge streamBridge = context.getBean(StreamBridge.class);
|
||||
streamBridge.send("myBinding-out-0",
|
||||
CloudEventMessageBuilder.withData("hello").setSource("my-source")
|
||||
.setId(UUID.randomUUID().toString()).setSpecVersion("1.0").setType("myType")
|
||||
.setHeader(MessageUtils.TARGET_PROTOCOL, "kafka").build(),
|
||||
MimeTypeUtils.APPLICATION_JSON);
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> result = output.receive();
|
||||
assertThat(result.getHeaders().get("ce_type")).isNotNull();
|
||||
assertThat(result.getHeaders().get("ce_source")).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This test must not result in exception stating "Partition key cannot be null"
|
||||
* See https://github.com/spring-cloud/spring-cloud-stream/issues/2249 for more details
|
||||
*/
|
||||
@Test
|
||||
void test_2249() throws Exception {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(
|
||||
EmptyConfiguration.class)).web(WebApplicationType.NONE).run(
|
||||
"--spring.cloud.stream.source=outputA;outputB",
|
||||
"--spring.cloud.stream.bindings.outputA-out-0.producer.partition-count=3",
|
||||
"--spring.cloud.stream.bindings.outputA-out-0.producer.partition-key-expression=headers['partitionKey']",
|
||||
"--spring.cloud.stream.bindings.outputB-out-0.destination=outputB",
|
||||
"--spring.cloud.stream.bindings.outputB-out-0.producer.partition-count=3",
|
||||
"--spring.jmx.enabled=false")) {
|
||||
StreamBridge streamBridge = context.getBean(StreamBridge.class);
|
||||
streamBridge.send("outputA-out-0", MessageBuilder.withPayload("A").setHeader("partitionKey", "A").build());
|
||||
streamBridge.send("outputB", MessageBuilder.withPayload("B").build());
|
||||
streamBridge.send("outputA-out-0", MessageBuilder.withPayload("C").setHeader("partitionKey", "C").build());
|
||||
streamBridge.send("outputB", MessageBuilder.withPayload("D").build());
|
||||
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
assertThat(output.receive(1000, "outputA-out-0").getHeaders().containsKey("scst_partition")).isTrue();
|
||||
assertThat(output.receive(1000, "outputB").getHeaders().containsKey("scst_partition")).isFalse();
|
||||
assertThat(output.receive(1000, "outputA-out-0").getHeaders().containsKey("scst_partition")).isTrue();
|
||||
assertThat(output.receive(1000, "outputB").getHeaders().containsKey("scst_partition")).isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This test verifies that when a partition key expression is set, then scst_partition is always set, even in
|
||||
* concurrent scenarios.
|
||||
* See https://github.com/spring-cloud/spring-cloud-stream/issues/2299 for more details
|
||||
*/
|
||||
@Test
|
||||
void test_2299_scstPartitionAlwaysSetEvenInConcurrentScenarios() throws Exception {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(EmptyConfiguration.class)).web(
|
||||
WebApplicationType.NONE).run("--spring.cloud.stream.source=outputA",
|
||||
"--spring.cloud.stream.bindings.outputA-out-0.producer.partition-count=3",
|
||||
"--spring.cloud.stream.bindings.outputA-out-0.producer.partition-key-expression=headers['partitionKey']",
|
||||
"--spring.jmx.enabled=false")) {
|
||||
StreamBridge streamBridge = context.getBean(StreamBridge.class);
|
||||
|
||||
int threadCount = 10;
|
||||
Set<Thread> threads = IntStream.range(0, threadCount)
|
||||
.mapToObj(i -> (Runnable) () -> IntStream.range(0, 100).forEach(j -> {
|
||||
String value = "M-" + i + "-" + j;
|
||||
streamBridge.send("outputA-out-0",
|
||||
MessageBuilder.withPayload(value).setHeader("partitionKey", value).build());
|
||||
})).map(Thread::new).collect(Collectors.toSet());
|
||||
|
||||
threads.forEach(Thread::start);
|
||||
for (Thread thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
|
||||
int messagesWithoutScstPartition = 0;
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> message = output.receive(1000, "outputA-out-0");
|
||||
while (message != null) {
|
||||
if (!message.getHeaders().containsKey("scst_partition")) {
|
||||
messagesWithoutScstPartition++;
|
||||
}
|
||||
message = output.receive(1000, "outputA-out-0");
|
||||
}
|
||||
assertThat(messagesWithoutScstPartition).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithOutputContentTypeWildCardBindings() throws Exception {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(ConsumerConfiguration.class, EmptyConfigurationWithCustomConverters.class))
|
||||
.web(WebApplicationType.NONE).run(
|
||||
"--spring.cloud.stream.bindings.foo.content-type=application/*+foo ",
|
||||
"--spring.cloud.stream.bindings.bar.content-type=application/*+non-registered-foo",
|
||||
"--spring.jmx.enabled=false")) {
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
bridge.send("foo", "hello foo");
|
||||
bridge.send("bar", "hello bar");
|
||||
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
|
||||
assertThat(output.receive(1000, "foo").getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeType.valueOf("application/json+foo"));
|
||||
assertThat(output.receive(1000, "bar").getHeaders().get(MessageHeaders.CONTENT_TYPE))
|
||||
.isEqualTo(MimeType.valueOf("application/blahblah+non-registered-foo"));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void testNoCachingOfStreamBridgeFunction() throws Exception {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(ConsumerConfiguration.class, InterceptorConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run(
|
||||
"--spring.cloud.function.definition=function",
|
||||
"--spring.jmx.enabled=false")) {
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
bridge.send("function-in-0", (Object) "hello foo", MimeTypeUtils.TEXT_PLAIN);
|
||||
bridge.send("function-in-0", (Object) "hello foo", MimeTypeUtils.APPLICATION_JSON);
|
||||
bridge.send("function-in-0", (Object) "hello foo", MimeTypeUtils.TEXT_HTML);
|
||||
|
||||
Field field = ReflectionUtils.findField(StreamBridge.class, "streamBridgeFunctionCache");
|
||||
field.setAccessible(true);
|
||||
Map<String, FunctionInvocationWrapper> map = (Map<String, FunctionInvocationWrapper>) field.get(bridge);
|
||||
assertThat(map.size()).isEqualTo(3);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDelayedSend() {
|
||||
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(ConsumerConfiguration.class, EmptyConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run(
|
||||
"--spring.jmx.enabled=false")) {
|
||||
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
executor.schedule(() -> bridge.send("blah", "hello foo"), 5000, TimeUnit.MILLISECONDS);
|
||||
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> message = outputDestination.receive(10000, "blah");
|
||||
assertThat(message).isNotNull();
|
||||
assertThat(new String(message.getPayload())).isEqualTo("hello foo");
|
||||
}
|
||||
finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithInterceptorsMatchedAgainstAllPatterns() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(ConsumerConfiguration.class, InterceptorConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run(
|
||||
"--spring.cloud.function.definition=function",
|
||||
"--spring.jmx.enabled=false")) {
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
bridge.send("function-in-0", "hello foo");
|
||||
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> message = outputDestination.receive(100, "function-out-0");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("hello foo");
|
||||
assertThat(message.getHeaders().get("intercepted")).isEqualTo("true");
|
||||
}
|
||||
}
|
||||
|
||||
@Test // validate that there is no exception thrown when sending to null channel
|
||||
void test_2268() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(InterceptorConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run(
|
||||
"--spring.jmx.enabled=false")) {
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
|
||||
bridge.send("nullChannel", "blah");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInterceptorIsNotAddedMultipleTimesToTheMessageChannel() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(InterceptorConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run(
|
||||
"--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.dynamic-destination-cache-size=1",
|
||||
"--spring.cloud.stream.output-bindings=outputA;outputB",
|
||||
"--spring.cloud.stream.bindings.outputA-out-0.destination=outputA",
|
||||
"--spring.cloud.stream.bindings.outputB-out-0.destination=outputB"
|
||||
)) {
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
|
||||
bridge.send("outputA-out-0", "hello foo");
|
||||
bridge.send("outputA-out-0", "hello foo");
|
||||
bridge.send("outputA-out-0", "hello foo");
|
||||
bridge.send("outputA-out-0", "hello foo");
|
||||
|
||||
AbstractMessageChannel messageChannel = context.getBean("outputA-out-0", AbstractMessageChannel.class);
|
||||
|
||||
assertThat(messageChannel.getInterceptors()).hasSize(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindingsAreRemovedWithCache() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(InterceptorConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run(
|
||||
"--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.dynamic-destination-cache-size=1"
|
||||
)) {
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
|
||||
bridge.send("a", "hello foo");
|
||||
bridge.send("b", "hello foo");
|
||||
bridge.send("c", "hello foo");
|
||||
bridge.send("d", "hello foo");
|
||||
|
||||
BindingService bindingService = context.getBean(BindingService.class);
|
||||
assertThat(bindingService.getProducerBindingNames().length).isEqualTo(1);
|
||||
assertThat(bindingService.getProducerBindingNames()[0]).isEqualTo("d");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithInterceptorsRegisteredOnlyOnOutputChannel() throws InterruptedException {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(GH2180Configuration.class))
|
||||
.web(WebApplicationType.NONE).run(
|
||||
"--spring.jmx.enabled=false")) {
|
||||
|
||||
MessageChannel inputChannel = context.getBean("inputChannel", MessageChannel.class);
|
||||
inputChannel.send(MessageBuilder.withPayload("hello foo").build());
|
||||
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> message = outputDestination.receive(100, "outgoing-out-0");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("hello foo");
|
||||
assertThat(message.getHeaders().get("intercepted")).isEqualTo("true");
|
||||
//Ensure that the LoggingHandler in the first SI flow is invoked.
|
||||
GH2180Configuration.LATCH1.await(10, TimeUnit.SECONDS);
|
||||
//Ensure that the second SI flow does not trigger its LoggingHandler (aka wiretap/interceptor).
|
||||
assertThat(GH2180Configuration.LATCH2.getCount()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindingPropertiesAreHonored() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(ConsumerConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run(
|
||||
"--spring.cloud.function.definition=consumer;function",
|
||||
"--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.bindings.foo.destination=function-in-0",
|
||||
"--spring.cloud.stream.bindings.foo.producer.partitionCount=5",
|
||||
"--spring.cloud.stream.bindings.foo.consumer.concurrency=2")) {
|
||||
|
||||
BindingServiceProperties bsProperties = context.getBean(BindingServiceProperties.class);
|
||||
assertThat(bsProperties.getConsumerProperties("foo").getConcurrency()).isEqualTo(2);
|
||||
assertThat(bsProperties.getProducerProperties("foo").getPartitionCount()).isEqualTo(5);
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
bridge.send("consumer-in-0", "hello foo");
|
||||
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> message = outputDestination.receive(100, "function-out-0");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("hello foo");
|
||||
assertThat(message.getHeaders().get("concurrency")).isEqualTo(2);
|
||||
assertThat(message.getHeaders().get("partitionCount")).isEqualTo(5);
|
||||
}
|
||||
}
|
||||
|
||||
//see https://github.com/spring-cloud/spring-cloud-function/issues/573 for more details
|
||||
@Test
|
||||
void testBridgeActivationWhenFunctionDefinitionIsPresent() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(SimpleConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run(
|
||||
"--spring.cloud.function.definition=echo;uppercase",
|
||||
"--spring.jmx.enabled=false")) {
|
||||
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
bridge.send("echo-in-0", "hello foo");
|
||||
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
assertThat(new String(outputDestination.receive(100, "echo-out-0").getPayload())).isEqualTo("hello foo");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoBridgeIfNoSourcePropertyDefined() {
|
||||
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
|
||||
.isThrownBy(() -> {
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration())
|
||||
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false");
|
||||
context.getBean(StreamBridge.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBridgeFunctions() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(EmptyConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run("--spring.cloud.stream.source=foo;bar",
|
||||
"--spring.jmx.enabled=false")) {
|
||||
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
bridge.send("foo-out-0", "hello foo");
|
||||
bridge.send("bar-out-0", "hello bar");
|
||||
|
||||
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
assertThat(new String(outputDestination.receive(100, "foo-out-0").getPayload())).isEqualTo("hello foo");
|
||||
assertThat(new String(outputDestination.receive(100, "bar-out-0").getPayload())).isEqualTo("hello bar");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBridgeFunctionsSendingMessagePreservingHeaders() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(EmptyConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run("--spring.cloud.stream.source=foo;bar",
|
||||
"--spring.jmx.enabled=false")) {
|
||||
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
bridge.send("foo-out-0", MessageBuilder.withPayload("hello foo").setHeader("foo", "foo").build());
|
||||
bridge.send("bar-out-0", MessageBuilder.withPayload("hello bar").setHeader("bar", "bar").build());
|
||||
|
||||
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
|
||||
|
||||
Message<?> message = outputDestination.receive(100, "foo-out-0");
|
||||
assertThat(message.getPayload()).isEqualTo("hello foo".getBytes());
|
||||
assertThat(message.getHeaders().get("foo")).isEqualTo("foo");
|
||||
|
||||
message = outputDestination.receive(100, "bar-out-0");
|
||||
assertThat(message.getPayload()).isEqualTo("hello bar".getBytes());
|
||||
assertThat(message.getHeaders().get("bar")).isEqualTo("bar");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBridgeFunctionsWitthPartitionInformation() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(EmptyConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run("--spring.cloud.stream.source=foo;bar",
|
||||
"--spring.cloud.stream.bindings.foo-out-0.producer.partitionKeyExpression=payload",
|
||||
"--spring.cloud.stream.bindings.foo-out-0.producer.partitionCount=5",
|
||||
"--spring.cloud.stream.bindings.bar-out-0.producer.partitionKeyExpression=payload",
|
||||
"--spring.cloud.stream.bindings.bar-out-0.producer.partitionCount=1",
|
||||
"--spring.jmx.enabled=false")) {
|
||||
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
bridge.send("foo-out-0", "a");
|
||||
bridge.send("bar-out-0", "b");
|
||||
bridge.send("foo-out-0", "c");
|
||||
bridge.send("foo-out-0", "d");
|
||||
bridge.send("bar-out-0", "e");
|
||||
bridge.send("foo-out-0", "f");
|
||||
bridge.send("bar-out-0", "g");
|
||||
|
||||
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> message = outputDestination.receive(100, "foo-out-0");
|
||||
|
||||
assertThat(new String(message.getPayload())).isEqualTo("a");
|
||||
assertThat(message.getHeaders().get("scst_partition")).isEqualTo(2);
|
||||
|
||||
message = outputDestination.receive(100, "foo-out-0");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("c");
|
||||
assertThat(message.getHeaders().get("scst_partition")).isEqualTo(4);
|
||||
|
||||
message = outputDestination.receive(100, "foo-out-0");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("d");
|
||||
assertThat(message.getHeaders().get("scst_partition")).isEqualTo(0);
|
||||
|
||||
message = outputDestination.receive(100, "bar-out-0");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("b");
|
||||
assertThat(message.getHeaders().get("scst_partition")).isEqualTo(0);
|
||||
|
||||
message = outputDestination.receive(100, "bar-out-0");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("e");
|
||||
assertThat(message.getHeaders().get("scst_partition")).isEqualTo(0);
|
||||
|
||||
message = outputDestination.receive(100, "bar-out-0");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("g");
|
||||
assertThat(message.getHeaders().get("scst_partition")).isEqualTo(0);
|
||||
|
||||
message = outputDestination.receive(100, "foo-out-0");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("f");
|
||||
assertThat(message.getHeaders().get("scst_partition")).isEqualTo(2);
|
||||
|
||||
//assertThat(new String(outputDestination.receive(100, "bar-out-0").getPayload())).isEqualTo("b");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendingMessageToOutputOfExistingSupplier() throws Exception {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(TestConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run("--spring.cloud.stream.source=supplier;foo",
|
||||
"--spring.jmx.enabled=false")) {
|
||||
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
bridge.send("supplier-out-0", "blah");
|
||||
bridge.send("foo-out-0", "b");
|
||||
|
||||
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> message = outputDestination.receive(100, "foo-out-0");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("b");
|
||||
message = outputDestination.receive(100, "supplier-out-0");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("hello");
|
||||
message = outputDestination.receive(100, "supplier-out-0");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("blah");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDynamicDestination() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(TestConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) {
|
||||
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
bridge.send("foo-out-0", "b");
|
||||
bridge.send("bar", "hello");
|
||||
bridge.send("blah", MessageBuilder.withPayload("message").setHeader("foo", "foo").build());
|
||||
|
||||
Message<byte[]> message = outputDestination.receive(100, "foo-out-0");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("b");
|
||||
|
||||
message = outputDestination.receive(100, "bar");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("hello");
|
||||
|
||||
message = outputDestination.receive(100, "blah");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("message");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithIntegrationFlowBecauseMarcinSaidSo() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(IntegrationFlowConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) {
|
||||
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
bridge.send("foo", "blah");
|
||||
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> message = outputDestination.receive(100, "output");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("BLAH");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNewBindingCallback() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(BindingCallbackConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run("--spring.cloud.stream.source=uppercase",
|
||||
"--spring.jmx.enabled=false")) {
|
||||
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
bridge.send("uppercase-in-0", "hello");
|
||||
assertThat(context.getBean("callbackVerifier", AtomicBoolean.class)).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDynamicProducerDestination() {
|
||||
System.clearProperty("spring.cloud.function.definition");
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(DynamicProducerDestinationConfig.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false",
|
||||
"--spring.cloud.function.definition=uppercase",
|
||||
"--spring.cloud.stream.bindings.uppercase-in-0.destination=upper"
|
||||
);
|
||||
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
source.send(new GenericMessage<>("John Doe".getBytes()), "upper");
|
||||
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> message = target.receive(5, "dynamicTopic");
|
||||
|
||||
assertThat(message).isNotNull();
|
||||
assertThat(new String(message.getPayload())).isEqualTo("JOHN DOE");
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class DynamicProducerDestinationConfig {
|
||||
@Bean
|
||||
public Function<Message<String>, Message<String>> uppercase() {
|
||||
return msg -> MessageBuilder.withPayload(msg.getPayload().toUpperCase())
|
||||
.setHeader("spring.cloud.stream.sendto.destination", "dynamicTopic").build();
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class EmptyConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class EmptyConfigurationWithCustomConverters {
|
||||
|
||||
@Bean
|
||||
public MessageConverter fooConverter() {
|
||||
return new AbstractMessageConverter(MimeType.valueOf("application/json+foo"), MimeType.valueOf("application/json+blah")) {
|
||||
@Override
|
||||
protected boolean supports(Class<?> clazz) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Object convertFromInternal(Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) {
|
||||
return message.getPayload();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) {
|
||||
if (headers.containsKey(MessageHeaders.CONTENT_TYPE) &&
|
||||
(headers.get(MessageHeaders.CONTENT_TYPE).toString().endsWith("+foo") ||
|
||||
headers.get(MessageHeaders.CONTENT_TYPE).toString().endsWith("+blah"))) {
|
||||
return payload;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MessageConverter barConverter() {
|
||||
return new AbstractMessageConverter(MimeType.valueOf("application/blahblah+non-registered-foo")) {
|
||||
@Override
|
||||
protected boolean supports(Class<?> clazz) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Object convertFromInternal(Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) {
|
||||
return message.getPayload();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) {
|
||||
if (headers.containsKey(MessageHeaders.CONTENT_TYPE) &&
|
||||
(headers.get(MessageHeaders.CONTENT_TYPE).toString().endsWith("+non-registered-foo"))) {
|
||||
return payload;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class ConsumerConfiguration {
|
||||
@Bean
|
||||
public Consumer<String> consumer(StreamBridge bridge, BindingServiceProperties properties) {
|
||||
return v -> {
|
||||
BindingServiceProperties p = properties;
|
||||
bridge.send("foo", v);
|
||||
};
|
||||
}
|
||||
@Bean
|
||||
public Function<String, Message<String>> function(StreamBridge bridge, BindingServiceProperties properties) {
|
||||
return v -> {
|
||||
int concurrency = properties.getConsumerProperties("foo").getConcurrency();
|
||||
int partitionCount = properties.getProducerProperties("foo").getPartitionCount();
|
||||
BindingServiceProperties p = properties;
|
||||
return MessageBuilder.withPayload(v)
|
||||
.setHeader("concurrency", concurrency)
|
||||
.setHeader("partitionCount", partitionCount)
|
||||
.build();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class InterceptorConfiguration {
|
||||
@Bean
|
||||
@GlobalChannelInterceptor(patterns = "*")
|
||||
public ChannelInterceptor interceptor() {
|
||||
return new ChannelInterceptor() {
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
return MessageBuilder.fromMessage(message).setHeader("intercepted", "true").build();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
public Supplier<String> supplier() {
|
||||
return () -> "hello";
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class SimpleConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> echo() {
|
||||
return v -> v;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return v -> v.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class BindingCallbackConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> echo() {
|
||||
return v -> v;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<String, String> uppercase() {
|
||||
return v -> v.toUpperCase();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AtomicBoolean callbackVerifier() {
|
||||
return new AtomicBoolean();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NewDestinationBindingCallback callback(AtomicBoolean callbackVerifier) {
|
||||
|
||||
return (name, channel, props, extended) -> {
|
||||
callbackVerifier.set(true);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class IntegrationFlowConfiguration {
|
||||
|
||||
@Bean
|
||||
public IntegrationFlow transform(StreamBridge bridge) {
|
||||
return IntegrationFlows.from("foo").transform(v -> {
|
||||
String s = new String((byte[]) v);
|
||||
return s.toUpperCase();
|
||||
})
|
||||
.handle(v -> bridge.send("output", v))
|
||||
.get();
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class GH2180Configuration {
|
||||
|
||||
static CountDownLatch LATCH1 = new CountDownLatch(1);
|
||||
static CountDownLatch LATCH2 = new CountDownLatch(1);
|
||||
|
||||
@Bean
|
||||
MessageChannel inputChannel() {
|
||||
return new DirectChannel();
|
||||
}
|
||||
|
||||
@Bean
|
||||
MessageChannel otherInputChannel() {
|
||||
return new DirectChannel();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IntegrationFlow someFlow(MessageHandler sendMessage, MessageChannel inputChannel) {
|
||||
return IntegrationFlows.from(inputChannel)
|
||||
.log(LoggingHandler.Level.INFO, (m) -> {
|
||||
LATCH1.countDown();
|
||||
return "Going through the first flow: " + m.getPayload();
|
||||
})
|
||||
.handle(sendMessage)
|
||||
.get();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IntegrationFlow someOtherFlow(MessageHandler sendMessage) {
|
||||
return IntegrationFlows.from(otherInputChannel())
|
||||
.log(LoggingHandler.Level.INFO, (m) -> {
|
||||
LATCH2.countDown();
|
||||
return "Going through the second flow: " + m.getPayload();
|
||||
})
|
||||
.handle(sendMessage)
|
||||
.get();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@GlobalChannelInterceptor(patterns = "outgoing-*")
|
||||
public ChannelInterceptor fooInterceptor() {
|
||||
return new ChannelInterceptor() {
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
return MessageBuilder.fromMessage(message).setHeader("intercepted", "true").build();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MessageHandler sendMessage(StreamBridge streamBridge) {
|
||||
return message -> {
|
||||
streamBridge.send("outgoing-out-0", message);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.function.edgecases;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
* This test validates that the issue https://github.com/spring-cloud/spring-cloud-stream/issues/1801
|
||||
* is addressed.
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
*
|
||||
*/
|
||||
public class GH1801Test {
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
SampleBootApplication.main("--spring.cloud.stream.defaultBinder=integration");
|
||||
}
|
||||
|
||||
@SpringBootApplication
|
||||
public static class SampleBootApplication {
|
||||
public static void main(String... args) {
|
||||
new SpringApplicationBuilder(SampleBootApplication.class).web(WebApplicationType.NONE).run(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
class StreamConfiguration {
|
||||
|
||||
@Bean
|
||||
public Consumer<Message<?>> consumer() {
|
||||
return System.out::println;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2022-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.kotlin;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.binder.test.OutputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class KotlinConfigurationTests {
|
||||
|
||||
@Test
|
||||
void testKotlinSupplierPollableBean() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(KotlinTestConfiguration.class))
|
||||
.web(WebApplicationType.NONE)
|
||||
.run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=produceNames")) {
|
||||
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> result = output.receive(1000, "produceNames-out-0");
|
||||
assertThat(result.getPayload()).isEqualTo("Ricky".getBytes());
|
||||
result = output.receive(1000, "produceNames-out-0");
|
||||
assertThat(result.getPayload()).isEqualTo("Julien".getBytes());
|
||||
result = output.receive(1000, "produceNames-out-0");
|
||||
assertThat(result.getPayload()).isEqualTo("Bubbles".getBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2015-2019 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.utils;
|
||||
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.cloud.stream.binder.Binder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
@Configuration
|
||||
public class IntegrationTestsMockBinderConfiguration {
|
||||
|
||||
@Bean
|
||||
public Binder<?, ?, ?> binder() {
|
||||
return Mockito.mock(Binder.class,
|
||||
Mockito.withSettings().defaultAnswer(Mockito.RETURNS_MOCKS));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2022-2022 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.cloud.stream.kotlin
|
||||
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.cloud.function.context.PollableBean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import reactor.core.publisher.Flux
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
open class KotlinTestConfiguration {
|
||||
|
||||
@PollableBean // it doesn't work with Kotlin lambda
|
||||
open fun produceNames(): () -> Flux<String> = {
|
||||
Flux.just(
|
||||
"Ricky",
|
||||
"Julien",
|
||||
"Bubbles"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user