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