GH-1569: Fix binder-specific environment merging
Fixes spring-cloud/spring-cloud-stream#1569 When `inheritEnvironment = true` (default), some provided properties (including `spring.main.sources`) are not populated into the target binder's environment, since Spring Boot relies on the presence of the `configurationProperties` properties source which is transferred from the parent context * Remove `configurationProperties` from the `binderEnvironment` before starting binder's application context * Ensure that `spring.main.sources` is applied for the binder's application context in the `BinderFactoryConfigurationTests` * Fix `GreenfieldFunctionEnableBindingTests` for random HTTP port since `8080` is too generic and clashes with build environment * Fix `StreamListenerAnnotatedMethodArgumentsTests` for `Locale.US` for proper assertion against validation message * Mention `spring.profiles.active` configuration property for the binder specific environment
This commit is contained in:
committed by
Oleg Zhurakousky
parent
00ce61a753
commit
460ba9cecb
@@ -1321,8 +1321,8 @@ spring:
|
||||
rabbitmq:
|
||||
host: <host2>
|
||||
----
|
||||
NOTE: The `environment` property of the particular binder can also be used for any Spring Boot property,
|
||||
including this `spring.main.sources` which can be useful for adding additional configurations for the
|
||||
NOTE: The `environment` property of the particular binder can also be used for any Spring Boot property,
|
||||
including this `spring.main.sources` which can be useful for adding additional configurations for the
|
||||
particular binders, e.g. overriding auto-configured beans.
|
||||
|
||||
For example;
|
||||
@@ -1334,6 +1334,16 @@ environment:
|
||||
sources: com.acme.config.MyCustomBinderConfiguration
|
||||
----
|
||||
|
||||
To activate some specific profile for the particular binder environment, you should use a `spring.profiles.active` property:
|
||||
|
||||
[source, yaml]
|
||||
----
|
||||
environment:
|
||||
spring:
|
||||
profiles:
|
||||
active: myBinderProfile
|
||||
----
|
||||
|
||||
=== Binding visualization and control
|
||||
Since version 2.0, Spring Cloud Stream supports visualization and control of the Bindings through Actuator endpoints.
|
||||
|
||||
@@ -2499,20 +2509,20 @@ When autoconfiguration is disabled, the test binder is available on the classpat
|
||||
|
||||
[[spring_integration_test_binder]]
|
||||
=== Spring Integration Test Binder
|
||||
Current test binder was specifically designed to facilitate _unit testing_ of the actual messaging components and thus bypasses some of the core functionality of the binder API.
|
||||
While such light-weight approach is sufficient for a lot of cases, it usually requires additional _integration testing_ with real binders (e.g., Rabbit, Kafka etc).
|
||||
Current test binder was specifically designed to facilitate _unit testing_ of the actual messaging components and thus bypasses some of the core functionality of the binder API.
|
||||
While such light-weight approach is sufficient for a lot of cases, it usually requires additional _integration testing_ with real binders (e.g., Rabbit, Kafka etc).
|
||||
|
||||
To begin bridging the gap between _unit_ and _integration_ testing we've developed a new test binder which uses https://spring.io/projects/spring-integration[Spring Integration] framework
|
||||
as an in-JVM Message Broker essentially giving you the best of both worlds - a real binder without the networking.
|
||||
|
||||
To enable Spring Integration Test Binder all you need is:
|
||||
|
||||
- Add required dependencies
|
||||
- Add required dependencies
|
||||
- Remove the dependency for `spring-cloud-stream-test-support`
|
||||
|
||||
***Add required dependencies***
|
||||
|
||||
Below is the example of the required Maven POM entries which could be easily retrofitted into Gradle.
|
||||
Below is the example of the required Maven POM entries which could be easily retrofitted into Gradle.
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
@@ -2584,7 +2594,7 @@ public class DemoTestBinderApplication {
|
||||
@Test
|
||||
public void sampleTest() {
|
||||
ApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.class,
|
||||
TestChannelBinderConfiguration.class,
|
||||
DemoTestBinderApplication.class)
|
||||
.web(WebApplicationType.NONE).run();
|
||||
InputDestination source = context.getBean(InputDestination.class);
|
||||
@@ -2594,20 +2604,20 @@ public void sampleTest() {
|
||||
}
|
||||
----
|
||||
|
||||
In the above you simply create an ApplicationContext with your configuration (your application) while additionally supplying `TestChannelBinderConfiguration`
|
||||
In the above you simply create an ApplicationContext with your configuration (your application) while additionally supplying `TestChannelBinderConfiguration`
|
||||
provided by the framework. Then you access `InputDestination` and `OutputDestination` beans to send/receive messages. In the context of this binder
|
||||
`InputDestination` and `OutputDestination` emulate remote destinations such as Rabbit _exchange/queue_ or Kafka _topic_.
|
||||
|
||||
In the future we plan to simplify the API.
|
||||
|
||||
NOTE: In its current state Spring Integration Test Binder only supports the three bindings provided by the framework (Source, Processor, Sink) specifically to promote
|
||||
NOTE: In its current state Spring Integration Test Binder only supports the three bindings provided by the framework (Source, Processor, Sink) specifically to promote
|
||||
light-weight microservices architectures rather then general purpose messaging applications.
|
||||
|
||||
|
||||
==== Spring Integration Test Binder and PollableMessageSource
|
||||
Spring Integration Test Binder also allows you to write tests when working with `PollableMessageSource` (see <<Using Polled Consumers>> for more details).
|
||||
|
||||
The important thing that needs to be understood though is that polling is not event-driven, and that `PollableMessageSource` is a strategy which exposes operation to produce (poll for) a Message (singular).
|
||||
How often you poll or how many threads you use or where you're polling from (message queue or file system) is entirely up to you;
|
||||
The important thing that needs to be understood though is that polling is not event-driven, and that `PollableMessageSource` is a strategy which exposes operation to produce (poll for) a Message (singular).
|
||||
How often you poll or how many threads you use or where you're polling from (message queue or file system) is entirely up to you;
|
||||
In other words it is your responsibility to configure Poller or Threads or the actual source of Message. Luckily Spring has plenty of abstractions to configure exactly that.
|
||||
|
||||
Let's look at the example:
|
||||
@@ -2624,7 +2634,7 @@ public void samplePollingTest() {
|
||||
System.out.println("Message 2: " + new String(destination.receive().getPayload()));
|
||||
System.out.println("Message 3: " + new String(destination.receive().getPayload()));
|
||||
}
|
||||
|
||||
|
||||
@EnableBinding(SamplePolledConfiguration.PolledConsumer.class)
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
@@ -2641,7 +2651,7 @@ public static class SamplePolledConfiguration {
|
||||
})) {
|
||||
Thread.sleep(2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
// handle failure
|
||||
}
|
||||
@@ -2649,7 +2659,7 @@ public static class SamplePolledConfiguration {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public static interface PolledConsumer extends Source {
|
||||
@Input
|
||||
PollableMessageSource pollableSource();
|
||||
@@ -2657,7 +2667,7 @@ public static class SamplePolledConfiguration {
|
||||
}
|
||||
----
|
||||
|
||||
The above (very rudimentary) example will produce 3 messages in 2 second intervals sending them to the output destination of `Source`
|
||||
The above (very rudimentary) example will produce 3 messages in 2 second intervals sending them to the output destination of `Source`
|
||||
which this binder sends to `OutputDestination` where we retrieve them (for any assertions).
|
||||
Currently it prints the following:
|
||||
[source, text]
|
||||
@@ -2666,9 +2676,9 @@ Message 1: POLLED DATA
|
||||
Message 2: POLLED DATA
|
||||
Message 3: POLLED DATA
|
||||
----
|
||||
As you can see the data is the same. That is because this binder defines a default implementation of the actual `MessageSource` - the source
|
||||
from which the Messages are polled using `poll()` operation. While sufficient for most testing scenarios, there are cases where you may want
|
||||
to define your own `MessageSource`. To do so simply configure a bean of type `MessageSource` in your test configuration providing your own
|
||||
As you can see the data is the same. That is because this binder defines a default implementation of the actual `MessageSource` - the source
|
||||
from which the Messages are polled using `poll()` operation. While sufficient for most testing scenarios, there are cases where you may want
|
||||
to define your own `MessageSource`. To do so simply configure a bean of type `MessageSource` in your test configuration providing your own
|
||||
implementation of Message sourcing.
|
||||
|
||||
Here is the example:
|
||||
@@ -2688,7 +2698,7 @@ Message 2: MY OWN DATA D8F3A477-5547-41B4-9434-E69DA7616FEE
|
||||
Message 3: MY OWN DATA 20BF2E64-7FF4-4CB6-A823-4053D30B5C74
|
||||
----
|
||||
|
||||
NOTE: DO NOT name this bean `messageSource` as it is going to be in conflict with the bean of the same name (different type)
|
||||
NOTE: DO NOT name this bean `messageSource` as it is going to be in conflict with the bean of the same name (different type)
|
||||
provided by Spring Boot for unrelated reasons.
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016-2017 the original author or authors.
|
||||
* Copyright 2016-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.
|
||||
@@ -18,11 +18,13 @@ package org.springframework.cloud.stream.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
@@ -49,12 +51,18 @@ import static org.springframework.cloud.stream.binding.StreamListenerErrorMessag
|
||||
* @author Marius Bogoevici
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Artem Bilan
|
||||
*/
|
||||
public class StreamListenerAnnotatedMethodArgumentsTests {
|
||||
|
||||
@BeforeClass
|
||||
public static void init() {
|
||||
Locale.setDefault(Locale.US);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testAnnotatedArguments() throws Exception {
|
||||
public void testAnnotatedArguments() {
|
||||
ConfigurableApplicationContext context = SpringApplication.run(TestPojoWithAnnotatedArguments.class,
|
||||
"--server.port=0");
|
||||
|
||||
@@ -79,7 +87,7 @@ public class StreamListenerAnnotatedMethodArgumentsTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInputAnnotationAtMethodParameter() throws Exception {
|
||||
public void testInputAnnotationAtMethodParameter() {
|
||||
try {
|
||||
SpringApplication.run(TestPojoWithInvalidInputAnnotatedArgument.class, "--server.port=0");
|
||||
fail("Exception expected: " + INVALID_DECLARATIVE_METHOD_PARAMETERS);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015-2018 the original author or authors.
|
||||
* 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.
|
||||
@@ -52,6 +52,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Gary Russell
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Soby Chacko
|
||||
* @author Artem Bilan
|
||||
*/
|
||||
public class DefaultBinderFactory implements BinderFactory, DisposableBean, ApplicationContextAware {
|
||||
|
||||
@@ -244,10 +245,12 @@ public class DefaultBinderFactory implements BinderFactory, DisposableBean, Appl
|
||||
if (environment != null && (useApplicationContextAsParent || binderConfiguration.isInheritEnvironment())) {
|
||||
StandardEnvironment binderEnvironment = new StandardEnvironment();
|
||||
binderEnvironment.merge(environment);
|
||||
// See ConfigurationPropertySources.ATTACHED_PROPERTY_SOURCE_NAME
|
||||
binderEnvironment.getPropertySources().remove("configurationProperties");
|
||||
springApplicationBuilder.environment(binderEnvironment);
|
||||
}
|
||||
ConfigurableApplicationContext binderProducingContext = springApplicationBuilder
|
||||
.run(args.toArray(new String[args.size()]));
|
||||
ConfigurableApplicationContext binderProducingContext =
|
||||
springApplicationBuilder.run(args.toArray(new String[0]));
|
||||
|
||||
Binder<T, ?, ?> binder = binderProducingContext.getBean(Binder.class);
|
||||
/*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015-2018 the original author or authors.
|
||||
* 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.
|
||||
@@ -33,7 +33,10 @@ import org.springframework.cloud.stream.binder.stub2.StubBinder2ConfigurationA;
|
||||
import org.springframework.cloud.stream.binder.stub2.StubBinder2ConfigurationB;
|
||||
import org.springframework.cloud.stream.config.BinderFactoryConfiguration;
|
||||
import org.springframework.cloud.stream.config.BindingServiceConfiguration;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
@@ -47,6 +50,7 @@ import static org.junit.Assert.fail;
|
||||
* @author Marius Bogoevici
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @author Soby Chacko
|
||||
* @author Artem Bilan
|
||||
*/
|
||||
public class BinderFactoryConfigurationTests {
|
||||
|
||||
@@ -130,6 +134,7 @@ public class BinderFactoryConfigurationTests {
|
||||
ConfigurableApplicationContext context = createBinderTestContext(
|
||||
new String[] { "binder1" }, "binder1.name=foo",
|
||||
"spring.cloud.stream.binders.custom.environment.foo=bar",
|
||||
"spring.cloud.stream.binders.custom.environment.spring.main.sources=org.springframework.cloud.stream.binder.BinderFactoryConfigurationTests.AdditionalBinderConfiguration",
|
||||
"spring.cloud.stream.binders.custom.type=binder1");
|
||||
|
||||
BinderFactory binderFactory = context.getBean(BinderFactory.class);
|
||||
@@ -138,6 +143,12 @@ public class BinderFactoryConfigurationTests {
|
||||
assertThat(binder1).hasFieldOrPropertyWithValue("name", "foo");
|
||||
|
||||
assertThat(binderFactory.getBinder(null, MessageChannel.class)).isSameAs(binder1);
|
||||
|
||||
SimpleApplication simpleApplication = context.getBean(SimpleApplication.class);
|
||||
|
||||
assertThat(simpleApplication.binderContext).isNotNull();
|
||||
|
||||
assertThat(simpleApplication.binderContext.containsBean("fooBean")).isTrue();
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@@ -214,7 +225,7 @@ public class BinderFactoryConfigurationTests {
|
||||
BinderTypeRegistry binderTypeRegistry = context.getBean(BinderTypeRegistry.class);
|
||||
assertThat(binderTypeRegistry).isNotNull();
|
||||
assertThat(binderTypeRegistry.getAll().size()).isEqualTo(3);
|
||||
assertThat(binderTypeRegistry.getAll().keySet().contains("binder1"));
|
||||
assertThat(binderTypeRegistry.getAll().keySet()).contains("binder1");
|
||||
assertThat((Class[]) binderTypeRegistry.get("binder1").getConfigurationClasses())
|
||||
.contains(StubBinder1Configuration.class);
|
||||
|
||||
@@ -260,5 +271,26 @@ public class BinderFactoryConfigurationTests {
|
||||
@EnableBinding
|
||||
public static class SimpleApplication {
|
||||
|
||||
private volatile ApplicationContext binderContext;
|
||||
|
||||
@Bean
|
||||
public DefaultBinderFactory.Listener testBinderListener() {
|
||||
return (configurationName, binderContext) -> {
|
||||
this.binderContext = binderContext;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
public static class AdditionalBinderConfiguration {
|
||||
|
||||
@Bean
|
||||
public String fooBean() {
|
||||
return "foo";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
* Copyright 2018-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.
|
||||
@@ -61,6 +61,7 @@ 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 {
|
||||
|
||||
@@ -110,10 +111,14 @@ public class GreenfieldFunctionEnableBindingTests {
|
||||
@Test
|
||||
public void testHttpEndpoint() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(HttpInboundEndpoint.class)).web(
|
||||
WebApplicationType.SERVLET).run("--spring.cloud.stream.function.definition=upperCase", "--spring.jmx.enabled=false")) {
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(HttpInboundEndpoint.class))
|
||||
.web(WebApplicationType.SERVLET)
|
||||
.run("--spring.cloud.stream.function.definition=upperCase",
|
||||
"--spring.jmx.enabled=false",
|
||||
"--server.port=0")) {
|
||||
TestRestTemplate restTemplate = new TestRestTemplate();
|
||||
restTemplate.postForLocation("http://localhost:8080", "hello");
|
||||
restTemplate.postForLocation(
|
||||
"http://localhost:" + context.getEnvironment().getProperty("local.server.port"), "hello");
|
||||
|
||||
OutputDestination target = context.getBean(OutputDestination.class);
|
||||
String result = new String(target.receive(10000).getPayload());
|
||||
@@ -139,7 +144,6 @@ public class GreenfieldFunctionEnableBindingTests {
|
||||
Foo result = mapper.readValue(payload, Foo.class);
|
||||
|
||||
assertThat(result.getBar()).isEqualTo("bar");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,24 +151,29 @@ public class GreenfieldFunctionEnableBindingTests {
|
||||
@EnableAutoConfiguration
|
||||
@EnableBinding(Source.class)
|
||||
public static class SourceFromSupplier {
|
||||
|
||||
@Bean
|
||||
public Supplier<Date> date() {
|
||||
return () -> new Date(12345L);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@EnableBinding(Processor.class)
|
||||
public static class ProcessorFromFunction {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> toUpperCase() {
|
||||
return s -> s.toUpperCase();
|
||||
return String::toUpperCase;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@EnableBinding(Sink.class)
|
||||
public static class SinkFromConsumer {
|
||||
|
||||
@Bean
|
||||
public PollableChannel result() {
|
||||
return new QueueChannel();
|
||||
@@ -177,6 +186,7 @@ public class GreenfieldFunctionEnableBindingTests {
|
||||
System.out.println(s);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@@ -188,7 +198,7 @@ public class GreenfieldFunctionEnableBindingTests {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> upperCase() {
|
||||
return s -> s.toUpperCase();
|
||||
return String::toUpperCase;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -200,6 +210,7 @@ public class GreenfieldFunctionEnableBindingTests {
|
||||
.requestChannel(this.source.output());
|
||||
return httpRequestHandler.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@@ -225,9 +236,11 @@ public class GreenfieldFunctionEnableBindingTests {
|
||||
return MessageBuilder.withPayload(foo).setHeader("foo","foo").build();
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class Foo {
|
||||
|
||||
String bar;
|
||||
|
||||
public String getBar() {
|
||||
@@ -238,4 +251,5 @@ public class GreenfieldFunctionEnableBindingTests {
|
||||
this.bar = bar;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user