Commit 582239b0 authored by Phillip Webb's avatar Phillip Webb

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
parent 3de25164
......@@ -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.
@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.
......
/*
* 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();
}
/*
* 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;
}
/*
* 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.
* <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 {
......
/*
* 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);
}
}
}
......@@ -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<Object> 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<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 {
((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);
}
/**
......
/*
* 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()));
}
}
......@@ -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();
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment