Rework BootRun so that it does not subclass JavaExec

This commit reworks BootRun so that it no longer subclasses JavaExec.
This provides Boot with greater control of how the executed JVM is
configured, including the possibility of using @Option to provide args
and JVM args via the command line (gh-1176). It also allows some usage
of convention mappings to be removed in favour of PropertyState and
Provider (gh-9891). For users who relied up the advanced (and rather
complex) configuration options provided by JavaExec, an escape hatch
is provided by allowing the JavaExecSpec that's used to execute the
JVM to be customized.

Closes gh-9884
This commit is contained in:
Andy Wilkinson 2017-09-26 12:04:46 +01:00
parent 6f3d1797a7
commit 6eee9de3c1
11 changed files with 273 additions and 29 deletions

View File

@ -8,16 +8,11 @@ To run your application without first building an archive use the `bootRun` task
$ ./gradlew bootRun $ ./gradlew bootRun
---- ----
The `bootRun` task is an instance of The `bootRun` task is automatically configured to use the runtime classpath of the
{boot-run-javadoc}[`BootRun`] which is a `JavaExec` subclass. As such, all of the main source set. By default, the main class will be discovered by looking for a class
{gradle-dsl}/org.gradle.api.tasks.JavaExec.html[usual configuration options] for executing with a `public static void main(String[])` method in directories on the task's
a Java process in Gradle are available to you. The task is automatically configured to use classpath. The main class can also be configured explicitly using the task's `main`
the runtime classpath of the main source set. property:
By default, the main class will be configured automatically by looking for a class with a
`public static void main(String[])` method in directories on the task's classpath.
The main class can also be configured explicitly using the task's `main` property:
[source,groovy,indent=0,subs="verbatim"] [source,groovy,indent=0,subs="verbatim"]
---- ----
@ -32,6 +27,15 @@ its `mainClassName` project property can be used:
include::../gradle/running/application-plugin-main-class-name.gradle[tags=main-class] include::../gradle/running/application-plugin-main-class-name.gradle[tags=main-class]
---- ----
Two properties, `args` and `jvmArgs`, are also provided for configuring the
arguments and JVM arguments that are used to run the application.
For more advanced configuration the `JavaExecSpec` that is used can be customized:
[source,groovy,indent=0,subs="verbatim"]
----
include::../gradle/running/boot-run-custom-exec-spec.gradle[tags=customization]
----
[[running-your-application-reloading-resources]] [[running-your-application-reloading-resources]]

View File

@ -0,0 +1,16 @@
buildscript {
dependencies {
classpath files(pluginClasspath.split(','))
}
}
apply plugin: 'org.springframework.boot'
apply plugin: 'java'
// tag::customization[]
bootRun {
execSpec {
systemProperty 'com.example.foo', 'bar'
}
}
// end::customization[]

View File

@ -97,6 +97,7 @@ final class JavaPluginAction implements PluginApplicationAction {
this.singlePublishedArtifact.addCandidate(artifact); this.singlePublishedArtifact.addCandidate(artifact);
} }
@SuppressWarnings("unchecked")
private void configureBootRunTask(Project project) { private void configureBootRunTask(Project project) {
JavaPluginConvention javaConvention = project.getConvention() JavaPluginConvention javaConvention = project.getConvention()
.getPlugin(JavaPluginConvention.class); .getPlugin(JavaPluginConvention.class);
@ -105,14 +106,14 @@ final class JavaPluginAction implements PluginApplicationAction {
run.setGroup(ApplicationPlugin.APPLICATION_GROUP); run.setGroup(ApplicationPlugin.APPLICATION_GROUP);
run.classpath(javaConvention.getSourceSets() run.classpath(javaConvention.getSourceSets()
.findByName(SourceSet.MAIN_SOURCE_SET_NAME).getRuntimeClasspath()); .findByName(SourceSet.MAIN_SOURCE_SET_NAME).getRuntimeClasspath());
run.getConventionMapping().map("jvmArgs", () -> { run.setJvmArgs(project.provider(() -> {
if (project.hasProperty("applicationDefaultJvmArgs")) { if (project.hasProperty("applicationDefaultJvmArgs")) {
return project.property("applicationDefaultJvmArgs"); return (List<String>) project.property("applicationDefaultJvmArgs");
} }
return Collections.emptyList(); return Collections.emptyList();
}); }));
run.conventionMapping("main", run.setMain(
new MainClassConvention(project, run::getClasspath)); project.provider(new MainClassConvention(project, run::getClasspath)));
} }
private void configureUtf8Encoding(Project project) { private void configureUtf8Encoding(Project project) {

View File

@ -32,7 +32,7 @@ import org.springframework.boot.loader.tools.MainClassFinder;
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
final class MainClassConvention implements Callable<Object> { final class MainClassConvention implements Callable<String> {
private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication"; private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication";
@ -46,11 +46,11 @@ final class MainClassConvention implements Callable<Object> {
} }
@Override @Override
public Object call() throws Exception { public String call() throws Exception {
if (this.project.hasProperty("mainClassName")) { if (this.project.hasProperty("mainClassName")) {
Object mainClassName = this.project.property("mainClassName"); Object mainClassName = this.project.property("mainClassName");
if (mainClassName != null) { if (mainClassName != null) {
return mainClassName; return mainClassName.toString();
} }
} }
return resolveMainClass(); return resolveMainClass();

View File

@ -16,10 +16,21 @@
package org.springframework.boot.gradle.tasks.run; package org.springframework.boot.gradle.tasks.run;
import java.util.ArrayList;
import java.util.List;
import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.SourceDirectorySet; import org.gradle.api.file.SourceDirectorySet;
import org.gradle.api.provider.PropertyState;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.JavaExec; import org.gradle.api.tasks.JavaExec;
import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetOutput; import org.gradle.api.tasks.SourceSetOutput;
import org.gradle.api.tasks.TaskAction;
import org.gradle.process.JavaExecSpec;
/** /**
* Custom {@link JavaExec} task for running a Spring Boot application. * Custom {@link JavaExec} task for running a Spring Boot application.
@ -27,7 +38,34 @@ import org.gradle.api.tasks.SourceSetOutput;
* @author Andy Wilkinson * @author Andy Wilkinson
* @since 2.0.0 * @since 2.0.0
*/ */
public class BootRun extends JavaExec { public class BootRun extends DefaultTask {
private final PropertyState<String> main = getProject().property(String.class);
@SuppressWarnings("unchecked")
private final PropertyState<List<String>> jvmArgs = (PropertyState<List<String>>) (Object) getProject()
.property(List.class);
@SuppressWarnings("unchecked")
private final PropertyState<List<String>> args = (PropertyState<List<String>>) (Object) getProject()
.property(List.class);
private FileCollection classpath = getProject().files();
private List<Action<JavaExecSpec>> execSpecConfigurers = new ArrayList<>();
/**
* Adds the given {@code entries} to the classpath used to run the application.
* @param entries the classpath entries
*/
public void classpath(Object... entries) {
this.classpath = this.classpath.plus(getProject().files(entries));
}
@InputFiles
public FileCollection getClasspath() {
return this.classpath;
}
/** /**
* Adds the {@link SourceDirectorySet#getSrcDirs() source directories} of the given * Adds the {@link SourceDirectorySet#getSrcDirs() source directories} of the given
@ -38,18 +76,115 @@ public class BootRun extends JavaExec {
* @param sourceSet the source set * @param sourceSet the source set
*/ */
public void sourceResources(SourceSet sourceSet) { public void sourceResources(SourceSet sourceSet) {
setClasspath(getProject() this.classpath = getProject()
.files(sourceSet.getResources().getSrcDirs(), getClasspath()) .files(sourceSet.getResources().getSrcDirs(), this.classpath)
.filter((file) -> !file.equals(sourceSet.getOutput().getResourcesDir()))); .filter((file) -> !file.equals(sourceSet.getOutput().getResourcesDir()));
} }
@Override /**
public void exec() { * Returns the name of the main class to be run.
if (System.console() != null) { * @return the main class name or {@code null}
// Record that the console is available here for AnsiOutput to detect later */
this.getEnvironment().put("spring.output.ansi.console-available", true); public String getMain() {
return this.main.getOrNull();
} }
super.exec();
/**
* Sets the main class to be executed using the given {@code mainProvider}.
*
* @param mainProvider provider of the main class name
*/
public void setMain(Provider<String> mainProvider) {
this.main.set(mainProvider);
}
/**
* Sets the main class to be run.
*
* @param main the main class name
*/
public void setMain(String main) {
this.main.set(main);
}
/**
* Returns the JVM arguments to be used to run the application.
* @return the JVM arguments or {@code null}
*/
public List<String> getJvmArgs() {
return this.jvmArgs.getOrNull();
}
/**
* Configures the application to be run using the JVM args provided by the given
* {@code jvmArgsProvider}.
*
* @param jvmArgsProvider the provider of the JVM args
*/
public void setJvmArgs(Provider<List<String>> jvmArgsProvider) {
this.jvmArgs.set(jvmArgsProvider);
}
/**
* Configures the application to be run using the given {@code jvmArgs}.
* @param jvmArgs the JVM args
*/
public void setJvmArgs(List<String> jvmArgs) {
this.jvmArgs.set(jvmArgs);
}
/**
* Returns the arguments to be used to run the application.
* @return the arguments or {@code null}
*/
public List<String> getArgs() {
return this.args.getOrNull();
}
/**
* Configures the application to be run using the given {@code args}.
* @param args the args
*/
public void setArgs(List<String> args) {
this.args.set(args);
}
/**
* Configures the application to be run using the args provided by the given
* {@code argsProvider}.
* @param argsProvider the provider of the args
*/
public void setArgs(Provider<List<String>> argsProvider) {
this.args.set(argsProvider);
}
/**
* Registers the given {@code execSpecConfigurer} to be called to customize the
* {@link JavaExecSpec} prior to running the application.
* @param execSpecConfigurer the configurer
*/
public void execSpec(Action<JavaExecSpec> execSpecConfigurer) {
this.execSpecConfigurers.add(execSpecConfigurer);
}
@TaskAction
public void run() {
getProject().javaexec((spec) -> {
spec.classpath(this.classpath);
spec.setMain(this.main.getOrNull());
if (this.jvmArgs.isPresent()) {
spec.setJvmArgs(this.jvmArgs.get());
}
if (this.args.isPresent()) {
spec.setArgs(this.args.get());
}
if (System.console() != null) {
// Record that the console is available here for AnsiOutput to detect
// later
spec.environment("spring.output.ansi.console-available", true);
}
this.execSpecConfigurers.forEach((configurer) -> configurer.execute(spec));
});
} }
} }

View File

@ -18,6 +18,7 @@ package com.example;
import java.io.File; import java.io.File;
import java.lang.management.ManagementFactory; import java.lang.management.ManagementFactory;
import java.util.Arrays;
/** /**
* Very basic application used for testing {@code BootRun}. * Very basic application used for testing {@code BootRun}.
@ -31,6 +32,12 @@ public class BootRunApplication {
} }
public static void main(String[] args) { public static void main(String[] args) {
dumpClassPath();
dumpArgs(args);
dumpJvmArgs();
}
private static void dumpClassPath() {
int i = 1; int i = 1;
for (String entry : ManagementFactory.getRuntimeMXBean().getClassPath() for (String entry : ManagementFactory.getRuntimeMXBean().getClassPath()
.split(File.pathSeparator)) { .split(File.pathSeparator)) {
@ -38,4 +45,12 @@ public class BootRunApplication {
} }
} }
private static void dumpArgs(String[] args) {
System.out.println(Arrays.toString(args));
}
private static void dumpJvmArgs() {
System.out.println(ManagementFactory.getRuntimeMXBean().getInputArguments());
}
} }

View File

@ -59,4 +59,11 @@ public class RunningDocumentationTests {
.contains(new File("src/main/resources").getPath()); .contains(new File("src/main/resources").getPath());
} }
@Test
public void bootRunExecSpecCustomization() throws IOException {
this.gradleBuild
.script("src/main/gradle/running/boot-run-custom-exec-spec.gradle")
.build();
}
} }

View File

@ -56,6 +56,33 @@ public class BootRunIntegrationTests {
.doesNotContain(canonicalPathOf("src/main/resources")); .doesNotContain(canonicalPathOf("src/main/resources"));
} }
@Test
public void argsCanBeConfigured() throws IOException {
copyApplication();
new File(this.gradleBuild.getProjectDir(), "src/main/resources").mkdirs();
BuildResult result = this.gradleBuild.build("bootRun");
assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("--com.example.foo=bar");
}
@Test
public void jvmArgsCanBeConfigured() throws IOException {
copyApplication();
new File(this.gradleBuild.getProjectDir(), "src/main/resources").mkdirs();
BuildResult result = this.gradleBuild.build("bootRun");
assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("-Dcom.example.foo=bar");
}
@Test
public void execSpecCanBeConfigured() throws IOException {
copyApplication();
new File(this.gradleBuild.getProjectDir(), "src/main/resources").mkdirs();
BuildResult result = this.gradleBuild.build("bootRun");
assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("-Dcom.example.foo=bar");
}
@Test @Test
public void sourceResourcesCanBeUsed() throws IOException { public void sourceResourcesCanBeUsed() throws IOException {
copyApplication(); copyApplication();

View File

@ -0,0 +1,12 @@
buildscript {
dependencies {
classpath files(pluginClasspath.split(','))
}
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
bootRun {
args = ['--com.example.foo=bar']
}

View File

@ -0,0 +1,15 @@
buildscript {
dependencies {
classpath files(pluginClasspath.split(','))
}
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
bootRun {
execSpec {
systemProperty 'com.example.foo', 'bar'
}
}

View File

@ -0,0 +1,12 @@
buildscript {
dependencies {
classpath files(pluginClasspath.split(','))
}
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
bootRun {
jvmArgs = ['-Dcom.example.foo=bar']
}