Browse Source

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
pull/3706/head
Phillip Webb 11 years ago
parent
commit
582239b03b
  1. 57
      spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc
  2. 74
      spring-boot/src/main/java/org/springframework/boot/ApplicationArguments.java
  3. 41
      spring-boot/src/main/java/org/springframework/boot/ApplicationRunner.java
  4. 6
      spring-boot/src/main/java/org/springframework/boot/CommandLineRunner.java
  5. 90
      spring-boot/src/main/java/org/springframework/boot/DefaultApplicationArguments.java
  6. 75
      spring-boot/src/main/java/org/springframework/boot/SpringApplication.java
  7. 95
      spring-boot/src/test/java/org/springframework/boot/DefaultApplicationArgumentsTests.java
  8. 73
      spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java

57
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 @@ -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<String> 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. @@ -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.

74
spring-boot/src/main/java/org/springframework/boot/ApplicationArguments.java

@ -0,0 +1,74 @@ @@ -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<String> 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.
* <ul>
* <li>if the option is present and has no argument (e.g.: "--foo"), return an empty
* collection ({@code []})</li>
* <li>if the option is present and has a single value (e.g. "--foo=bar"), return a
* collection having one element ({@code ["bar"]})</li>
* <li>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"]})</li>
* <li>if the option is not present, return {@code null}</li>
* </ul>
* @param name the name of the option
* @return a list of option values for the given name
*/
List<String> getOptionValues(String name);
/**
* Return the collection of non-option arguments parsed.
* @return the non-option arguments or an empty list
*/
List<String> getNonOptionArgs();
}

41
spring-boot/src/main/java/org/springframework/boot/ApplicationRunner.java

@ -0,0 +1,41 @@ @@ -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 <em>run</em> 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;
}

6
spring-boot/src/main/java/org/springframework/boot/CommandLineRunner.java

@ -1,5 +1,5 @@ @@ -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; @@ -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.
* <p>
* 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 {

90
spring-boot/src/main/java/org/springframework/boot/DefaultApplicationArguments.java

@ -0,0 +1,90 @@ @@ -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<String> getOptionNames() {
String[] names = this.source.getPropertyNames();
return Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(names)));
}
@Override
public boolean containsOption(String name) {
return this.source.containsProperty(name);
}
@Override
public List<String> getOptionValues(String name) {
List<String> values = this.source.getOptionValues(name);
return (values == null ? null : Collections.unmodifiableList(values));
}
@Override
public List<String> getNonOptionArgs() {
return this.source.getNonOptionArgs();
}
private static class Source extends SimpleCommandLinePropertySource {
public Source(String[] args) {
super(args);
}
@Override
public List<String> getNonOptionArgs() {
return super.getNonOptionArgs();
}
@Override
public List<String> getOptionValues(String name) {
return super.getOptionValues(name);
}
}
}

75
spring-boot/src/main/java/org/springframework/boot/SpringApplication.java

@ -271,7 +271,6 @@ public class SpringApplication { @@ -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 { @@ -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<Object> sources = getSources();
Assert.notEmpty(sources, "Sources must not be empty");
@ -336,7 +340,7 @@ public class SpringApplication { @@ -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 { @@ -654,20 +658,6 @@ public class SpringApplication {
return new BeanDefinitionLoader(registry, sources);
}
private void runCommandLineRunners(ApplicationContext context, String... args) {
List<CommandLineRunner> runners = new ArrayList<CommandLineRunner>(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 { @@ -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<Object> runners = new ArrayList<Object>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<Object>(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);
}
/**

95
spring-boot/src/test/java/org/springframework/boot/DefaultApplicationArgumentsTests.java

@ -0,0 +1,95 @@ @@ -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<String> expected = new HashSet<String>(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.<String> 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.<String> emptyList()));
}
}

73
spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java

@ -442,12 +442,13 @@ public class SpringApplicationTests { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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();
}
}

Loading…
Cancel
Save