diff --git a/spring-cloud-stream/pom.xml b/spring-cloud-stream/pom.xml
index 73a11c2ea..f4637e97b 100644
--- a/spring-cloud-stream/pom.xml
+++ b/spring-cloud-stream/pom.xml
@@ -34,6 +34,10 @@
org.springframework.integration
spring-integration-core
+
+ org.springframework.integration
+ spring-integration-jmx
+
org.springframework
spring-tuple
diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/AggregateApplication.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/AggregateApplication.java
index 0927a78bd..e56e42e39 100644
--- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/AggregateApplication.java
+++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/AggregateApplication.java
@@ -27,6 +27,7 @@ import org.springframework.messaging.SubscribableChannel;
/**
* Class that is responsible for embedding apps using shared channel registry.
+ *
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
* @author Venil Noronha
@@ -35,45 +36,35 @@ abstract class AggregateApplication {
private static final String SPRING_CLOUD_STREAM_INTERNAL_PREFIX = "spring.cloud.stream.internal";
- private static final String CHANNEL_NAMESPACE_PROPERTY_NAME = SPRING_CLOUD_STREAM_INTERNAL_PREFIX + ".channelNamespace";
+ private static final String CHANNEL_NAMESPACE_PROPERTY_NAME =
+ SPRING_CLOUD_STREAM_INTERNAL_PREFIX + ".channelNamespace";
- private static final String SELF_CONTAINED_APP_PROPERTY_NAME = SPRING_CLOUD_STREAM_INTERNAL_PREFIX + ".selfContained";
+ private static final String SELF_CONTAINED_APP_PROPERTY_NAME =
+ SPRING_CLOUD_STREAM_INTERNAL_PREFIX + ".selfContained";
public static final String INPUT_CHANNEL_NAME = "input";
public static final String OUTPUT_CHANNEL_NAME = "output";
- static ConfigurableApplicationContext createParentContext(Object[] sources, String[] args, boolean selfContained) {
+ static ConfigurableApplicationContext createParentContext(Object[] sources, String[] args, final
+ boolean selfContained, boolean webEnvironment,
+ boolean headless) {
SpringApplicationBuilder aggregatorParentConfiguration = new SpringApplicationBuilder();
aggregatorParentConfiguration
- .sources(AggregatorParentConfiguration.class)
.sources(sources)
- .web(false)
- .headless(true)
+ .web(webEnvironment)
+ .headless(headless)
.properties("spring.jmx.default-domain="
- + AggregatorParentConfiguration.class.getName(),
+ + AggregateApplicationBuilder.ParentConfiguration.class.getName(),
SELF_CONTAINED_APP_PROPERTY_NAME + "=" + selfContained);
return aggregatorParentConfiguration.run(args);
}
- static ConfigurableApplicationContext createParentContext(ConfigurableApplicationContext parentContext, String[] args,
- boolean selfContained) {
- SpringApplicationBuilder aggregatorParentConfiguration = new SpringApplicationBuilder();
- aggregatorParentConfiguration
- .sources(AggregatorParentConfiguration.class)
- .web(false)
- .headless(true)
- .properties("spring.jmx.default-domain="
- + AggregatorParentConfiguration.class.getName(),
- SELF_CONTAINED_APP_PROPERTY_NAME + "=" + selfContained)
- .parent(parentContext);
- return aggregatorParentConfiguration.run(args);
- }
-
- static String getNamespace(String appClassName, int index) {
+ static String getDefaultNamespace(String appClassName, int index) {
return appClassName + "_" + index;
}
+
protected static SpringApplicationBuilder embedApp(
ConfigurableApplicationContext parentContext, String namespace,
Class> app) {
@@ -81,7 +72,7 @@ abstract class AggregateApplication {
.web(false)
.main(app)
.bannerMode(Mode.OFF)
- .properties("spring.jmx.default-domain=" + app)
+ .properties("spring.jmx.default-domain=" + namespace)
.properties(CHANNEL_NAMESPACE_PROPERTY_NAME + "=" + namespace)
.registerShutdownHook(false)
.parent(parentContext);
diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/AggregateApplicationBuilder.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/AggregateApplicationBuilder.java
index 8e6e82782..09ea7047d 100644
--- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/AggregateApplicationBuilder.java
+++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/AggregateApplicationBuilder.java
@@ -21,9 +21,15 @@ import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
+import org.springframework.boot.actuate.endpoint.MetricReaderPublicMetrics;
+import org.springframework.boot.actuate.endpoint.MetricsEndpoint;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.integration.monitor.IntegrationMBeanExporter;
import org.springframework.util.StringUtils;
/**
@@ -50,12 +56,16 @@ public class AggregateApplicationBuilder {
private List parentArgs = new ArrayList<>();
+ private boolean headless = true;
+
+ private boolean webEnvironment = true;
+
public AggregateApplicationBuilder(String... args) {
- this(new Object[]{ParentConfiguration.class}, args);
+ this(new Object[]{ ParentConfiguration.class }, args);
}
public AggregateApplicationBuilder(Object source, String... args) {
- this(new Object[]{source}, args);
+ this(new Object[]{ source }, args);
}
public AggregateApplicationBuilder(Object[] sources, String[] args) {
@@ -75,7 +85,7 @@ public class AggregateApplicationBuilder {
}
public AggregateApplicationBuilder parent(Object source, String... args) {
- return parent(new Object[]{source}, args);
+ return parent(new Object[]{ source }, args);
}
public AggregateApplicationBuilder parent(Object[] sources, String[] args) {
@@ -84,6 +94,30 @@ public class AggregateApplicationBuilder {
return this;
}
+ /**
+ * Flag to explicitly request a web or non-web environment.
+ *
+ * @param webEnvironment true if the application has a web environment
+ * @return the AggregateApplicationBuilder being constructed
+ * @see SpringApplicationBuilder#web(boolean)
+ */
+ public AggregateApplicationBuilder web(boolean webEnvironment) {
+ this.webEnvironment = webEnvironment;
+ return this;
+ }
+
+ /**
+ * Configures the headless attribute of the build application.
+ *
+ * @param headless true if the application is headless
+ * @return the AggregateApplicationBuilder being constructed
+ * @see SpringApplicationBuilder#headless(boolean)
+ */
+ public AggregateApplicationBuilder headless(boolean headless) {
+ this.headless = headless;
+ return this;
+ }
+
public SourceConfigurer from(Class> app) {
SourceConfigurer sourceConfigurer = new SourceConfigurer(app);
this.sourceConfigurer = sourceConfigurer;
@@ -110,12 +144,13 @@ public class AggregateApplicationBuilder {
Class> appToEmbed = appConfigurer.getApp();
// Always update namespace before preparing SharedChannelRegistry
if (appConfigurer.namespace == null) {
- appConfigurer.namespace = AggregateApplication.getNamespace(appConfigurer.getApp().getName(), i);
+ appConfigurer.namespace = AggregateApplication.getDefaultNamespace(appConfigurer.getApp().getName(),
+ i);
}
appsToEmbed.put(appToEmbed, appConfigurer.namespace);
}
this.parentContext = AggregateApplication.createParentContext(this.parentSources.toArray(new Object[0]),
- this.parentArgs.toArray(new String[0]), areAppsSelfContained());
+ this.parentArgs.toArray(new String[0]), selfContained(), this.webEnvironment, this.headless);
SharedChannelRegistry sharedChannelRegistry = this.parentContext.getBean(SharedChannelRegistry.class);
AggregateApplication.prepareSharedChannelRegistry(sharedChannelRegistry, appsToEmbed);
for (int i = apps.size() - 1; i >= 0; i--) {
@@ -125,7 +160,7 @@ public class AggregateApplicationBuilder {
return this.parentContext;
}
- private boolean areAppsSelfContained() {
+ private boolean selfContained() {
return (this.sourceConfigurer != null) && (this.sinkConfigurer != null);
}
@@ -225,9 +260,15 @@ public class AggregateApplicationBuilder {
}
void embed() {
- childContext(this.app, AggregateApplicationBuilder.this.parentContext,
- this.namespace).args(this.args).config(this.names)
- .profiles(this.profiles).run();
+ final ConfigurableApplicationContext childContext =
+ childContext(this.app, AggregateApplicationBuilder.this.parentContext,
+ this.namespace).args(this.args).config(this.names)
+ .profiles(this.profiles).run();
+ AggregateApplicationBuilder.this.parentContext.getBeanFactory().getBean(
+ MetricsEndpoint.class).registerPublicMetrics(
+ new MetricReaderPublicMetrics(
+ new NamespaceAwareSpringIntegrationMetricReader(this.namespace, childContext.getBean(
+ IntegrationMBeanExporter.class))));
}
}
@@ -262,7 +303,7 @@ public class AggregateApplicationBuilder {
return this;
}
- public void run() {
+ public ConfigurableApplicationContext run() {
List args = new ArrayList();
if (this.args != null) {
args.addAll(Arrays.asList(this.args));
@@ -270,13 +311,18 @@ public class AggregateApplicationBuilder {
if (this.configName != null) {
args.add("--spring.config.name=" + this.configName);
}
- this.builder.run(args.toArray(new String[0]));
+ return this.builder.run(args.toArray(new String[0]));
}
}
@EnableAutoConfiguration
+ @EnableBinding
public static class ParentConfiguration {
+ @Bean
+ @ConditionalOnMissingBean(SharedChannelRegistry.class)
+ public SharedChannelRegistry sharedChannelRegistry() {
+ return new SharedChannelRegistry();
+ }
}
-
}
diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/AggregatorParentConfiguration.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/AggregatorParentConfiguration.java
deleted file mode 100644
index 268cb17dd..000000000
--- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/AggregatorParentConfiguration.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright 2015-2016 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
- *
- * http://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.aggregate;
-
-import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
-import org.springframework.cloud.stream.annotation.EnableBinding;
-import org.springframework.context.annotation.Bean;
-
-/**
- * Basic configuration for an aggregator application parent.
- *
- * @author Marius Bogoevici
- */
-@EnableAutoConfiguration
-@EnableBinding
-public class AggregatorParentConfiguration {
-
- @Bean
- @ConditionalOnMissingBean(SharedChannelRegistry.class)
- public SharedChannelRegistry sharedChannelRegistry() {
- return new SharedChannelRegistry();
- }
-}
diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/NamespaceAwareSpringIntegrationMetricReader.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/NamespaceAwareSpringIntegrationMetricReader.java
new file mode 100644
index 000000000..2982c3b9f
--- /dev/null
+++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/NamespaceAwareSpringIntegrationMetricReader.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016 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
+ *
+ * http://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.aggregate;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.springframework.boot.actuate.metrics.Metric;
+import org.springframework.boot.actuate.metrics.reader.MetricReader;
+import org.springframework.integration.monitor.IntegrationMBeanExporter;
+import org.springframework.integration.support.management.Statistics;
+import org.springframework.util.Assert;
+
+/**
+ * A customized version of {@link org.springframework.boot.actuate.metrics.integration.SpringIntegrationMetricReader} that
+ * provides support for customizing channels with a namespace prefix.
+ *
+ * @author Marius Bogoevici
+ * @see org.springframework.boot.actuate.metrics.integration.SpringIntegrationMetricReader for original implementation
+ */
+public class NamespaceAwareSpringIntegrationMetricReader implements MetricReader {
+
+ private final String namespace;
+
+ private final IntegrationMBeanExporter exporter;
+
+ public NamespaceAwareSpringIntegrationMetricReader(String namespace, IntegrationMBeanExporter exporter) {
+ Assert.hasText(namespace, "cannot be null or empty String");
+ Assert.notNull(exporter, "cannot be null");
+ this.namespace = namespace;
+ this.exporter = exporter;
+ }
+
+ @Override
+ public Metric> findOne(String metricName) {
+ return null;
+ }
+
+ @Override
+ public Iterable> findAll() {
+ IntegrationMBeanExporter exporter = this.exporter;
+ List> metrics = new ArrayList>();
+ for (String name : exporter.getChannelNames()) {
+ String prefix = "integration.channel." + namespace + "." + name;
+ metrics.addAll(getStatistics(prefix + ".errorRate",
+ exporter.getChannelErrorRate(name)));
+ metrics.add(new Metric(prefix + ".sendCount",
+ exporter.getChannelSendCountLong(name)));
+ metrics.addAll(getStatistics(prefix + ".sendRate",
+ exporter.getChannelSendRate(name)));
+ metrics.add(new Metric(prefix + ".receiveCount",
+ exporter.getChannelReceiveCountLong(name)));
+ }
+ for (String name : exporter.getHandlerNames()) {
+ metrics.addAll(getStatistics("integration." + namespace + ".handler." + name + ".duration",
+ exporter.getHandlerDuration(name)));
+ }
+ metrics.add(new Metric("integration." + namespace + ".activeHandlerCount",
+ exporter.getActiveHandlerCount()));
+ metrics.add(new Metric("integration." + namespace + ".handlerCount",
+ exporter.getHandlerCount()));
+ metrics.add(new Metric("integration." + namespace + ".channelCount",
+ exporter.getChannelCount()));
+ metrics.add(new Metric("integration." + namespace + ".queuedMessageCount",
+ exporter.getQueuedMessageCount()));
+ return metrics;
+ }
+
+ private Collection extends Metric>> getStatistics(String name,
+ Statistics statistic) {
+ List> metrics = new ArrayList>();
+ metrics.add(new Metric(name + ".mean", statistic.getMean()));
+ metrics.add(new Metric(name + ".max", statistic.getMax()));
+ metrics.add(new Metric(name + ".min", statistic.getMin()));
+ metrics.add(
+ new Metric(name + ".stdev", statistic.getStandardDeviation()));
+ metrics.add(new Metric(name + ".count", statistic.getCountLong()));
+ return metrics;
+ }
+
+ @Override
+ public long count() {
+ int totalChannelCount = this.exporter.getChannelCount() * 11;
+ int totalHandlerCount = this.exporter.getHandlerCount() * 5;
+ return totalChannelCount + totalHandlerCount + 4;
+ }
+
+}
+
diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/SharedChannelRegistry.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/SharedChannelRegistry.java
index 210b43f91..2dc52ae00 100644
--- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/SharedChannelRegistry.java
+++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/aggregate/SharedChannelRegistry.java
@@ -38,10 +38,10 @@ public class SharedChannelRegistry {
}
public void register(String id, MessageChannel messageChannel) {
- sharedChannels.put(id, messageChannel);
+ this.sharedChannels.put(id, messageChannel);
}
public Map getAll() {
- return Collections.unmodifiableMap(sharedChannels);
+ return Collections.unmodifiableMap(this.sharedChannels);
}
}
diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/aggregation/ModuleAggregationTest.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/aggregation/ModuleAggregationTest.java
index adf3755b7..8164f62f9 100644
--- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/aggregation/ModuleAggregationTest.java
+++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/aggregation/ModuleAggregationTest.java
@@ -16,13 +16,12 @@
package org.springframework.cloud.stream.aggregation;
-import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
-import org.junit.Assert;
import org.junit.Test;
+import org.springframework.beans.DirectFieldAccessor;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.aggregate.AggregateApplicationBuilder;
import org.springframework.cloud.stream.aggregate.SharedChannelRegistry;
@@ -34,7 +33,6 @@ import org.springframework.cloud.stream.utils.MockBinderRegistryConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.MessageChannel;
-import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -65,24 +63,22 @@ public class ModuleAggregationTest {
argsToVerify.add("--foo1=bar1");
argsToVerify.add("--foo2=bar2");
argsToVerify.add("--foo3=bar3");
+ argsToVerify.add("--server.port=0");
AggregateApplicationBuilder aggregateApplicationBuilder =
new AggregateApplicationBuilder(MockBinderRegistryConfiguration.class,
"--foo1=bar1");
- aggregateApplicationBuilder.parent(DummyConfig.class, "--foo2=bar2")
- .from(TestSource.class)
- .namespace("foo").to(TestProcessor.class).namespace("bar")
- .run("--foo3=bar3");
- Field parentArgsField = ReflectionUtils.findField(AggregateApplicationBuilder.class,"parentArgs", List.class);
- ReflectionUtils.makeAccessible(parentArgsField);
- Field parentSourcesField = ReflectionUtils.findField(AggregateApplicationBuilder.class,"parentSources", List.class);
- ReflectionUtils.makeAccessible(parentSourcesField);
- String args = ReflectionUtils.getField(parentArgsField, aggregateApplicationBuilder).toString();
- Assert.assertEquals(args, argsToVerify.toString());
- List