Skip to content

Commit 6eee9de

Browse files
committed
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
1 parent 6f3d179 commit 6eee9de

File tree

11 files changed

+275
-31
lines changed

11 files changed

+275
-31
lines changed

spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/running.adoc

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,11 @@ To run your application without first building an archive use the `bootRun` task
88
$ ./gradlew bootRun
99
----
1010

11-
The `bootRun` task is an instance of
12-
{boot-run-javadoc}[`BootRun`] which is a `JavaExec` subclass. As such, all of the
13-
{gradle-dsl}/org.gradle.api.tasks.JavaExec.html[usual configuration options] for executing
14-
a Java process in Gradle are available to you. The task is automatically configured to use
15-
the runtime classpath of the main source set.
16-
17-
By default, the main class will be configured automatically by looking for a class with a
18-
`public static void main(String[])` method in directories on the task's classpath.
19-
20-
The main class can also be configured explicitly using the task's `main` property:
11+
The `bootRun` task is automatically configured to use the runtime classpath of the
12+
main source set. By default, the main class will be discovered by looking for a class
13+
with a `public static void main(String[])` method in directories on the task's
14+
classpath. The main class can also be configured explicitly using the task's `main`
15+
property:
2116

2217
[source,groovy,indent=0,subs="verbatim"]
2318
----
@@ -32,6 +27,15 @@ its `mainClassName` project property can be used:
3227
include::../gradle/running/application-plugin-main-class-name.gradle[tags=main-class]
3328
----
3429

30+
Two properties, `args` and `jvmArgs`, are also provided for configuring the
31+
arguments and JVM arguments that are used to run the application.
32+
33+
For more advanced configuration the `JavaExecSpec` that is used can be customized:
34+
35+
[source,groovy,indent=0,subs="verbatim"]
36+
----
37+
include::../gradle/running/boot-run-custom-exec-spec.gradle[tags=customization]
38+
----
3539

3640

3741
[[running-your-application-reloading-resources]]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
buildscript {
2+
dependencies {
3+
classpath files(pluginClasspath.split(','))
4+
}
5+
}
6+
7+
apply plugin: 'org.springframework.boot'
8+
apply plugin: 'java'
9+
10+
// tag::customization[]
11+
bootRun {
12+
execSpec {
13+
systemProperty 'com.example.foo', 'bar'
14+
}
15+
}
16+
// end::customization[]

spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ private void configureArtifactPublication(Project project, BootJar bootJar) {
9797
this.singlePublishedArtifact.addCandidate(artifact);
9898
}
9999

100+
@SuppressWarnings("unchecked")
100101
private void configureBootRunTask(Project project) {
101102
JavaPluginConvention javaConvention = project.getConvention()
102103
.getPlugin(JavaPluginConvention.class);
@@ -105,14 +106,14 @@ private void configureBootRunTask(Project project) {
105106
run.setGroup(ApplicationPlugin.APPLICATION_GROUP);
106107
run.classpath(javaConvention.getSourceSets()
107108
.findByName(SourceSet.MAIN_SOURCE_SET_NAME).getRuntimeClasspath());
108-
run.getConventionMapping().map("jvmArgs", () -> {
109+
run.setJvmArgs(project.provider(() -> {
109110
if (project.hasProperty("applicationDefaultJvmArgs")) {
110-
return project.property("applicationDefaultJvmArgs");
111+
return (List<String>) project.property("applicationDefaultJvmArgs");
111112
}
112113
return Collections.emptyList();
113-
});
114-
run.conventionMapping("main",
115-
new MainClassConvention(project, run::getClasspath));
114+
}));
115+
run.setMain(
116+
project.provider(new MainClassConvention(project, run::getClasspath)));
116117
}
117118

118119
private void configureUtf8Encoding(Project project) {

spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/MainClassConvention.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
*
3333
* @author Andy Wilkinson
3434
*/
35-
final class MainClassConvention implements Callable<Object> {
35+
final class MainClassConvention implements Callable<String> {
3636

3737
private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication";
3838

@@ -46,11 +46,11 @@ final class MainClassConvention implements Callable<Object> {
4646
}
4747

4848
@Override
49-
public Object call() throws Exception {
49+
public String call() throws Exception {
5050
if (this.project.hasProperty("mainClassName")) {
5151
Object mainClassName = this.project.property("mainClassName");
5252
if (mainClassName != null) {
53-
return mainClassName;
53+
return mainClassName.toString();
5454
}
5555
}
5656
return resolveMainClass();

spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/run/BootRun.java

Lines changed: 148 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,56 @@
1616

1717
package org.springframework.boot.gradle.tasks.run;
1818

19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
import org.gradle.api.Action;
23+
import org.gradle.api.DefaultTask;
24+
import org.gradle.api.file.FileCollection;
1925
import org.gradle.api.file.SourceDirectorySet;
26+
import org.gradle.api.provider.PropertyState;
27+
import org.gradle.api.provider.Provider;
28+
import org.gradle.api.tasks.InputFiles;
2029
import org.gradle.api.tasks.JavaExec;
2130
import org.gradle.api.tasks.SourceSet;
2231
import org.gradle.api.tasks.SourceSetOutput;
32+
import org.gradle.api.tasks.TaskAction;
33+
import org.gradle.process.JavaExecSpec;
2334

2435
/**
2536
* Custom {@link JavaExec} task for running a Spring Boot application.
2637
*
2738
* @author Andy Wilkinson
2839
* @since 2.0.0
2940
*/
30-
public class BootRun extends JavaExec {
41+
public class BootRun extends DefaultTask {
42+
43+
private final PropertyState<String> main = getProject().property(String.class);
44+
45+
@SuppressWarnings("unchecked")
46+
private final PropertyState<List<String>> jvmArgs = (PropertyState<List<String>>) (Object) getProject()
47+
.property(List.class);
48+
49+
@SuppressWarnings("unchecked")
50+
private final PropertyState<List<String>> args = (PropertyState<List<String>>) (Object) getProject()
51+
.property(List.class);
52+
53+
private FileCollection classpath = getProject().files();
54+
55+
private List<Action<JavaExecSpec>> execSpecConfigurers = new ArrayList<>();
56+
57+
/**
58+
* Adds the given {@code entries} to the classpath used to run the application.
59+
* @param entries the classpath entries
60+
*/
61+
public void classpath(Object... entries) {
62+
this.classpath = this.classpath.plus(getProject().files(entries));
63+
}
64+
65+
@InputFiles
66+
public FileCollection getClasspath() {
67+
return this.classpath;
68+
}
3169

3270
/**
3371
* Adds the {@link SourceDirectorySet#getSrcDirs() source directories} of the given
@@ -38,18 +76,115 @@ public class BootRun extends JavaExec {
3876
* @param sourceSet the source set
3977
*/
4078
public void sourceResources(SourceSet sourceSet) {
41-
setClasspath(getProject()
42-
.files(sourceSet.getResources().getSrcDirs(), getClasspath())
43-
.filter((file) -> !file.equals(sourceSet.getOutput().getResourcesDir())));
44-
}
45-
46-
@Override
47-
public void exec() {
48-
if (System.console() != null) {
49-
// Record that the console is available here for AnsiOutput to detect later
50-
this.getEnvironment().put("spring.output.ansi.console-available", true);
51-
}
52-
super.exec();
79+
this.classpath = getProject()
80+
.files(sourceSet.getResources().getSrcDirs(), this.classpath)
81+
.filter((file) -> !file.equals(sourceSet.getOutput().getResourcesDir()));
82+
}
83+
84+
/**
85+
* Returns the name of the main class to be run.
86+
* @return the main class name or {@code null}
87+
*/
88+
public String getMain() {
89+
return this.main.getOrNull();
90+
}
91+
92+
/**
93+
* Sets the main class to be executed using the given {@code mainProvider}.
94+
*
95+
* @param mainProvider provider of the main class name
96+
*/
97+
public void setMain(Provider<String> mainProvider) {
98+
this.main.set(mainProvider);
99+
}
100+
101+
/**
102+
* Sets the main class to be run.
103+
*
104+
* @param main the main class name
105+
*/
106+
public void setMain(String main) {
107+
this.main.set(main);
108+
}
109+
110+
/**
111+
* Returns the JVM arguments to be used to run the application.
112+
* @return the JVM arguments or {@code null}
113+
*/
114+
public List<String> getJvmArgs() {
115+
return this.jvmArgs.getOrNull();
116+
}
117+
118+
/**
119+
* Configures the application to be run using the JVM args provided by the given
120+
* {@code jvmArgsProvider}.
121+
*
122+
* @param jvmArgsProvider the provider of the JVM args
123+
*/
124+
public void setJvmArgs(Provider<List<String>> jvmArgsProvider) {
125+
this.jvmArgs.set(jvmArgsProvider);
126+
}
127+
128+
/**
129+
* Configures the application to be run using the given {@code jvmArgs}.
130+
* @param jvmArgs the JVM args
131+
*/
132+
public void setJvmArgs(List<String> jvmArgs) {
133+
this.jvmArgs.set(jvmArgs);
134+
}
135+
136+
/**
137+
* Returns the arguments to be used to run the application.
138+
* @return the arguments or {@code null}
139+
*/
140+
public List<String> getArgs() {
141+
return this.args.getOrNull();
142+
}
143+
144+
/**
145+
* Configures the application to be run using the given {@code args}.
146+
* @param args the args
147+
*/
148+
public void setArgs(List<String> args) {
149+
this.args.set(args);
150+
}
151+
152+
/**
153+
* Configures the application to be run using the args provided by the given
154+
* {@code argsProvider}.
155+
* @param argsProvider the provider of the args
156+
*/
157+
public void setArgs(Provider<List<String>> argsProvider) {
158+
this.args.set(argsProvider);
159+
}
160+
161+
/**
162+
* Registers the given {@code execSpecConfigurer} to be called to customize the
163+
* {@link JavaExecSpec} prior to running the application.
164+
* @param execSpecConfigurer the configurer
165+
*/
166+
public void execSpec(Action<JavaExecSpec> execSpecConfigurer) {
167+
this.execSpecConfigurers.add(execSpecConfigurer);
168+
}
169+
170+
@TaskAction
171+
public void run() {
172+
getProject().javaexec((spec) -> {
173+
spec.classpath(this.classpath);
174+
spec.setMain(this.main.getOrNull());
175+
if (this.jvmArgs.isPresent()) {
176+
spec.setJvmArgs(this.jvmArgs.get());
177+
}
178+
if (this.args.isPresent()) {
179+
spec.setArgs(this.args.get());
180+
}
181+
if (System.console() != null) {
182+
// Record that the console is available here for AnsiOutput to detect
183+
// later
184+
spec.environment("spring.output.ansi.console-available", true);
185+
}
186+
this.execSpecConfigurers.forEach((configurer) -> configurer.execute(spec));
187+
});
53188
}
54189

55190
}

spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/BootRunApplication.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.io.File;
2020
import java.lang.management.ManagementFactory;
21+
import java.util.Arrays;
2122

2223
/**
2324
* Very basic application used for testing {@code BootRun}.
@@ -31,11 +32,25 @@ protected BootRunApplication() {
3132
}
3233

3334
public static void main(String[] args) {
35+
dumpClassPath();
36+
dumpArgs(args);
37+
dumpJvmArgs();
38+
}
39+
40+
private static void dumpClassPath() {
3441
int i = 1;
3542
for (String entry : ManagementFactory.getRuntimeMXBean().getClassPath()
3643
.split(File.pathSeparator)) {
3744
System.out.println(i++ + ". " + entry);
3845
}
3946
}
4047

48+
private static void dumpArgs(String[] args) {
49+
System.out.println(Arrays.toString(args));
50+
}
51+
52+
private static void dumpJvmArgs() {
53+
System.out.println(ManagementFactory.getRuntimeMXBean().getInputArguments());
54+
}
55+
4156
}

spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/RunningDocumentationTests.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,11 @@ public void bootRunSourceResources() throws IOException {
5959
.contains(new File("src/main/resources").getPath());
6060
}
6161

62+
@Test
63+
public void bootRunExecSpecCustomization() throws IOException {
64+
this.gradleBuild
65+
.script("src/main/gradle/running/boot-run-custom-exec-spec.gradle")
66+
.build();
67+
}
68+
6269
}

spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,33 @@ public void basicExecution() throws IOException {
5656
.doesNotContain(canonicalPathOf("src/main/resources"));
5757
}
5858

59+
@Test
60+
public void argsCanBeConfigured() throws IOException {
61+
copyApplication();
62+
new File(this.gradleBuild.getProjectDir(), "src/main/resources").mkdirs();
63+
BuildResult result = this.gradleBuild.build("bootRun");
64+
assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
65+
assertThat(result.getOutput()).contains("--com.example.foo=bar");
66+
}
67+
68+
@Test
69+
public void jvmArgsCanBeConfigured() throws IOException {
70+
copyApplication();
71+
new File(this.gradleBuild.getProjectDir(), "src/main/resources").mkdirs();
72+
BuildResult result = this.gradleBuild.build("bootRun");
73+
assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
74+
assertThat(result.getOutput()).contains("-Dcom.example.foo=bar");
75+
}
76+
77+
@Test
78+
public void execSpecCanBeConfigured() throws IOException {
79+
copyApplication();
80+
new File(this.gradleBuild.getProjectDir(), "src/main/resources").mkdirs();
81+
BuildResult result = this.gradleBuild.build("bootRun");
82+
assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
83+
assertThat(result.getOutput()).contains("-Dcom.example.foo=bar");
84+
}
85+
5986
@Test
6087
public void sourceResourcesCanBeUsed() throws IOException {
6188
copyApplication();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
buildscript {
2+
dependencies {
3+
classpath files(pluginClasspath.split(','))
4+
}
5+
}
6+
7+
apply plugin: 'java'
8+
apply plugin: 'org.springframework.boot'
9+
10+
bootRun {
11+
args = ['--com.example.foo=bar']
12+
}

0 commit comments

Comments
 (0)