Automatically create developmentOnly configuration

Previously, the developmentOnly configuration, typically used for
Devtools, had to be declared manually. The BootJar and BootWar tasks
then had a property, excludeDevtools, that could be used to control
whether or not Devtools would be excluded from the executable archive.

This commit updates the reaction to the Java plugin being applied to
automatically create the developmentOnly configuration. The classpaths
of bootJar and bootWar are then configured not to include the contents
of the developmentOnly configuration. As a result of this, the
excludeDevtools property is no longer needed and has been deprecated.
Its default has also been changed from true to false to make it easy
to opt in to Devtools, when configured as a development-only
dependency, being included in executable jars and wars by adding
developmentOnly to the classpath of the archive task.

Closes gh-16599
pull/21207/head
Andy Wilkinson 5 years ago
parent cbdc5d9746
commit fb33610027

@ -533,12 +533,6 @@ To include devtools support, add the module dependency to your build, as shown i
.Gradle .Gradle
[source,groovy,indent=0,subs="attributes"] [source,groovy,indent=0,subs="attributes"]
---- ----
configurations {
developmentOnly
runtimeClasspath {
extendsFrom developmentOnly
}
}
dependencies { dependencies {
developmentOnly("org.springframework.boot:spring-boot-devtools") developmentOnly("org.springframework.boot:spring-boot-devtools")
} }
@ -548,11 +542,12 @@ NOTE: Developer tools are automatically disabled when running a fully packaged a
If your application is launched from `java -jar` or if it is started from a special classloader, then it is considered a "`production application`". If your application is launched from `java -jar` or if it is started from a special classloader, then it is considered a "`production application`".
If that does not apply to you (i.e. if you run your application from a container), consider excluding devtools or set the `-Dspring.devtools.restart.enabled=false` system property. If that does not apply to you (i.e. if you run your application from a container), consider excluding devtools or set the `-Dspring.devtools.restart.enabled=false` system property.
TIP: Flagging the dependency as optional in Maven or using a custom `developmentOnly` configuration in Gradle (as shown above) is a best practice that prevents devtools from being transitively applied to other modules that use your project. TIP: Flagging the dependency as optional in Maven or using the `developmentOnly` configuration in Gradle (as shown above) prevents devtools from being transitively applied to other modules that use your project.
TIP: Repackaged archives do not contain devtools by default. TIP: Repackaged archives do not contain devtools by default.
If you want to use a <<using-boot-devtools-remote,certain remote devtools feature>>, you need to disable the `excludeDevtools` build property to include it. If you want to use a <<using-boot-devtools-remote,certain remote devtools feature>>, you need to include it.
The property is supported with both the Maven and Gradle plugins. When using the Maven plugin, set the `excludeDevtools` property to `false`.
When using the Gradle plugin, {spring-boot-gradle-plugin-docs}#packaging-executable-configuring-including-development-only-dependencies[configure the task's classpath to include the `developmentOnly` configuration].

@ -149,10 +149,11 @@ include::../gradle/packaging/boot-jar-manifest-main-class.gradle.kts[tags=main-c
[[packaging-executable-configuring-excluding-devtools]] [[packaging-executable-configuring-including-development-only-dependencies]]
==== Excluding Devtools ==== Including Development-only Dependencies
By default, Spring Boot's Devtools module, `org.springframework.boot:spring-boot-devtools`, will be excluded from an executable jar or war. By default all dependencies declared in the `developmentOnly` configuration will be excluded from an executable jar or war.
If you want to include Devtools in your archive set the `excludeDevtools` property to `false`:
If you want to include dependencies declared in the `developmentOnly` configuration in your archive, configure the classpath of its task to include the configuration, as shown in the following example for the `bootWar` task:
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] [source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
.Groovy .Groovy

@ -16,8 +16,9 @@ When Gradle's {java-plugin}[`java` plugin] is applied to a project, the Spring B
4. Creates a {boot-build-image-javadoc}[`BootBuildImage`] task named `bootBuildImage` that will create a OCI image using a https://buildpacks.io[buildpack]. 4. Creates a {boot-build-image-javadoc}[`BootBuildImage`] task named `bootBuildImage` that will create a OCI image using a https://buildpacks.io[buildpack].
5. Creates a {boot-run-javadoc}[`BootRun`] task named `bootRun` that can be used to run your application. 5. Creates a {boot-run-javadoc}[`BootRun`] task named `bootRun` that can be used to run your application.
6. Creates a configuration named `bootArchives` that contains the artifact produced by the `bootJar` task. 6. Creates a configuration named `bootArchives` that contains the artifact produced by the `bootJar` task.
7. Configures any `JavaCompile` tasks with no configured encoding to use `UTF-8`. 7. Creates a configuration named `developmentOnly` for dependencies that are only required at development time, such as Spring Boot's Devtools, and should not be packaged in executable jars and wars.
8. Configures any `JavaCompile` tasks to use the `-parameters` compiler argument. 8. Configures any `JavaCompile` tasks with no configured encoding to use `UTF-8`.
9. Configures any `JavaCompile` tasks to use the `-parameters` compiler argument.

@ -5,11 +5,14 @@ plugins {
bootWar { bootWar {
mainClassName 'com.example.ExampleApplication' mainClassName 'com.example.ExampleApplication'
classpath file('spring-boot-devtools-1.2.3.RELEASE.jar') }
dependencies {
developmentOnly files("spring-boot-devtools-1.2.3.RELEASE.jar")
} }
// tag::include-devtools[] // tag::include-devtools[]
bootWar { bootWar {
excludeDevtools = false classpath configurations.developmentOnly
} }
// end::include-devtools[] // end::include-devtools[]

@ -7,11 +7,14 @@ plugins {
tasks.getByName<BootWar>("bootWar") { tasks.getByName<BootWar>("bootWar") {
mainClassName = "com.example.ExampleApplication" mainClassName = "com.example.ExampleApplication"
classpath(file("spring-boot-devtools-1.2.3.RELEASE.jar")) }
dependencies {
"developmentOnly"(files("spring-boot-devtools-1.2.3.RELEASE.jar"))
} }
// tag::include-devtools[] // tag::include-devtools[]
tasks.getByName<BootWar>("bootWar") { tasks.getByName<BootWar>("bootWar") {
isExcludeDevtools = false classpath(configurations["developmentOnly"])
} }
// end::include-devtools[] // end::include-devtools[]

@ -27,6 +27,7 @@ import org.gradle.api.Action;
import org.gradle.api.Plugin; import org.gradle.api.Plugin;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.Task; import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileCollection;
import org.gradle.api.internal.artifacts.dsl.LazyPublishArtifact; import org.gradle.api.internal.artifacts.dsl.LazyPublishArtifact;
import org.gradle.api.plugins.ApplicationPlugin; import org.gradle.api.plugins.ApplicationPlugin;
@ -67,6 +68,7 @@ final class JavaPluginAction implements PluginApplicationAction {
public void execute(Project project) { public void execute(Project project) {
disableJarTask(project); disableJarTask(project);
configureBuildTask(project); configureBuildTask(project);
configureDevelopmentOnlyConfiguration(project);
TaskProvider<BootJar> bootJar = configureBootJarTask(project); TaskProvider<BootJar> bootJar = configureBootJarTask(project);
configureBootBuildImageTask(project, bootJar); configureBootBuildImageTask(project, bootJar);
configureArtifactPublication(bootJar); configureArtifactPublication(bootJar);
@ -92,7 +94,8 @@ final class JavaPluginAction implements PluginApplicationAction {
bootJar.setGroup(BasePlugin.BUILD_GROUP); bootJar.setGroup(BasePlugin.BUILD_GROUP);
SourceSet mainSourceSet = javaPluginConvention(project).getSourceSets() SourceSet mainSourceSet = javaPluginConvention(project).getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME); .getByName(SourceSet.MAIN_SOURCE_SET_NAME);
bootJar.classpath((Callable<FileCollection>) mainSourceSet::getRuntimeClasspath); bootJar.classpath((Callable<FileCollection>) () -> mainSourceSet.getRuntimeClasspath().minus(
project.getConfigurations().getByName(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME)));
bootJar.conventionMapping("mainClassName", new MainClassConvention(project, bootJar::getClasspath)); bootJar.conventionMapping("mainClassName", new MainClassConvention(project, bootJar::getClasspath));
}); });
} }
@ -157,6 +160,15 @@ final class JavaPluginAction implements PluginApplicationAction {
compile.doFirst(new AdditionalMetadataLocationsConfigurer()); compile.doFirst(new AdditionalMetadataLocationsConfigurer());
} }
private void configureDevelopmentOnlyConfiguration(Project project) {
Configuration developmentOnly = project.getConfigurations()
.create(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME);
developmentOnly
.setDescription("Configuration for development-only dependencies such as Spring Boot's DevTools.");
project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME)
.extendsFrom(developmentOnly);
}
/** /**
* Task {@link Action} to add additional meta-data locations. We need to use an * Task {@link Action} to add additional meta-data locations. We need to use an
* inner-class rather than a lambda due to * inner-class rather than a lambda due to

@ -70,6 +70,12 @@ public class SpringBootPlugin implements Plugin<Project> {
*/ */
public static final String BOOT_BUILD_IMAGE_TASK_NAME = "bootBuildImage"; public static final String BOOT_BUILD_IMAGE_TASK_NAME = "bootBuildImage";
/**
* The name of the {@code developmentOnly} configuration.
* @since 2.3.0
*/
public static final String DEVELOPMENT_ONLY_CONFIGURATION_NAME = "developmentOnly";
/** /**
* The coordinates {@code (group:name:version)} of the * The coordinates {@code (group:name:version)} of the
* {@code spring-boot-dependencies} bom. * {@code spring-boot-dependencies} bom.

@ -19,7 +19,8 @@ package org.springframework.boot.gradle.plugin;
import org.gradle.api.Action; import org.gradle.api.Action;
import org.gradle.api.Plugin; import org.gradle.api.Plugin;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.file.FileCollection;
import org.gradle.api.internal.artifacts.dsl.LazyPublishArtifact; import org.gradle.api.internal.artifacts.dsl.LazyPublishArtifact;
import org.gradle.api.plugins.BasePlugin; import org.gradle.api.plugins.BasePlugin;
import org.gradle.api.plugins.WarPlugin; import org.gradle.api.plugins.WarPlugin;
@ -63,12 +64,15 @@ class WarPluginAction implements PluginApplicationAction {
bootWar.setDescription("Assembles an executable war archive containing webapp" bootWar.setDescription("Assembles an executable war archive containing webapp"
+ " content, and the main classes and their dependencies."); + " content, and the main classes and their dependencies.");
bootWar.providedClasspath(providedRuntimeConfiguration(project)); bootWar.providedClasspath(providedRuntimeConfiguration(project));
bootWar.setClasspath(bootWar.getClasspath().minus(
project.getConfigurations().getByName(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME)));
bootWar.conventionMapping("mainClassName", new MainClassConvention(project, bootWar::getClasspath)); bootWar.conventionMapping("mainClassName", new MainClassConvention(project, bootWar::getClasspath));
}); });
} }
private Configuration providedRuntimeConfiguration(Project project) { private FileCollection providedRuntimeConfiguration(Project project) {
return project.getConfigurations().getByName(WarPlugin.PROVIDED_RUNTIME_CONFIGURATION_NAME); ConfigurationContainer configurations = project.getConfigurations();
return configurations.getByName(WarPlugin.PROVIDED_RUNTIME_CONFIGURATION_NAME);
} }
private void configureArtifactPublication(TaskProvider<BootWar> bootWar) { private void configureArtifactPublication(TaskProvider<BootWar> bootWar) {

@ -118,15 +118,21 @@ public interface BootArchive extends Task {
* {@code false}. * {@code false}.
* @return {@code true} if the Devtools jar should be excluded, or {@code false} if * @return {@code true} if the Devtools jar should be excluded, or {@code false} if
* not * not
* @deprecated since 2.3.0 in favour of configuring a classpath that does not include
* development-only dependencies
*/ */
@Input @Input
@Deprecated
boolean isExcludeDevtools(); boolean isExcludeDevtools();
/** /**
* Sets whether or not the Devtools jar should be excluded. * Sets whether or not the Devtools jar should be excluded.
* @param excludeDevtools {@code true} if the Devtools jar should be excluded, or * @param excludeDevtools {@code true} if the Devtools jar should be excluded, or
* {@code false} if not * {@code false} if not
* @deprecated since 2.3.0 in favour of configuring a classpath that does not include
* development-only dependencies
*/ */
@Deprecated
void setExcludeDevtools(boolean excludeDevtools); void setExcludeDevtools(boolean excludeDevtools);
} }

@ -76,7 +76,7 @@ class BootArchiveSupport {
private LaunchScriptConfiguration launchScript; private LaunchScriptConfiguration launchScript;
private boolean excludeDevtools = true; private boolean excludeDevtools = false;
BootArchiveSupport(String loaderMainClass, Spec<FileCopyDetails> librarySpec, BootArchiveSupport(String loaderMainClass, Spec<FileCopyDetails> librarySpec,
Function<FileCopyDetails, ZipCompression> compressionResolver) { Function<FileCopyDetails, ZipCompression> compressionResolver) {

@ -139,6 +139,13 @@ class JavaPluginActionIntegrationTests {
assertThat(result.getOutput()).contains("compileJava compiler args: [-parameters]"); assertThat(result.getOutput()).contains("compileJava compiler args: [-parameters]");
} }
@TestTemplate
void applyingJavaPluginCreatesDevelopmentOnlyConfiguration() {
assertThat(this.gradleBuild
.build("configurationExists", "-PconfigurationName=developmentOnly", "-PapplyJavaPlugin").getOutput())
.contains("developmentOnly exists = true");
}
private void createMinimalMainSource() throws IOException { private void createMinimalMainSource() throws IOException {
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/com/example"); File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/com/example");
examplePackage.mkdirs(); examplePackage.mkdirs();

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,7 +18,9 @@ package org.springframework.boot.gradle.tasks.bundling;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.jar.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import java.util.stream.Stream;
import org.gradle.testkit.runner.InvalidRunnerConfigurationException; import org.gradle.testkit.runner.InvalidRunnerConfigurationException;
import org.gradle.testkit.runner.TaskOutcome; import org.gradle.testkit.runner.TaskOutcome;
@ -40,12 +42,15 @@ import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(GradleCompatibilityExtension.class) @ExtendWith(GradleCompatibilityExtension.class)
abstract class AbstractBootArchiveIntegrationTests { abstract class AbstractBootArchiveIntegrationTests {
GradleBuild gradleBuild;
private final String taskName; private final String taskName;
protected AbstractBootArchiveIntegrationTests(String taskName) { private final String libPath;
GradleBuild gradleBuild;
protected AbstractBootArchiveIntegrationTests(String taskName, String libPath) {
this.taskName = taskName; this.taskName = taskName;
this.libPath = libPath;
} }
@TestTemplate @TestTemplate
@ -135,4 +140,27 @@ abstract class AbstractBootArchiveIntegrationTests {
.isEqualTo(TaskOutcome.SUCCESS); .isEqualTo(TaskOutcome.SUCCESS);
} }
@TestTemplate
void developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault() throws IOException {
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome())
.isEqualTo(TaskOutcome.SUCCESS);
try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) {
Stream<String> libEntryNames = jarFile.stream().filter((entry) -> !entry.isDirectory())
.map(JarEntry::getName).filter((name) -> name.startsWith(this.libPath));
assertThat(libEntryNames).containsExactly(this.libPath + "commons-io-2.6.jar");
}
}
@TestTemplate
void developmentOnlyDependenciesCanBeIncludedInTheArchive() throws IOException {
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome())
.isEqualTo(TaskOutcome.SUCCESS);
try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) {
Stream<String> libEntryNames = jarFile.stream().filter((entry) -> !entry.isDirectory())
.map(JarEntry::getName).filter((name) -> name.startsWith(this.libPath));
assertThat(libEntryNames).containsExactly(this.libPath + "commons-io-2.6.jar",
this.libPath + "commons-lang3-3.9.jar");
}
}
} }

@ -370,6 +370,7 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
} }
@Test @Test
@Deprecated
void devtoolsJarCanBeIncluded() throws IOException { void devtoolsJarCanBeIncluded() throws IOException {
this.task.setMainClassName("com.example.Main"); this.task.setMainClassName("com.example.Main");
this.task.classpath(jarFile("spring-boot-devtools-0.1.2.jar")); this.task.classpath(jarFile("spring-boot-devtools-0.1.2.jar"));

@ -57,7 +57,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
BootJarIntegrationTests() { BootJarIntegrationTests() {
super("bootJar"); super("bootJar", "BOOT-INF/lib/");
} }
@TestTemplate @TestTemplate

@ -24,7 +24,7 @@ package org.springframework.boot.gradle.tasks.bundling;
class BootWarIntegrationTests extends AbstractBootArchiveIntegrationTests { class BootWarIntegrationTests extends AbstractBootArchiveIntegrationTests {
BootWarIntegrationTests() { BootWarIntegrationTests() {
super("bootWar"); super("bootWar", "WEB-INF/lib/");
} }
} }

@ -19,3 +19,9 @@ task('javaCompileEncoding') {
} }
} }
} }
task('configurationExists') {
doFirst {
println "${configurationName} exists = ${configurations.findByName(configurationName) != null}"
}
}

@ -0,0 +1,17 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
bootJar {
mainClassName = 'com.example.Application'
}
repositories {
mavenCentral()
}
dependencies {
developmentOnly("org.apache.commons:commons-lang3:3.9")
implementation("commons-io:commons-io:2.6")
}

@ -0,0 +1,17 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
bootJar {
mainClassName = 'com.example.Application'
}
repositories {
mavenCentral()
}
dependencies {
developmentOnly("org.apache.commons:commons-lang3:3.9")
implementation("commons-io:commons-io:2.6")
}

@ -0,0 +1,21 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
bootJar {
mainClassName = 'com.example.Application'
}
repositories {
mavenCentral()
}
dependencies {
developmentOnly("org.apache.commons:commons-lang3:3.9")
implementation("commons-io:commons-io:2.6")
}
bootJar {
classpath configurations.developmentOnly
}

@ -0,0 +1,17 @@
plugins {
id 'war'
id 'org.springframework.boot' version '{version}'
}
bootWar {
mainClassName = 'com.example.Application'
}
repositories {
mavenCentral()
}
dependencies {
developmentOnly("org.apache.commons:commons-lang3:3.9")
implementation("commons-io:commons-io:2.6")
}

@ -0,0 +1,21 @@
plugins {
id 'war'
id 'org.springframework.boot' version '{version}'
}
bootWar {
mainClassName = 'com.example.Application'
}
repositories {
mavenCentral()
}
dependencies {
developmentOnly("org.apache.commons:commons-lang3:3.9")
implementation("commons-io:commons-io:2.6")
}
bootWar {
classpath configurations.developmentOnly
}
Loading…
Cancel
Save