diff --git a/core/spring-cloud-stream/pom.xml b/core/spring-cloud-stream/pom.xml
index 2adf5ca9d..3fc8a3942 100644
--- a/core/spring-cloud-stream/pom.xml
+++ b/core/spring-cloud-stream/pom.xml
@@ -64,6 +64,11 @@
spring-boot-starter-test
test
+
+ org.springframework
+ spring-core-test
+ test
+
org.springframework.boot
spring-boot-autoconfigure-processor
diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderChildContextInitializer.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderChildContextInitializer.java
index 11dc24dd8..8c429fe2f 100644
--- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderChildContextInitializer.java
+++ b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderChildContextInitializer.java
@@ -30,6 +30,10 @@ import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
import org.springframework.beans.factory.aot.BeanRegistrationCode;
import org.springframework.beans.factory.support.RegisteredBean;
+import org.springframework.boot.context.properties.bind.Bindable;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.cloud.stream.config.BindingServiceConfiguration;
+import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationContextInitializer;
@@ -85,7 +89,13 @@ public class BinderChildContextInitializer implements ApplicationContextAware, B
if (registeredBean.getBeanClass().equals(getClass())) { //&& registeredBean.getBeanFactory().equals(this.context)) {
this.logger.debug(() -> "Beginning AOT processing for binder child contexts");
ensureBinderFactoryIsSet();
- Map binderConfigurations = this.binderFactory.getBinderConfigurations();
+ // Load the binding service properties from the environment and update the binder factory with them
+ // in order to pick up any user-declared binders. Without this step only the default binder defined
+ // in 'META-INF/spring.binders' will be processed.
+ BindingServiceProperties declaredBinders = this.createBindingServiceProperties();
+ Map binderConfigurations = BindingServiceConfiguration.getBinderConfigurations(
+ this.binderFactory.getBinderTypeRegistry(), declaredBinders);
+ this.binderFactory.updateBinderConfigurations(binderConfigurations);
Map binderChildContexts = binderConfigurations.entrySet().stream()
.map(e -> Map.entry(e.getKey(), binderFactory.createBinderContextForAOT(e.getKey())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
@@ -94,6 +104,13 @@ public class BinderChildContextInitializer implements ApplicationContextAware, B
return null;
}
+ private BindingServiceProperties createBindingServiceProperties() {
+ BindingServiceProperties bindingServiceProperties = new BindingServiceProperties();
+ Binder.get(this.context.getEnvironment())
+ .bind("spring.cloud.stream", Bindable.ofInstance(bindingServiceProperties));
+ return bindingServiceProperties;
+ }
+
private void ensureBinderFactoryIsSet() {
if (this.binderFactory == null) {
Assert.notNull(this.context, () -> "Unable to lookup binder factory from context as this.context is null");
@@ -111,7 +128,7 @@ public class BinderChildContextInitializer implements ApplicationContextAware, B
@SuppressWarnings({"unused", "raw"})
public BinderChildContextInitializer withChildContextInitializers(
Map> childContextInitializers) {
- this.logger.debug(() -> "Replacing instance w/ one that uses; child context initializers");
+ this.logger.debug(() -> "Replacing instance w/ one that uses child context initializers");
Map> downcastedInitializers =
childContextInitializers.entrySet().stream()
.map(e -> Map.entry(e.getKey(), (ApplicationContextInitializer) e.getValue()))
diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinderFactory.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinderFactory.java
index f0b0ffabb..21f9c7fdf 100644
--- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinderFactory.java
+++ b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinderFactory.java
@@ -104,6 +104,21 @@ public class DefaultBinderFactory implements BinderFactory, DisposableBean, Appl
this.binderCustomizer = binderCustomizer;
}
+ /**
+ * Replaces the existing binder configurations - useful in AOT processing where the binding service properties
+ * have to be manually loaded after the binder factory is constructed.
+ *
+ * @param binderConfigurations the updated configurations
+ */
+ void updateBinderConfigurations(Map binderConfigurations) {
+ this.binderConfigurations.clear();
+ this.binderConfigurations.putAll(binderConfigurations);
+ }
+
+ BinderTypeRegistry getBinderTypeRegistry() {
+ return this.binderTypeRegistry;
+ }
+
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
Assert.isInstanceOf(ConfigurableApplicationContext.class, applicationContext);
@@ -126,17 +141,14 @@ public class DefaultBinderFactory implements BinderFactory, DisposableBean, Appl
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
- public synchronized Binder getBinder(String name,
- Class extends T> bindingTargetType) {
+ public synchronized Binder getBinder(String name, Class extends T> bindingTargetType) {
String binderName = StringUtils.hasText(name) ? name : this.defaultBinder;
- Map binders = this.context == null ? Collections.emptyMap()
- : this.context.getBeansOfType(Binder.class);
+ Map binders = this.context == null ? Collections.emptyMap() : this.context.getBeansOfType(Binder.class);
Binder binder;
if (StringUtils.hasText(binderName) && binders.containsKey(binderName)) {
- binder = (Binder) this.context
- .getBean(binderName);
+ binder = (Binder) this.context.getBean(binderName);
}
else if (binders.size() == 1) {
binder = binders.values().iterator().next();
@@ -149,7 +161,7 @@ public class DefaultBinderFactory implements BinderFactory, DisposableBean, Appl
}
else {
/*
- * This is the fall back to the old bootstrap that relies on spring.binders.
+ * This is the fallback to the old bootstrap that relies on spring.binders.
*/
binder = this.doGetBinder(binderName, bindingTargetType);
}
@@ -160,27 +172,37 @@ public class DefaultBinderFactory implements BinderFactory, DisposableBean, Appl
}
private Binder doGetBinder(String name, Class extends T> bindingTargetType) {
- if (CollectionUtils.isEmpty(this.binderChildContextInitializers)) {
- return this.doGetBinderConventional(name, bindingTargetType);
+ // If child initializers - use AOT lookup
+ if (!CollectionUtils.isEmpty(this.binderChildContextInitializers)) {
+ return this.doGetBinderAOT(name, bindingTargetType);
}
- else {
- if ((!StringUtils.hasText(name) || this.defaultBinder != null) && this.binderChildContextInitializers.size() == 1) {
+ return this.doGetBinderConventional(name, bindingTargetType);
+ }
+
+ private Binder doGetBinderAOT(String name, Class extends T> bindingTargetType) {
+ // If neither name nor default given - return single or fail when > 1
+ if (!StringUtils.hasText(name) && !StringUtils.hasText(this.defaultBinder)) {
+ if (this.binderChildContextInitializers.size() == 1) {
String configurationName = this.binderChildContextInitializers.keySet().iterator().next();
+ this.logger.debug("No specific name or default given - using single available child initializer '" + configurationName + "'");
return this.getBinderInstance(configurationName);
}
- else if (this.defaultBinder != null && this.binderChildContextInitializers.size() > 1) {
- // Handling default binder when different binders are present on the classpath.
- for (String binderName : this.binderChildContextInitializers.keySet()) {
- if (binderName.equals(this.defaultBinder)) {
- return this.getBinderInstance(binderName);
- }
- }
- throw new IllegalStateException("Default binder provided, but can't determine which binder to initialize");
- }
- else {
- throw new IllegalStateException("Can't determine which binder to use: " + name + "/" + this.binderChildContextInitializers.size());
- }
+ throw new IllegalStateException("No specific name or default given - can't determine which binder to use");
}
+
+ // Prefer specific name over default
+ String configurationName = name;
+ if (!StringUtils.hasText(configurationName)) {
+ configurationName = this.defaultBinder;
+ }
+
+ // Check for matching child initializer
+ if (this.binderChildContextInitializers.containsKey(configurationName)) {
+ return this.getBinderInstance(configurationName);
+ }
+
+ throw new IllegalStateException("Requested binder '" + name + "' did not match available binders: " +
+ this.binderChildContextInitializers.keySet());
}
private Binder doGetBinderConventional(String name,
@@ -296,16 +318,19 @@ public class DefaultBinderFactory implements BinderFactory, DisposableBean, Appl
ConfigurableApplicationContext binderProducingContext;
if (this.binderChildContextInitializers.containsKey(configurationName)) {
this.logger.info("Using AOT pre-prepared initializer to construct binder child context for " + configurationName);
+ if (binderConfiguration != null) {
+ this.flatten(null, binderConfiguration.getProperties(), binderProperties);
+ }
binderProducingContext = this.createUnitializedContextForAOT(configurationName, binderProperties, binderConfiguration);
this.binderChildContextInitializers.get(configurationName).initialize(binderProducingContext);
binderProducingContext.refresh();
}
else {
+ this.logger.info("Constructing binder child context for " + configurationName);
Assert.state(binderConfiguration != null, "Unknown binder configuration: " + configurationName);
+ this.flatten(null, binderConfiguration.getProperties(), binderProperties);
BinderType binderType = this.binderTypeRegistry.get(binderConfiguration.getBinderType());
Assert.notNull(binderType, "Binder type " + binderConfiguration.getBinderType() + " is not defined");
- this.flatten(null, binderConfiguration.getProperties(), binderProperties);
- this.logger.info("Constructing binder child context for " + configurationName);
binderProducingContext = this.initializeBinderContextSimple(configurationName, binderProperties,
binderType, binderConfiguration, true);
}
diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingServiceConfiguration.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingServiceConfiguration.java
index 00e37a3dd..56a6f9f0c 100644
--- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingServiceConfiguration.java
+++ b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingServiceConfiguration.java
@@ -90,7 +90,7 @@ public class BindingServiceConfiguration {
@Autowired(required = false)
private Collection binderFactoryListeners;
- private static Map getBinderConfigurations(
+ public static Map getBinderConfigurations(
BinderTypeRegistry binderTypeRegistry,
BindingServiceProperties bindingServiceProperties) {
diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderChildContextInitializerTests.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderChildContextInitializerTests.java
new file mode 100644
index 000000000..76d42a988
--- /dev/null
+++ b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderChildContextInitializerTests.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2022-2023 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.Supplier;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.aot.AotDetector;
+import org.springframework.aot.test.generate.TestGenerationContext;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.context.annotation.UserConfigurations;
+import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.boot.test.system.CapturedOutput;
+import org.springframework.boot.test.system.OutputCaptureExtension;
+import org.springframework.boot.test.util.TestPropertyValues;
+import org.springframework.cloud.stream.config.BinderFactoryAutoConfiguration;
+import org.springframework.cloud.stream.config.BindingServiceConfiguration;
+import org.springframework.cloud.stream.function.FunctionConfiguration;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.aot.ApplicationContextAotGenerator;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.core.convert.support.GenericConversionService;
+import org.springframework.core.log.LogAccessor;
+import org.springframework.core.test.tools.CompileWithForkedClassLoader;
+import org.springframework.core.test.tools.TestCompiler;
+import org.springframework.javapoet.ClassName;
+import org.springframework.messaging.MessageChannel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for the {@link BinderChildContextInitializer}.
+ *
+ * @author Chris Bono
+ */
+@ExtendWith(OutputCaptureExtension.class)
+class BinderChildContextInitializerTests {
+
+ private static final LogAccessor LOG = new LogAccessor(BinderChildContextInitializerTests.class);
+
+ @Test
+ @CompileWithForkedClassLoader
+ void shouldStartDefaultBinderChildContextFromAotContributions(CapturedOutput output) {
+
+ // Test description:
+ // -----------------------
+ // Use context runner to create a boostrap context that we can then pass into AOT processor.
+ // The AOT processor will then generate the ACI for the default binder (no user declared binders).
+ // We then initialize a fresh app context using the generated ACI and verify the expected output.
+
+ ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(BinderFactoryAutoConfiguration.class,
+ BindingServiceConfiguration.class, FunctionConfiguration.class))
+ .withInitializer(new ConfigDataApplicationContextInitializer())
+ .withConfiguration(UserConfigurations.of(TestFooBinderAppConfiguration.class));
+ contextRunner.prepare(context -> {
+ TestGenerationContext generationContext = new TestGenerationContext(TestTarget.class);
+ ClassName className = new ApplicationContextAotGenerator().processAheadOfTime(
+ (GenericApplicationContext) context.getSourceApplicationContext(), generationContext);
+ generationContext.writeGeneratedContent();
+ TestCompiler compiler = TestCompiler.forSystem();
+ compiler.with(generationContext).compile(compiled -> {
+ // Initialize the context w/ the generated ACI
+ GenericApplicationContext freshApplicationContext = new GenericApplicationContext();
+ ApplicationContextInitializer initializer = compiled
+ .getInstance(ApplicationContextInitializer.class, className.toString());
+ initializer.initialize(freshApplicationContext);
+ assertThat(output).contains("Beginning AOT processing for binder child contexts");
+ assertThat(output).contains("Pre-creating binder child context (AOT) for mock");
+ assertThat(output).contains("Generating AOT child context initializer for mock");
+ assertThat(output).contains("Refreshing mock_context");
+
+ // Refresh the initialized context and verify the binder child contexts are used
+ TestPropertyValues.of(AotDetector.AOT_ENABLED + "=true")
+ .applyToSystemProperties(freshApplicationContext::refresh);
+ assertThat(output).contains("Replacing instance w/ one that uses child context initializers");
+ assertThat(output).contains("Setting binder child context initializers on binder factory");
+
+ // Make sure we can get the binders
+ DefaultBinderFactory binderFactory = freshApplicationContext.getBean(DefaultBinderFactory.class);
+ Binder mockBinder = binderFactory.getBinder("mock", MessageChannel.class);
+ assertThat(mockBinder).isNotNull();
+ assertThat(output).contains("Caching the binder: mock");
+
+ // no default or name given - uses single available binder
+ assertThat(binderFactory.getBinder(null, MessageChannel.class)).isSameAs(mockBinder);
+ assertThat(output).contains("No specific name or default given - using single available child initializer 'mock'");
+
+ assertThatIllegalStateException().isThrownBy(
+ () -> binderFactory.getBinder("mockBinder1", MessageChannel.class))
+ .withMessageContaining("Requested binder 'mockBinder1' did not match available binders");
+
+ binderFactory.setDefaultBinder("mock");
+ assertThat(binderFactory.getBinder(null, MessageChannel.class)).isSameAs(mockBinder);
+ });
+ });
+ }
+
+ @Test
+ @CompileWithForkedClassLoader
+ @SuppressWarnings("unchecked")
+ void shouldStartDeclardBinderChildContextsFromAotContributions(CapturedOutput output) {
+
+ // Test description:
+ // -----------------------
+ // Use context runner to create a boostrap context that we can then pass into AOT processor.
+ // The AOT processor will then generate the ACI for each binder child context defined in the application.yml.
+ // We then initialize a fresh app context using the generated ACIs and verify the expected output.
+ ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(BinderFactoryAutoConfiguration.class,
+ BindingServiceConfiguration.class, FunctionConfiguration.class))
+ .withInitializer(new ConfigDataApplicationContextInitializer())
+ .withPropertyValues("spring.config.location=classpath:binder-aot-test/")
+ .withConfiguration(UserConfigurations.of(TestFooBinderAppConfiguration.class));
+
+ contextRunner.prepare(context -> {
+ TestGenerationContext generationContext = new TestGenerationContext(TestTarget.class);
+ ClassName className = new ApplicationContextAotGenerator().processAheadOfTime(
+ (GenericApplicationContext) context.getSourceApplicationContext(), generationContext);
+ generationContext.writeGeneratedContent();
+ TestCompiler compiler = TestCompiler.forSystem();
+ compiler.with(generationContext).compile(compiled -> {
+ // Initialize the context w/ the generated ACI
+ GenericApplicationContext freshApplicationContext = new GenericApplicationContext();
+ ApplicationContextInitializer initializer = compiled
+ .getInstance(ApplicationContextInitializer.class, className.toString());
+ initializer.initialize(freshApplicationContext);
+
+ assertThat(output).contains("Beginning AOT processing for binder child contexts");
+ assertThat(output).contains("Pre-creating binder child context (AOT) for mockBinder2");
+ assertThat(output).contains("Pre-creating binder child context (AOT) for mockBinder1");
+ assertThat(output).contains("Generating AOT child context initializer for mockBinder2");
+ assertThat(output).contains("Refreshing mockBinder2_context");
+ assertThat(output).contains("Generating AOT child context initializer for mockBinder1");
+ assertThat(output).contains("Refreshing mockBinder1_context");
+
+ // Refresh the initialized context and verify the binder child contexts are used
+ TestPropertyValues.of(AotDetector.AOT_ENABLED + "=true")
+ .applyToSystemProperties(freshApplicationContext::refresh);
+
+ assertThat(output).contains("Replacing instance w/ one that uses child context initializers");
+ assertThat(output).contains("Setting binder child context initializers on binder factory");
+
+ // Make sure we can get the binders
+ DefaultBinderFactory binderFactory = freshApplicationContext.getBean(DefaultBinderFactory.class);
+ assertThat(binderFactory.getBinder("mockBinder1", MessageChannel.class)).isNotNull();
+ assertThat(output).contains("Caching the binder: mockBinder1");
+
+ Binder mockBinder2 = binderFactory.getBinder("mockBinder2", MessageChannel.class);
+ assertThat(mockBinder2).isNotNull();
+ assertThat(output).contains("Caching the binder: mockBinder2");
+
+ assertThatIllegalStateException().isThrownBy(
+ () -> binderFactory.getBinder("mockBinder3", MessageChannel.class))
+ .withMessageContaining("Requested binder 'mockBinder3' did not match available binders");
+
+ assertThatIllegalStateException().isThrownBy(
+ () -> binderFactory.getBinder(null, MessageChannel.class))
+ .withMessageContaining("No specific name or default given - can't determine which binder to use");
+
+ binderFactory.setDefaultBinder("mockBinder2");
+ assertThat(binderFactory.getBinder(null, MessageChannel.class)).isSameAs(mockBinder2);
+ });
+ });
+ }
+
+ static class TestTarget {
+ }
+
+ @EnableAutoConfiguration
+ @Configuration(proxyBeanMethods = false)
+ static class TestFooBinderAppConfiguration {
+
+ @Bean
+ GenericConversionService integrationConversionService() {
+ return mock(GenericConversionService.class);
+ }
+
+ @Bean
+ Supplier fooSource() {
+ return () -> "foo-" + System.currentTimeMillis();
+ }
+
+ @Bean
+ Consumer fooSink() {
+ return (foo) -> LOG.info("*** FOO: " + foo);
+ }
+ }
+
+}
diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/DefaultBinderFactoryTests.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/DefaultBinderFactoryTests.java
new file mode 100644
index 000000000..d6edc2328
--- /dev/null
+++ b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/DefaultBinderFactoryTests.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023-2023 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.HashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link DefaultBinderFactory}
+ *
+ * @author Chris Bono
+ */
+class DefaultBinderFactoryTests {
+
+ @Test
+ void updateBinderConfigurations() {
+ Map binderConfigs = new HashMap<>();
+ binderConfigs.put("foo", mock(BinderConfiguration.class));
+ DefaultBinderFactory binderFactory = new DefaultBinderFactory(binderConfigs, null, null);
+
+ Map newBinderConfigs = new HashMap<>();
+ newBinderConfigs.put("bar", mock(BinderConfiguration.class));
+ binderFactory.updateBinderConfigurations(newBinderConfigs);
+
+ assertThat(binderFactory.getBinderConfigurations()).containsExactlyInAnyOrderEntriesOf(newBinderConfigs);
+ }
+
+}
diff --git a/core/spring-cloud-stream/src/test/resources/binder-aot-test/application.yml b/core/spring-cloud-stream/src/test/resources/binder-aot-test/application.yml
new file mode 100644
index 000000000..c85248c56
--- /dev/null
+++ b/core/spring-cloud-stream/src/test/resources/binder-aot-test/application.yml
@@ -0,0 +1,21 @@
+spring.cloud:
+ function:
+ definition: fooSource;fooSink
+ stream:
+ default-binder: mockBinder1
+ binders:
+ mockBinder1:
+ type: mock
+ environment:
+ foo: bar1
+ mockBinder2:
+ type: mock
+ environment:
+ foo: bar2
+ bindings:
+ fooSource-out-0:
+ destination: fooSink-in-0
+ binder: mockBinder2
+ fooSink-in-0:
+ destination: fooSource-out-0
+ binder: mockBinder2