diff --git a/spring-cloud-stream-module-launcher/README.md b/spring-cloud-stream-module-launcher/README.md
index a6aa29ff1..1f0bd24a8 100644
--- a/spring-cloud-stream-module-launcher/README.md
+++ b/spring-cloud-stream-module-launcher/README.md
@@ -23,10 +23,11 @@ From the `spring-cloud-stream/spring-cloud-stream-module-launcher` directory:
````
java -Dmodules=org.springframework.cloud.stream.module:time-source:1.0.0.BUILD-SNAPSHOT -Dspring.cloud.stream.bindings.output=ticktock -jar target/spring-cloud-stream-module-launcher-1.0.0.BUILD-SNAPSHOT.jar
-java -Dmodules=org.springframework.cloud.stream.module:log-sink:1.0.0.BUILD-SNAPSHOT -Dserver.port=8081 -Dspring.cloud.stream.bindings.input=ticktock -jar target/spring-cloud-stream-module-launcher-1.0.0.BUILD-SNAPSHOT.jar
+java -Dmodules=org.springframework.cloud.stream.module:log-sink:1.0.0.BUILD-SNAPSHOT -Dargs.0.server.port=8081 -Dspring.cloud.stream.bindings.input=ticktock -jar target/spring-cloud-stream-module-launcher-1.0.0.BUILD-SNAPSHOT.jar
````
Note that `server.port` needs to be specified explicitly for the log sink module as the time source module already uses the default port `8080`.
+The module launcher is able to launch several modules, hence the `args.0.` prefix.
The binding property is set to use the same name `ticktock` for both the output/input bindings of source/sink modules so that the log sink receives messages from the time source.
The time messages will be emitted every second. The console for the log module will display each:
diff --git a/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLaunchRequest.java b/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLaunchRequest.java
new file mode 100644
index 000000000..532caa578
--- /dev/null
+++ b/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLaunchRequest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2015 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.module.launcher;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.util.Assert;
+
+/**
+ * Encapsulates a reference to a module (as maven coordinates) and a set of "arguments" that must be passed to it.
+ * Those arguments will eventually be passed to the module by the launcher, using any way appropriate.
+ *
+ * @author Eric Bottard
+ */
+public class ModuleLaunchRequest {
+
+ private final String module;
+
+ private final Map arguments;
+
+ public ModuleLaunchRequest(String module, Map arguments) {
+ this.module = module;
+ this.arguments = arguments != null ? new HashMap<>(arguments) : new HashMap();
+ }
+
+ public String getModule() {
+ return module;
+ }
+
+ public Map getArguments() {
+ return arguments;
+ }
+
+ public void addArgument(String name, String value) {
+ this.arguments.put(name, value);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s with arguments %s", module, arguments);
+ }
+}
diff --git a/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncher.java b/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncher.java
index a705364d0..6d868515e 100644
--- a/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncher.java
+++ b/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncher.java
@@ -18,7 +18,9 @@ package org.springframework.cloud.stream.module.launcher;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -36,6 +38,7 @@ import org.springframework.util.StringUtils;
* @author Mark Fisher
* @author Ilayaperumal Gopinathan
* @author Marius Bogoevici
+ * @author Eric Bottard
*/
public class ModuleLauncher {
@@ -59,28 +62,36 @@ public class ModuleLauncher {
/**
* Launches one or more modules, with the corresponding arguments, if any.
*
+ * Modules must be passed in "natural" left to right order.
+ *
* The format of each module must conform to the Aether convention:
* <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>
*
- * To pass arguments to a module, prefix with the module name and a dot. The arg name will be de-qualified and passed along.
- * For example: --org.springframework.cloud.stream.module:time-source:1.0.0.BUILD-SNAPSHOT.bar=123 becomes --bar=123 and is only passed to the 'org.springframework.cloud.stream.module:time-source:1.0.0.BUILD-SNAPSHOT' module.
- *
- * @param modules a list of modules
- * @param args a list of arguments, prefixed with the module name
+ * @param moduleLaunchRequests a list of modules with their (unqualified) arguments
*/
- public void launch(String[] modules, String[] args) {
- for (String module : modules) {
- List moduleArgs = new ArrayList<>();
- for (String arg : args) {
- if (arg.startsWith("--" + module + ".")) {
- moduleArgs.add("--" + arg.substring(module.length() + 3));
- }
- }
- moduleArgs.add("--spring.jmx.default-domain=" + module.replace("/", ".").replace(":", "."));
- launchModule(module, moduleArgs.toArray(new String[moduleArgs.size()]));
+ public void launch(List moduleLaunchRequests) {
+ List reversed = new ArrayList<>(moduleLaunchRequests);
+ Collections.reverse(reversed);
+ for (ModuleLaunchRequest moduleLaunchRequest : reversed) {
+ String module = moduleLaunchRequest.getModule();
+ moduleLaunchRequest.addArgument("spring.jmx.default-domain", module.replace("/", ".").replace(":", "."));
+ launchModule(module, toArgArray(moduleLaunchRequest.getArguments()));
}
}
+ /**
+ * Converts a set of semantic program arguments to "command line program arguments" that is, to the
+ * {@literal --foo=bar} form.
+ */
+ private String[] toArgArray(Map args) {
+ String[] result = new String[args.size()];
+ int i = 0;
+ for (Map.Entry kv : args.entrySet()) {
+ result[i++] = String.format("--%s=%s", kv.getKey(), kv.getValue());
+ }
+ return result;
+ }
+
private void launchModule(String module, String[] args) {
try {
Resource resource = resolveModule(module);
diff --git a/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncherApplication.java b/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncherApplication.java
index fb98c1173..770f763db 100644
--- a/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncherApplication.java
+++ b/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncherApplication.java
@@ -24,7 +24,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
* or "MODULES" environment variable as a comma-delimited list, with the arguments
* provided at launch.
*
- * @see ModuleLauncher#launch(String[], String[]) for module and argument structure and
+ * @see ModuleLauncherProperties for module and argument structure and
* format
*
* @author Marius Bogoevici
diff --git a/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncherConfiguration.java b/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncherConfiguration.java
index 9449cde18..5b921ac59 100644
--- a/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncherConfiguration.java
+++ b/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncherConfiguration.java
@@ -33,6 +33,7 @@ import org.springframework.context.annotation.Configuration;
*
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
+ * @author Eric Bottard
*/
@Configuration
@EnableConfigurationProperties(ModuleResolverProperties.class)
diff --git a/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncherProperties.java b/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncherProperties.java
index 7b80de024..c7ee6b893 100644
--- a/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncherProperties.java
+++ b/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncherProperties.java
@@ -16,24 +16,50 @@
package org.springframework.cloud.stream.module.launcher;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.validation.constraints.AssertFalse;
+
import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.core.env.Environment;
/**
* Configuration properties for {@link ModuleLauncher}.
*
+ * Expects the following keys (resolved from the {@link Environment}, so this could take many forms _ System
+ * properties, environment variables, program arguments, etc. _):
+ * - {@literal modules =
}: an ordered list of maven coordinates of modules to launch
+ * - {@literal args[][] = }: key/value pairs that will become module arguments,
+ * where {@literal } is the 0-based index of the module in the list above
+ *
+ *
+ * As an example, this is how one would launch {@literal time --fixedDelay=4 | log} canonical example:
+ *
+ * modules = org.springframework.cloud.modules:time-source:1.0.0-SNAPSHOT,org.springframework.cloud.modules:log-sink:1.0.0-SNAPSHOT
+ * args.0.fixedDelay=4
+ *
+ *
+ *
* @author Ilayaperumal Gopinathan
* @author Marius Bogoevici
+ * @author Eric Bottard
*/
@ConfigurationProperties
public class ModuleLauncherProperties {
/**
- * Array of modules that need to be launched.
+ * Array of coordinates for modules that need to be launched.
*/
private String[] modules;
+ /**
+ * Map of arguments, keyed by the 0-based index in the {@kink #modules array}.
+ */
+ private Map> args = new HashMap<>();
+
public void setModules(String[] modules) {
this.modules = modules;
}
@@ -43,4 +69,12 @@ public class ModuleLauncherProperties {
return modules;
}
+ public void setArgs(Map> args) {
+ this.args = args;
+ }
+
+ public Map> getArgs() {
+ return args;
+ }
+
}
diff --git a/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncherRunner.java b/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncherRunner.java
index 09f2ce288..347a5b264 100644
--- a/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncherRunner.java
+++ b/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/launcher/ModuleLauncherRunner.java
@@ -16,15 +16,18 @@
package org.springframework.cloud.stream.module.launcher;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
+import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
-import org.springframework.util.StringUtils;
/**
* Spring boot {@link ApplicationRunner} that triggers {@link ModuleLauncher} to launch the modules.
@@ -34,7 +37,7 @@ import org.springframework.util.StringUtils;
*/
@Component
@EnableConfigurationProperties(ModuleLauncherProperties.class)
-public class ModuleLauncherRunner implements ApplicationRunner {
+public class ModuleLauncherRunner implements CommandLineRunner {
private final static Log log = LogFactory.getLog(ModuleLauncherRunner.class);
@@ -45,15 +48,27 @@ public class ModuleLauncherRunner implements ApplicationRunner {
private ModuleLauncher moduleLauncher;
@Override
- public void run(ApplicationArguments applicationArguments) throws Exception {
- String[] launchedModules = moduleLauncherProperties.getModules();
+ public void run(String... args) throws Exception {
+ List launchRequests = toModuleLaunchRequests(moduleLauncherProperties);
if (log.isInfoEnabled()) {
- log.info("Launching: "
- + StringUtils.arrayToCommaDelimitedString(launchedModules)
- + " with arguments: "
- + StringUtils.arrayToCommaDelimitedString(applicationArguments
- .getSourceArgs()));
+ StringBuilder sb = new StringBuilder("Launching\n");
+ for (ModuleLaunchRequest moduleLaunchRequest : launchRequests) {
+ sb.append('\t').append(moduleLaunchRequest).append('\n');
+ }
+ log.info(sb.toString());
}
- this.moduleLauncher.launch(launchedModules, applicationArguments.getSourceArgs());
+ this.moduleLauncher.launch(launchRequests);
+ }
+
+ private List toModuleLaunchRequests(ModuleLauncherProperties moduleLauncherProperties) {
+ List requests = new ArrayList<>();
+ String[] modules = moduleLauncherProperties.getModules();
+ Map> arguments = moduleLauncherProperties.getArgs();
+ for (int i = 0; i < modules.length; i++) {
+ ModuleLaunchRequest moduleLaunchRequest = new ModuleLaunchRequest(modules[i], arguments.get(i));
+ requests.add(moduleLaunchRequest);
+ }
+ return requests;
+
}
}