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
pull/8988/merge
Andy Wilkinson 7 years ago
parent 6f3d1797a7
commit 6eee9de3c1

@ -8,16 +8,11 @@ To run your application without first building an archive use the `bootRun` task
$ ./gradlew bootRun
----
The `bootRun` task is an instance of
{boot-run-javadoc}[`BootRun`] which is a `JavaExec` subclass. As such, all of the
{gradle-dsl}/org.gradle.api.tasks.JavaExec.html[usual configuration options] for executing
a Java process in Gradle are available to you. The task is automatically configured to use
the runtime classpath of the main source set.
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:
The `bootRun` task is automatically configured to use the runtime classpath of the
main source set. By default, the main class will be discovered 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"]
----
@ -32,6 +27,15 @@ its `mainClassName` project property can be used:
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]]

@ -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[]

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

@ -32,7 +32,7 @@ import org.springframework.boot.loader.tools.MainClassFinder;
*
* @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";
@ -46,11 +46,11 @@ final class MainClassConvention implements Callable<Object> {
}
@Override
public Object call() throws Exception {
public String call() throws Exception {
if (this.project.hasProperty("mainClassName")) {
Object mainClassName = this.project.property("mainClassName");
if (mainClassName != null) {
return mainClassName;
return mainClassName.toString();
}
}
return resolveMainClass();

@ -16,10 +16,21 @@
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.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.SourceSet;
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.
@ -27,7 +38,34 @@ import org.gradle.api.tasks.SourceSetOutput;
* @author Andy Wilkinson
* @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
@ -38,18 +76,115 @@ public class BootRun extends JavaExec {
* @param sourceSet the source set
*/
public void sourceResources(SourceSet sourceSet) {
setClasspath(getProject()
.files(sourceSet.getResources().getSrcDirs(), getClasspath())
.filter((file) -> !file.equals(sourceSet.getOutput().getResourcesDir())));
}
@Override
public void exec() {
if (System.console() != null) {
// Record that the console is available here for AnsiOutput to detect later
this.getEnvironment().put("spring.output.ansi.console-available", true);
}
super.exec();
this.classpath = getProject()
.files(sourceSet.getResources().getSrcDirs(), this.classpath)
.filter((file) -> !file.equals(sourceSet.getOutput().getResourcesDir()));
}
/**
* Returns the name of the main class to be run.
* @return the main class name or {@code null}
*/
public String getMain() {
return this.main.getOrNull();
}
/**
* 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));
});
}
}

@ -18,6 +18,7 @@ package com.example;
import java.io.File;
import java.lang.management.ManagementFactory;
import java.util.Arrays;
/**
* Very basic application used for testing {@code BootRun}.
@ -31,6 +32,12 @@ public class BootRunApplication {
}
public static void main(String[] args) {
dumpClassPath();
dumpArgs(args);
dumpJvmArgs();
}
private static void dumpClassPath() {
int i = 1;
for (String entry : ManagementFactory.getRuntimeMXBean().getClassPath()
.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());
}
}

@ -59,4 +59,11 @@ public class RunningDocumentationTests {
.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();
}
}

@ -56,6 +56,33 @@ public class BootRunIntegrationTests {
.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
public void sourceResourcesCanBeUsed() throws IOException {
copyApplication();

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

@ -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'
}
}

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