From 582239b03bdf8f041f9e13bb2fa0628081e51860 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 8 Jul 2015 23:20:47 -0700 Subject: [PATCH] Add ApplicationArguments and ApplicationRunner Add ApplicationArguments interface which allows SpringApplication.run arguments to be injected into any bean. The interface provides access to both the raw String[] arguments and also provides some convenience methods to access the parsed 'option' and 'non-option' arguments. A new ApplicationRunner interface has also been added which is similar to the existing CommandLineRunner. Fixes gh-1990 --- .../main/asciidoc/spring-boot-features.adoc | 57 +++++++++-- .../boot/ApplicationArguments.java | 74 +++++++++++++++ .../boot/ApplicationRunner.java | 41 ++++++++ .../boot/CommandLineRunner.java | 6 +- .../boot/DefaultApplicationArguments.java | 90 ++++++++++++++++++ .../boot/SpringApplication.java | 75 +++++++++++---- .../DefaultApplicationArgumentsTests.java | 95 +++++++++++++++++++ .../boot/SpringApplicationTests.java | 73 +++++++++++--- 8 files changed, 468 insertions(+), 43 deletions(-) create mode 100644 spring-boot/src/main/java/org/springframework/boot/ApplicationArguments.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/ApplicationRunner.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/DefaultApplicationArguments.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/DefaultApplicationArgumentsTests.java diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 89227460662..f46953fcd84 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -184,12 +184,49 @@ TIP: It is often desirable to call `setWebEnvironment(false)` when using +[[boot-features-application-arguments]] +=== Accessing application arguments +If you need to access the application arguments that were passed to +`SpringApplication.run(...)` you can inject a +`org.springframework.boot.ApplicationArguments` bean. The `ApplicationArguments` interface +provides access to both the raw `String[]` arguments as well as parsed `option` and +`non-option` arguments: + +[source,java,indent=0] +---- + import org.springframework.boot.* + import org.springframework.beans.factory.annotation.* + import org.springframework.stereotype.* + + @Component + public class MyBean { + + @Autowired + public MyBean(ApplicationArguments args) { + boolean debug = args.containsOption("debug"); + List files = args.getNonOptionArgs(); + // if run with "--debug logfile.txt" debug=true, files=["logfile.txt"] + } + + } +---- + +TIP: Spring Boot will also register a `CommandLinePropertySource` with the Spring +`Environment`. This allows you to also inject single application arguments using the +`@Value` annotation. + + + [[boot-features-command-line-runner]] -=== Using the CommandLineRunner -If you want access to the raw command line arguments, or you need to run some specific -code once the `SpringApplication` has started you can implement the `CommandLineRunner` -interface. The `run(String... args)` method will be called on all Spring beans -implementing this interface. +=== Using the ApplicationRunner or CommandLineRunner +If you need to run some specific code once the `SpringApplication` has started, you can +implement the `ApplicationRunner` or `CommandLineRunner` interfaces. Both interfaces work +in the same way and offer a single `run` method which will be called just before +`SpringApplication.run(...)` completes. + +The `CommandLineRunner` interfaces provides access to application arguments as a simple +string array, whereas the `ApplicationRunner` uses the `ApplicationArguments` interface +discussed above. [source,java,indent=0] ---- @@ -199,16 +236,16 @@ implementing this interface. @Component public class MyBean implements CommandLineRunner { - public void run(String... args) { - // Do something... - } + public void run(String... args) { + // Do something... + } } ---- You can additionally implement the `org.springframework.core.Ordered` interface or use the -`org.springframework.core.annotation.Order` annotation if several `CommandLineRunner` -beans are defined that must be called in a specific order. +`org.springframework.core.annotation.Order` annotation if several `CommandLineRunner` or +`ApplicationRunner` beans are defined that must be called in a specific order. diff --git a/spring-boot/src/main/java/org/springframework/boot/ApplicationArguments.java b/spring-boot/src/main/java/org/springframework/boot/ApplicationArguments.java new file mode 100644 index 00000000000..7985e278de3 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/ApplicationArguments.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-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.boot; + +import java.util.List; +import java.util.Set; + +/** + * Provides access to the arguments that were used run a {@link SpringApplication}. + * + * @author Phillip Webb + * @since 1.3.0 + */ +public interface ApplicationArguments { + + /** + * Return the raw unprocessed arguments that were passed to the application. + * @return the arguments + */ + public String[] getSourceArgs(); + + /** + * Return then names of all option arguments. For example, if the arguments were + * "--foo=bar --debug" would return the values {@code ["foo", "bar"]}. + * @return the option names or an empty set + */ + public Set getOptionNames(); + + /** + * Return whether the set of option arguments parsed from the arguments contains an + * option with the given name. + * @param name the name to check + * @return {@code true} if the arguments contains an option with the given name + */ + boolean containsOption(String name); + + /** + * Return the collection of values associated with the arguments option having the + * given name. + *
    + *
  • if the option is present and has no argument (e.g.: "--foo"), return an empty + * collection ({@code []})
  • + *
  • if the option is present and has a single value (e.g. "--foo=bar"), return a + * collection having one element ({@code ["bar"]})
  • + *
  • if the option is present and has multiple values (e.g. "--foo=bar --foo=baz"), + * return a collection having elements for each value ({@code ["bar", "baz"]})
  • + *
  • if the option is not present, return {@code null}
  • + *
+ * @param name the name of the option + * @return a list of option values for the given name + */ + List getOptionValues(String name); + + /** + * Return the collection of non-option arguments parsed. + * @return the non-option arguments or an empty list + */ + List getNonOptionArgs(); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/ApplicationRunner.java b/spring-boot/src/main/java/org/springframework/boot/ApplicationRunner.java new file mode 100644 index 00000000000..f87abaadb80 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/ApplicationRunner.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-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.boot; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +/** + * Interface used to indicate that a bean should run when it is contained within + * a {@link SpringApplication}. Multiple {@link ApplicationArguments} beans can be defined + * within the same application context and can be ordered using the {@link Ordered} + * interface or {@link Order @Order} annotation. + * + * @author Phillip Webb + * @see CommandLineRunner + * @since 1.3.0 + */ +public interface ApplicationRunner { + + /** + * Callback used to run the bean. + * @param args incoming application arguments + * @throws Exception on error + */ + void run(ApplicationArguments args) throws Exception; + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/CommandLineRunner.java b/spring-boot/src/main/java/org/springframework/boot/CommandLineRunner.java index 0eafd39bdb1..115ff3bf7c8 100644 --- a/spring-boot/src/main/java/org/springframework/boot/CommandLineRunner.java +++ b/spring-boot/src/main/java/org/springframework/boot/CommandLineRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-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. @@ -24,8 +24,12 @@ import org.springframework.core.annotation.Order; * a {@link SpringApplication}. Multiple {@link CommandLineRunner} beans can be defined * within the same application context and can be ordered using the {@link Ordered} * interface or {@link Order @Order} annotation. + *

+ * If you need access to {@link ApplicationArguments} instead of the raw String array + * consider using {@link ApplicationRunner}. * * @author Dave Syer + * @see ApplicationRunner */ public interface CommandLineRunner { diff --git a/spring-boot/src/main/java/org/springframework/boot/DefaultApplicationArguments.java b/spring-boot/src/main/java/org/springframework/boot/DefaultApplicationArguments.java new file mode 100644 index 00000000000..c6f4a32d9a5 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/DefaultApplicationArguments.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-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.boot; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.core.env.SimpleCommandLinePropertySource; +import org.springframework.util.Assert; + +/** + * Default internal implementation of {@link ApplicationArguments}. + * + * @author Phillip Webb + */ +class DefaultApplicationArguments implements ApplicationArguments { + + private final Source source; + + private final String[] args; + + public DefaultApplicationArguments(String[] args) { + Assert.notNull(args, "Args must not be null"); + this.source = new Source(args); + this.args = args; + } + + @Override + public String[] getSourceArgs() { + return this.args; + } + + @Override + public Set getOptionNames() { + String[] names = this.source.getPropertyNames(); + return Collections.unmodifiableSet(new HashSet(Arrays.asList(names))); + } + + @Override + public boolean containsOption(String name) { + return this.source.containsProperty(name); + } + + @Override + public List getOptionValues(String name) { + List values = this.source.getOptionValues(name); + return (values == null ? null : Collections.unmodifiableList(values)); + } + + @Override + public List getNonOptionArgs() { + return this.source.getNonOptionArgs(); + } + + private static class Source extends SimpleCommandLinePropertySource { + + public Source(String[] args) { + super(args); + } + + @Override + public List getNonOptionArgs() { + return super.getNonOptionArgs(); + } + + @Override + public List getOptionValues(String name) { + return super.getOptionValues(name); + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index e67b4e1f6f5..657c4180f98 100644 --- a/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -271,7 +271,6 @@ public class SpringApplication { listeners.started(); try { context = doRun(listeners, args); - stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted( @@ -328,6 +327,11 @@ public class SpringApplication { logStartupInfo(context.getParent() == null); } + // Add boot specific singleton beans + ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); + context.getBeanFactory().registerSingleton("springApplicationArguments", + applicationArguments); + // Load the sources Set sources = getSources(); Assert.notEmpty(sources, "Sources must not be empty"); @@ -336,7 +340,7 @@ public class SpringApplication { // Refresh the context refresh(context); - afterRefresh(context, args); + afterRefresh(context, applicationArguments); listeners.finished(context, null); return context; } @@ -654,20 +658,6 @@ public class SpringApplication { return new BeanDefinitionLoader(registry, sources); } - private void runCommandLineRunners(ApplicationContext context, String... args) { - List runners = new ArrayList(context - .getBeansOfType(CommandLineRunner.class).values()); - AnnotationAwareOrderComparator.sort(runners); - for (CommandLineRunner runner : runners) { - try { - runner.run(args); - } - catch (Exception ex) { - throw new IllegalStateException("Failed to execute CommandLineRunner", ex); - } - } - } - /** * Refresh the underlying {@link ApplicationContext}. * @param applicationContext the application context to refresh @@ -677,8 +667,59 @@ public class SpringApplication { ((AbstractApplicationContext) applicationContext).refresh(); } + /** + * Called after the context has been refreshed. + * @param context the application context + * @param args the application argumments + */ + protected void afterRefresh(ConfigurableApplicationContext context, + ApplicationArguments args) { + afterRefresh(context, args.getSourceArgs()); + callRunners(context, args); + } + + private void callRunners(ApplicationContext context, ApplicationArguments args) { + List runners = new ArrayList(); + runners.addAll(context.getBeansOfType(ApplicationRunner.class).values()); + runners.addAll(context.getBeansOfType(CommandLineRunner.class).values()); + AnnotationAwareOrderComparator.sort(runners); + for (Object runner : new LinkedHashSet(runners)) { + if (runner instanceof ApplicationRunner) { + callRunner((ApplicationRunner) runner, args); + } + if (runner instanceof CommandLineRunner) { + callRunner((CommandLineRunner) runner, args); + } + } + } + + private void callRunner(ApplicationRunner runner, ApplicationArguments args) { + try { + (runner).run(args); + } + catch (Exception ex) { + throw new IllegalStateException("Failed to execute ApplicationRunner", ex); + } + } + + private void callRunner(CommandLineRunner runner, ApplicationArguments args) { + try { + (runner).run(args.getSourceArgs()); + } + catch (Exception ex) { + throw new IllegalStateException("Failed to execute CommandLineRunner", ex); + } + } + + /** + * Called after the context has been refreshed. + * @param context the application context + * @param args the application argumments + * @deprecated in 1.3 in favor of + * {@link #afterRefresh(ConfigurableApplicationContext, ApplicationArguments)} + */ + @Deprecated protected void afterRefresh(ConfigurableApplicationContext context, String[] args) { - runCommandLineRunners(context, args); } /** diff --git a/spring-boot/src/test/java/org/springframework/boot/DefaultApplicationArgumentsTests.java b/spring-boot/src/test/java/org/springframework/boot/DefaultApplicationArgumentsTests.java new file mode 100644 index 00000000000..3596e325a9d --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/DefaultApplicationArgumentsTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-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.boot; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link DefaultApplicationArguments}. + * + * @author Phillip Webb + */ +public class DefaultApplicationArgumentsTests { + + private static final String[] ARGS = new String[] { "--foo=bar", "--foo=baz", + "--debug", "spring", "boot" }; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void argumentsMustNoBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Args must not be null"); + new DefaultApplicationArguments(null); + } + + @Test + public void getArgs() throws Exception { + ApplicationArguments arguments = new DefaultApplicationArguments(ARGS); + assertThat(arguments.getSourceArgs(), equalTo(ARGS)); + } + + @Test + public void optionNames() throws Exception { + ApplicationArguments arguments = new DefaultApplicationArguments(ARGS); + Set expected = new HashSet(Arrays.asList("foo", "debug")); + assertThat(arguments.getOptionNames(), equalTo(expected)); + } + + @Test + public void containsOption() throws Exception { + ApplicationArguments arguments = new DefaultApplicationArguments(ARGS); + assertThat(arguments.containsOption("foo"), equalTo(true)); + assertThat(arguments.containsOption("debug"), equalTo(true)); + assertThat(arguments.containsOption("spring"), equalTo(false)); + } + + @Test + public void getOptionValues() throws Exception { + ApplicationArguments arguments = new DefaultApplicationArguments(ARGS); + assertThat(arguments.getOptionValues("foo"), equalTo(Arrays.asList("bar", "baz"))); + assertThat(arguments.getOptionValues("debug"), + equalTo(Collections. emptyList())); + assertThat(arguments.getOptionValues("spring"), equalTo(null)); + } + + @Test + public void getNonOptionArgs() throws Exception { + ApplicationArguments arguments = new DefaultApplicationArguments(ARGS); + assertThat(arguments.getNonOptionArgs(), equalTo(Arrays.asList("spring", "boot"))); + } + + @Test + public void getNoNonOptionArgs() throws Exception { + ApplicationArguments arguments = new DefaultApplicationArguments( + new String[] { "--debug" }); + assertThat(arguments.getNonOptionArgs(), + equalTo(Collections. emptyList())); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index fe2e606cb14..4c35b128026 100644 --- a/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -442,12 +442,13 @@ public class SpringApplicationTests { } @Test - public void runCommandLineRunners() throws Exception { + public void runCommandLineRunnersAndApplicationRunners() throws Exception { SpringApplication application = new SpringApplication(CommandLineRunConfig.class); application.setWebEnvironment(false); this.context = application.run("arg"); assertTrue(this.context.getBean("runnerA", TestCommandLineRunner.class).hasRun()); - assertTrue(this.context.getBean("runnerB", TestCommandLineRunner.class).hasRun()); + assertTrue(this.context.getBean("runnerB", TestApplicationRunner.class).hasRun()); + assertTrue(this.context.getBean("runnerC", TestCommandLineRunner.class).hasRun()); } @Test @@ -604,6 +605,16 @@ public class SpringApplicationTests { assertThat(System.getProperty("java.awt.headless"), equalTo("false")); } + @Test + public void getApplicationArgumentsBean() throws Exception { + TestSpringApplication application = new TestSpringApplication(ExampleConfig.class); + application.setWebEnvironment(false); + this.context = application.run("--debug", "spring", "boot"); + ApplicationArguments args = this.context.getBean(ApplicationArguments.class); + assertThat(args.getNonOptionArgs(), equalTo(Arrays.asList("spring", "boot"))); + assertThat(args.containsOption("debug"), equalTo(true)); + } + private boolean hasPropertySource(ConfigurableEnvironment environment, Class propertySourceClass, String name) { for (PropertySource source : environment.getPropertySources()) { @@ -715,8 +726,14 @@ public class SpringApplicationTests { static class CommandLineRunConfig { @Bean - public TestCommandLineRunner runnerB() { - return new TestCommandLineRunner(Ordered.LOWEST_PRECEDENCE, "runnerA"); + public TestCommandLineRunner runnerC() { + return new TestCommandLineRunner(Ordered.LOWEST_PRECEDENCE, "runnerB", + "runnerA"); + } + + @Bean + public TestApplicationRunner runnerB() { + return new TestApplicationRunner(Ordered.LOWEST_PRECEDENCE - 1, "runnerA"); } @Bean @@ -725,18 +742,17 @@ public class SpringApplicationTests { } } - static class TestCommandLineRunner implements CommandLineRunner, - ApplicationContextAware, Ordered { + static class AbstractTestRunner implements ApplicationContextAware, Ordered { private final String[] expectedBefore; private ApplicationContext applicationContext; - private String[] args; - private final int order; - public TestCommandLineRunner(int order, String... expectedBefore) { + private boolean run; + + public AbstractTestRunner(int order, String... expectedBefore) { this.expectedBefore = expectedBefore; this.order = order; } @@ -752,18 +768,45 @@ public class SpringApplicationTests { return this.order; } - @Override - public void run(String... args) { - this.args = args; + public void markAsRan() { + this.run = true; for (String name : this.expectedBefore) { - TestCommandLineRunner bean = this.applicationContext.getBean(name, - TestCommandLineRunner.class); + AbstractTestRunner bean = this.applicationContext.getBean(name, + AbstractTestRunner.class); assertTrue(bean.hasRun()); } } public boolean hasRun() { - return this.args != null; + return this.run; + } + + } + + private static class TestCommandLineRunner extends AbstractTestRunner implements + CommandLineRunner { + + public TestCommandLineRunner(int order, String... expectedBefore) { + super(order, expectedBefore); + } + + @Override + public void run(String... args) { + markAsRan(); + } + + } + + private static class TestApplicationRunner extends AbstractTestRunner implements + ApplicationRunner { + + public TestApplicationRunner(int order, String... expectedBefore) { + super(order, expectedBefore); + } + + @Override + public void run(ApplicationArguments args) { + markAsRan(); } }