Commit 15cd590f authored by Madhura Bhave's avatar Madhura Bhave

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
parent 56475c19
...@@ -7787,8 +7787,7 @@ This layering is designed to separate code based on how likely it is to change b ...@@ -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. 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. 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`. 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.
See the {spring-boot-maven-plugin-docs}/#goals-build-image-parameters-details-layout[layout section] for more details.
For Gradle, refer to the {spring-boot-gradle-plugin-docs}/#packaging-layered-jars[packaging layered jars section] of the Gradle plugin documentation. 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 ==== Writing the Dockerfile
......
...@@ -287,3 +287,18 @@ The jar will then be split into layer folders which may include: ...@@ -287,3 +287,18 @@ The jar will then be split into layer folders which may include:
* `snapshots-dependencies` * `snapshots-dependencies`
* `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]
----
\ No newline at end of file
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
bootJar {
mainClassName 'com.example.ExampleApplication'
}
// tag::layered[]
bootJar {
layered {
includeLayerTools = false
}
}
// end::layered[]
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[]
...@@ -40,6 +40,8 @@ import org.gradle.api.java.archives.Attributes; ...@@ -40,6 +40,8 @@ import org.gradle.api.java.archives.Attributes;
import org.gradle.api.specs.Spec; import org.gradle.api.specs.Spec;
import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Internal; 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.gradle.api.tasks.bundling.Jar;
import org.springframework.boot.loader.tools.Layer; import org.springframework.boot.loader.tools.Layer;
...@@ -67,6 +69,8 @@ public class BootJar extends Jar implements BootArchive { ...@@ -67,6 +69,8 @@ public class BootJar extends Jar implements BootArchive {
private Layers layers; private Layers layers;
private LayerConfiguration layerConfiguration;
private static final String BOOT_INF_LAYERS = "BOOT-INF/layers"; private static final String BOOT_INF_LAYERS = "BOOT-INF/layers";
private final List<String> dependencies = new ArrayList<>(); private final List<String> dependencies = new ArrayList<>();
...@@ -164,20 +168,33 @@ public class BootJar extends Jar implements BootArchive { ...@@ -164,20 +168,33 @@ public class BootJar extends Jar implements BootArchive {
action.execute(enableLaunchScriptIfNecessary()); action.execute(enableLaunchScriptIfNecessary());
} }
@Optional
@Nested
@Input
public LayerConfiguration getLayerConfiguration() {
return this.layerConfiguration;
}
/** /**
* Configures the archive to have layers. * Configures the archive to have layers.
*/ */
public void layered() { public void layered() {
this.layers = Layers.IMPLICIT; enableLayers();
this.bootInf.into("lib", (spec) -> spec.from((Callable<File>) () -> { applyLayers();
String jarName = "spring-boot-jarmode-layertools.jar"; }
InputStream stream = getClass().getClassLoader().getResourceAsStream("META-INF/jarmode/" + jarName);
File taskTmp = new File(getProject().getBuildDir(), "tmp/" + getName()); private void applyLayers() {
taskTmp.mkdirs(); if (this.layerConfiguration.isIncludeLayerTools()) {
File layerToolsJar = new File(taskTmp, jarName); this.bootInf.into("lib", (spec) -> spec.from((Callable<File>) () -> {
FileCopyUtils.copy(stream, new FileOutputStream(layerToolsJar)); String jarName = "spring-boot-jarmode-layertools.jar";
return layerToolsJar; 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) -> { this.bootInf.eachFile((details) -> {
Layer layer = layerForFileDetails(details); Layer layer = layerForFileDetails(details);
if (layer != null) { if (layer != null) {
...@@ -188,6 +205,20 @@ public class BootJar extends Jar implements BootArchive { ...@@ -188,6 +205,20 @@ public class BootJar extends Jar implements BootArchive {
this.bootInf.into("", (spec) -> spec.from(createLayersIndex())); 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) { private Layer layerForFileDetails(FileCopyDetails details) {
String path = details.getPath(); String path = details.getPath();
if (path.startsWith("BOOT-INF/lib/")) { if (path.startsWith("BOOT-INF/lib/")) {
......
/*
* 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;
}
}
...@@ -29,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -29,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Integration tests for {@link BootJar}. * Integration tests for {@link BootJar}.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Madhura Bhave
*/ */
class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
...@@ -53,4 +54,13 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { ...@@ -53,4 +54,13 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
.isEqualTo(TaskOutcome.SUCCESS); .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);
}
} }
...@@ -25,6 +25,7 @@ import java.util.jar.JarFile; ...@@ -25,6 +25,7 @@ import java.util.jar.JarFile;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import org.gradle.api.Action;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
...@@ -129,6 +130,13 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> { ...@@ -129,6 +130,13 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
assertThat(entryNames).contains("BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar"); 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 @Test
void classpathIndexPointsToBootInfLibs() throws IOException { void classpathIndexPointsToBootInfLibs() throws IOException {
try (JarFile jarFile = new JarFile(createPopulatedJar())) { try (JarFile jarFile = new JarFile(createPopulatedJar())) {
...@@ -145,13 +153,22 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> { ...@@ -145,13 +153,22 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
return getTask().getArchiveFile().get().getAsFile(); return getTask().getArchiveFile().get().getAsFile();
} }
private File createLayeredJar() throws IOException { private File createLayeredJar(Action<LayerConfiguration> action) throws IOException {
addContent(); addContent();
getTask().layered(); if (action != null) {
getTask().layered(action);
}
else {
getTask().layered();
}
executeTask(); executeTask();
return getTask().getArchiveFile().get().getAsFile(); return getTask().getArchiveFile().get().getAsFile();
} }
private File createLayeredJar() throws IOException {
return createLayeredJar(null);
}
private void addContent() throws IOException { private void addContent() throws IOException {
BootJar bootJar = getTask(); BootJar bootJar = getTask();
bootJar.setMainClassName("com.example.Main"); bootJar.setMainClassName("com.example.Main");
......
...@@ -11,6 +11,8 @@ bootJar { ...@@ -11,6 +11,8 @@ bootJar {
} }
} }
if (project.hasProperty('layered') && project.getProperty('layered')) { if (project.hasProperty('layered') && project.getProperty('layered')) {
layered() layered {
includeLayerTools = project.hasProperty('excludeTools') && project.getProperty('excludeTools') ? false : true
}
} }
} }
...@@ -397,3 +397,56 @@ This example excludes any artifact belonging to the `com.foo` group: ...@@ -397,3 +397,56 @@ This example excludes any artifact belonging to the `com.foo` group:
</build> </build>
</project> </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>
----
\ No newline at end of file
...@@ -287,7 +287,21 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests { ...@@ -287,7 +287,21 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
File repackaged = new File(project, "jar/target/jar-layered-0.0.1.BUILD-SNAPSHOT.jar"); File repackaged = new File(project, "jar/target/jar-layered-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/layers/application/classes/") assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/layers/application/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/layers/dependencies/lib/jar-release") .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");
}); });
} }
......
<?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>
<?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>
<?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>
/*
* 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) {
}
}
<?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>
...@@ -22,7 +22,9 @@ ...@@ -22,7 +22,9 @@
<goal>repackage</goal> <goal>repackage</goal>
</goals> </goals>
<configuration> <configuration>
<layout>LAYERED_JAR</layout> <layered>
<enabled>true</enabled>
</layered>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>
......
...@@ -104,6 +104,13 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo ...@@ -104,6 +104,13 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
@Parameter(defaultValue = "false") @Parameter(defaultValue = "false")
public boolean includeSystemScope; 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. * Return a {@link Packager} configured for this MOJO.
* @param <P> the packager type * @param <P> the packager type
...@@ -119,6 +126,10 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo ...@@ -119,6 +126,10 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
getLog().info("Layout: " + this.layout); getLog().info("Layout: " + this.layout);
packager.setLayout(this.layout.layout()); packager.setLayout(this.layout.layout());
} }
if (this.layered != null && this.layered.isEnabled()) {
packager.setLayout(new LayeredJar());
packager.setIncludeRelevantJarModeJars(this.layered.isIncludeLayerTools());
}
return packager; return packager;
} }
...@@ -158,11 +169,6 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo ...@@ -158,11 +169,6 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
*/ */
JAR(new Jar()), JAR(new Jar()),
/**
* Layered Jar Layout.
*/
LAYERED_JAR(new LayeredJar()),
/** /**
* War Layout. * War Layout.
*/ */
......
/*
* 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;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment