From 460ba9cecb27bc4b2d6e7c23a5fed0177119d68c Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 16 Jan 2019 13:16:05 -0500 Subject: [PATCH] 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 --- .../main/asciidoc/spring-cloud-stream.adoc | 50 +++++++++++-------- ...ListenerAnnotatedMethodArgumentsTests.java | 14 ++++-- .../stream/binder/DefaultBinderFactory.java | 9 ++-- .../BinderFactoryConfigurationTests.java | 36 ++++++++++++- .../GreenfieldFunctionEnableBindingTests.java | 28 ++++++++--- 5 files changed, 102 insertions(+), 35 deletions(-) diff --git a/docs/src/main/asciidoc/spring-cloud-stream.adoc b/docs/src/main/asciidoc/spring-cloud-stream.adoc index 028280020..022c97fd0 100644 --- a/docs/src/main/asciidoc/spring-cloud-stream.adoc +++ b/docs/src/main/asciidoc/spring-cloud-stream.adoc @@ -1321,8 +1321,8 @@ spring: rabbitmq: host: ---- -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 <> 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. diff --git a/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/config/StreamListenerAnnotatedMethodArgumentsTests.java b/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/config/StreamListenerAnnotatedMethodArgumentsTests.java index 295962c95..afef15c37 100644 --- a/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/config/StreamListenerAnnotatedMethodArgumentsTests.java +++ b/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/config/StreamListenerAnnotatedMethodArgumentsTests.java @@ -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); diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinderFactory.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinderFactory.java index 5336ce6d7..f7262e835 100644 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinderFactory.java +++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinderFactory.java @@ -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 binder = binderProducingContext.getBean(Binder.class); /* diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderFactoryConfigurationTests.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderFactoryConfigurationTests.java index 20b212878..fa5b41495 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderFactoryConfigurationTests.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderFactoryConfigurationTests.java @@ -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"; + } + + } + } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/function/GreenfieldFunctionEnableBindingTests.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/function/GreenfieldFunctionEnableBindingTests.java index d04ca43da..8234370c8 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/function/GreenfieldFunctionEnableBindingTests.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/function/GreenfieldFunctionEnableBindingTests.java @@ -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() { return () -> new Date(12345L); } + } @EnableAutoConfiguration @EnableBinding(Processor.class) public static class ProcessorFromFunction { + @Bean public Function 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 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; } } + }