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:
Artem Bilan
2019-01-16 13:16:05 -05:00
committed by Oleg Zhurakousky
parent 00ce61a753
commit 460ba9cecb
5 changed files with 102 additions and 35 deletions

View File

@@ -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.

View File

@@ -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);

View File

@@ -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);
/*

View File

@@ -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";
}
}
}

View File

@@ -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;
}
}
}