Allow users to opt out of including the layer tools in a layered jar

For Maven, the layer configuration is now an additional configuration
option instead of a layout type.

Closes gh-19866
This commit is contained in:
Madhura Bhave
2020-02-07 16:02:35 -08:00
parent 56475c19fb
commit 15cd590f7f
19 changed files with 408 additions and 22 deletions

View File

@@ -7787,8 +7787,7 @@ This layering is designed to separate code based on how likely it is to change b
Library code is less likely to change between builds, so it is placed in its own layers to allow tooling to re-use the layers from cache.
Application code is more likely to change between builds so it is isolated in a separate layer.
For Maven, layered jars can be created using a new `layout` type called `LAYERED_JAR`.
See the {spring-boot-maven-plugin-docs}/#goals-build-image-parameters-details-layout[layout section] for more details.
For Maven, refer to the {spring-boot-maven-plugin-docs}/#repackage-layered-jars[packaging layered jars section] for more details on creating a layered jar.
For Gradle, refer to the {spring-boot-gradle-plugin-docs}/#packaging-layered-jars[packaging layered jars section] of the Gradle plugin documentation.
==== Writing the Dockerfile

View File

@@ -287,3 +287,18 @@ The jar will then be split into layer folders which may include:
* `snapshots-dependencies`
* `dependencies`
When you create a layered jar, the `spring-boot-layertools` jar will be added as a dependency to your jar.
With this jar on the classpath, you can launch your application in a special mode which allows the bootstrap code to run something entirely different from your application, for example, something that extracts the layers.
If you wish to exclude this dependency, you can do so in the following manner:
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
.Groovy
----
include::../gradle/packaging/boot-jar-layered-exclude-tools.gradle[tags=layered]
----
[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"]
.Kotlin
----
include::../gradle/packaging/boot-jar-layered-exclude-tools.gradle.kts[tags=layered]
----

View File

@@ -0,0 +1,16 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
bootJar {
mainClassName 'com.example.ExampleApplication'
}
// tag::layered[]
bootJar {
layered {
includeLayerTools = false
}
}
// end::layered[]

View File

@@ -0,0 +1,18 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar
plugins {
java
id("org.springframework.boot") version "{version}"
}
tasks.getByName<BootJar>("bootJar") {
mainClassName = "com.example.ExampleApplication"
}
// tag::layered[]
tasks.getByName<BootJar>("bootJar") {
layered {
includeLayerTools = false
}
}
// end::layered[]

View File

@@ -40,6 +40,8 @@ import org.gradle.api.java.archives.Attributes;
import org.gradle.api.specs.Spec;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.bundling.Jar;
import org.springframework.boot.loader.tools.Layer;
@@ -67,6 +69,8 @@ public class BootJar extends Jar implements BootArchive {
private Layers layers;
private LayerConfiguration layerConfiguration;
private static final String BOOT_INF_LAYERS = "BOOT-INF/layers";
private final List<String> dependencies = new ArrayList<>();
@@ -164,20 +168,33 @@ public class BootJar extends Jar implements BootArchive {
action.execute(enableLaunchScriptIfNecessary());
}
@Optional
@Nested
@Input
public LayerConfiguration getLayerConfiguration() {
return this.layerConfiguration;
}
/**
* Configures the archive to have layers.
*/
public void layered() {
this.layers = Layers.IMPLICIT;
this.bootInf.into("lib", (spec) -> spec.from((Callable<File>) () -> {
String jarName = "spring-boot-jarmode-layertools.jar";
InputStream stream = getClass().getClassLoader().getResourceAsStream("META-INF/jarmode/" + jarName);
File taskTmp = new File(getProject().getBuildDir(), "tmp/" + getName());
taskTmp.mkdirs();
File layerToolsJar = new File(taskTmp, jarName);
FileCopyUtils.copy(stream, new FileOutputStream(layerToolsJar));
return layerToolsJar;
}));
enableLayers();
applyLayers();
}
private void applyLayers() {
if (this.layerConfiguration.isIncludeLayerTools()) {
this.bootInf.into("lib", (spec) -> spec.from((Callable<File>) () -> {
String jarName = "spring-boot-jarmode-layertools.jar";
InputStream stream = getClass().getClassLoader().getResourceAsStream("META-INF/jarmode/" + jarName);
File taskTmp = new File(getProject().getBuildDir(), "tmp/" + getName());
taskTmp.mkdirs();
File layerToolsJar = new File(taskTmp, jarName);
FileCopyUtils.copy(stream, new FileOutputStream(layerToolsJar));
return layerToolsJar;
}));
}
this.bootInf.eachFile((details) -> {
Layer layer = layerForFileDetails(details);
if (layer != null) {
@@ -188,6 +205,20 @@ public class BootJar extends Jar implements BootArchive {
this.bootInf.into("", (spec) -> spec.from(createLayersIndex()));
}
public void layered(Action<LayerConfiguration> action) {
action.execute(enableLayers());
applyLayers();
}
private LayerConfiguration enableLayers() {
this.layers = Layers.IMPLICIT;
if (this.layerConfiguration == null) {
this.layerConfiguration = new LayerConfiguration();
}
return this.layerConfiguration;
}
private Layer layerForFileDetails(FileCopyDetails details) {
String path = details.getPath();
if (path.startsWith("BOOT-INF/lib/")) {

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2012-2020 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
*
* https://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.gradle.tasks.bundling;
import org.gradle.api.tasks.Input;
/**
* Encapsulates the configuration for a layered jar.
*
* @author Madhura Bhave
* @since 2.3.0
*/
public class LayerConfiguration {
private boolean includeLayerTools = true;
/**
* Whether to include the layer tools jar.
* @return true if layer tools is included
*/
@Input
public boolean isIncludeLayerTools() {
return this.includeLayerTools;
}
public void setIncludeLayerTools(boolean includeLayerTools) {
this.includeLayerTools = includeLayerTools;
}
}

View File

@@ -29,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Integration tests for {@link BootJar}.
*
* @author Andy Wilkinson
* @author Madhura Bhave
*/
class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
@@ -53,4 +54,13 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
.isEqualTo(TaskOutcome.SUCCESS);
}
@TestTemplate
void notUpToDateWhenBuiltWithLayersAndToolsAndThenWithLayersAndWithoutTools()
throws InvalidRunnerConfigurationException, UnexpectedBuildFailure, IOException {
assertThat(this.gradleBuild.build("-Playered=true", "bootJar").task(":bootJar").getOutcome())
.isEqualTo(TaskOutcome.SUCCESS);
assertThat(this.gradleBuild.build("-Playered=true", "-PexcludeTools=true", "bootJar").task(":bootJar")
.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
}
}

View File

@@ -25,6 +25,7 @@ import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import org.gradle.api.Action;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -129,6 +130,13 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
assertThat(entryNames).contains("BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar");
}
@Test
void whenJarIsLayeredAndIncludeLayerToolsIsFalseThenLayerToolsAreNotAddedToTheJar() throws IOException {
List<String> entryNames = getEntryNames(
createLayeredJar((configuration) -> configuration.setIncludeLayerTools(false)));
assertThat(entryNames).doesNotContain("BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar");
}
@Test
void classpathIndexPointsToBootInfLibs() throws IOException {
try (JarFile jarFile = new JarFile(createPopulatedJar())) {
@@ -145,13 +153,22 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
return getTask().getArchiveFile().get().getAsFile();
}
private File createLayeredJar() throws IOException {
private File createLayeredJar(Action<LayerConfiguration> action) throws IOException {
addContent();
getTask().layered();
if (action != null) {
getTask().layered(action);
}
else {
getTask().layered();
}
executeTask();
return getTask().getArchiveFile().get().getAsFile();
}
private File createLayeredJar() throws IOException {
return createLayeredJar(null);
}
private void addContent() throws IOException {
BootJar bootJar = getTask();
bootJar.setMainClassName("com.example.Main");

View File

@@ -11,6 +11,8 @@ bootJar {
}
}
if (project.hasProperty('layered') && project.getProperty('layered')) {
layered()
layered {
includeLayerTools = project.hasProperty('excludeTools') && project.getProperty('excludeTools') ? false : true
}
}
}

View File

@@ -397,3 +397,56 @@ This example excludes any artifact belonging to the `com.foo` group:
</build>
</project>
----
[[repackage-layered-jars]]
==== Packaging layered jars
By default, the repackaged jar contains the application's classes and dependencies in `BOOT-INF/classes` and `BOOT-INF/lib` respectively.
For cases where a docker image needs to be built from the contents of the jar, the jar format can be enhanced to support layer folders.
To use this feature, the layering feature must be enabled:
[source,xml,indent=0,subs="verbatim,attributes"]
----
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>{gradle-project-version}</version>
<configuration>
<layered>
<enabled>true</enabled>
</layered>
</configuration>
</plugin>
</plugins>
</build>
</project>
----
When you create a layered jar, the `spring-boot-layertools` jar will be added as a dependency to your jar.
With this jar on the classpath, you can launch your application in a special mode which allows the bootstrap code to run something entirely different from your application, for example, something that extracts the layers.
If you wish to exclude this dependency, you can do so in the following manner:
[source,xml,indent=0,subs="verbatim,attributes"]
----
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>{gradle-project-version}</version>
<configuration>
<layered>
<enabled>true</enabled>
<includeLayerTools>false</enabled>
</layered>
</configuration>
</plugin>
</plugins>
</build>
</project>
----

View File

@@ -287,7 +287,21 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
File repackaged = new File(project, "jar/target/jar-layered-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/layers/application/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/layers/dependencies/lib/jar-release")
.hasEntryWithNameStartingWith("BOOT-INF/layers/snapshot-dependencies/lib/jar-snapshot");
.hasEntryWithNameStartingWith("BOOT-INF/layers/snapshot-dependencies/lib/jar-snapshot")
.hasEntryWithNameStartingWith(
"BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar");
});
}
@TestTemplate
void whenJarIsRepackagedWithTheLayeredLayoutAndLayerToolsExcluded(MavenBuild mavenBuild) {
mavenBuild.project("jar-layered-no-layer-tools").execute((project) -> {
File repackaged = new File(project, "jar/target/jar-layered-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/layers/application/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/layers/dependencies/lib/jar-release")
.hasEntryWithNameStartingWith("BOOT-INF/layers/snapshot-dependencies/lib/jar-snapshot")
.doesNotHaveEntryWithNameStartingWith(
"BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar");
});
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-release</artifactId>
<version>0.0.1.RELEASE</version>
<packaging>jar</packaging>
<name>jar</name>
<description>Release Jar dependency</description>
</project>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-snapshot</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<packaging>jar</packaging>
<name>jar</name>
<description>Snapshot Jar dependency</description>
</project>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-layered</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<layered>
<enabled>true</enabled>
<includeLayerTools>false</includeLayerTools>
</layered>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-snapshot</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-release</artifactId>
<version>0.0.1.RELEASE</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2012-2020 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
*
* https://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.test;
public class SampleApplication {
public static void main(String[] args) {
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>aggregator</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<modules>
<module>jar-snapshot</module>
<module>jar-release</module>
<module>jar</module>
</modules>
</project>

View File

@@ -22,7 +22,9 @@
<goal>repackage</goal>
</goals>
<configuration>
<layout>LAYERED_JAR</layout>
<layered>
<enabled>true</enabled>
</layered>
</configuration>
</execution>
</executions>

View File

@@ -104,6 +104,13 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
@Parameter(defaultValue = "false")
public boolean includeSystemScope;
/**
* Layer configuration with the option to exclude layer tools jar.
* @since 2.3.0
*/
@Parameter
private Layered layered;
/**
* Return a {@link Packager} configured for this MOJO.
* @param <P> the packager type
@@ -119,6 +126,10 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
getLog().info("Layout: " + this.layout);
packager.setLayout(this.layout.layout());
}
if (this.layered != null && this.layered.isEnabled()) {
packager.setLayout(new LayeredJar());
packager.setIncludeRelevantJarModeJars(this.layered.isIncludeLayerTools());
}
return packager;
}
@@ -158,11 +169,6 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
*/
JAR(new Jar()),
/**
* Layered Jar Layout.
*/
LAYERED_JAR(new LayeredJar()),
/**
* War Layout.
*/

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2012-2020 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
*
* https://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.maven;
/**
* Layer configuration options.
*
* @author Madhura Bhave
* @since 2.3.0
*/
public class Layered {
private boolean enabled;
private boolean includeLayerTools = true;
/**
* Whether layered jar layout is enabled.
* @return true if the layered layout is enabled.
*/
public boolean isEnabled() {
return this.enabled;
}
/**
* Whether to include the layer tools jar.
* @return true if layer tools should be included
*/
public boolean isIncludeLayerTools() {
return this.includeLayerTools;
}
}