Add support for layered jars in gradle plugin
Closes gh-19792 Co-authored-by: Andy Wilkinson <awilkinson@pivotal.io>
This commit is contained in:
parent
9ff50f903f
commit
df5b0f1163
|
|
@ -272,3 +272,29 @@ include::../gradle/packaging/boot-war-properties-launcher.gradle[tags=properties
|
|||
include::../gradle/packaging/boot-war-properties-launcher.gradle.kts[tags=properties-launcher]
|
||||
----
|
||||
|
||||
|
||||
[[packaging-layered-jars]]
|
||||
==== Packaging layered jars
|
||||
|
||||
By default, the `bootJar` tasks builds an archive that 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,groovy,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Groovy
|
||||
----
|
||||
include::../gradle/packaging/boot-jar-layered.gradle[tags=layered]
|
||||
----
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
include::../gradle/packaging/boot-jar-layered.gradle.kts[tags=layered]
|
||||
----
|
||||
|
||||
The jar will then be split into layer folders which may include:
|
||||
|
||||
* `application`
|
||||
* `resources`
|
||||
* `snapshots-dependencies`
|
||||
* `dependencies`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
}
|
||||
|
||||
bootJar {
|
||||
mainClassName 'com.example.ExampleApplication'
|
||||
}
|
||||
|
||||
// tag::layered[]
|
||||
bootJar {
|
||||
layered()
|
||||
}
|
||||
// end::layered[]
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
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()
|
||||
}
|
||||
// end::layered[]
|
||||
|
|
@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -16,7 +16,10 @@
|
|||
|
||||
package org.springframework.boot.gradle.tasks.bundling;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
|
|
@ -26,14 +29,21 @@ import org.gradle.api.file.FileCollection;
|
|||
import org.gradle.api.file.FileCopyDetails;
|
||||
import org.gradle.api.file.FileTreeElement;
|
||||
import org.gradle.api.internal.file.copy.CopyAction;
|
||||
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.bundling.Jar;
|
||||
|
||||
import org.springframework.boot.loader.tools.Layer;
|
||||
import org.springframework.boot.loader.tools.Layers;
|
||||
import org.springframework.boot.loader.tools.Library;
|
||||
|
||||
/**
|
||||
* A custom {@link Jar} task that produces a Spring Boot executable jar.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Madhura Bhave
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class BootJar extends Jar implements BootArchive {
|
||||
|
|
@ -47,6 +57,10 @@ public class BootJar extends Jar implements BootArchive {
|
|||
|
||||
private FileCollection classpath;
|
||||
|
||||
private Layers layers;
|
||||
|
||||
private static final String BOOT_INF_LAYERS = "BOOT-INF/layers/";
|
||||
|
||||
/**
|
||||
* Creates a new {@code BootJar} task.
|
||||
*/
|
||||
|
|
@ -73,6 +87,12 @@ public class BootJar extends Jar implements BootArchive {
|
|||
@Override
|
||||
public void copy() {
|
||||
this.support.configureManifest(this, getMainClassName(), "BOOT-INF/classes/", "BOOT-INF/lib/");
|
||||
Attributes attributes = this.getManifest().getAttributes();
|
||||
if (this.layers != null) {
|
||||
attributes.remove("Spring-Boot-Classes");
|
||||
attributes.remove("Spring-Boot-Lib");
|
||||
attributes.putIfAbsent("Spring-Boot-Layers-Index", "BOOT-INF/layers.idx");
|
||||
}
|
||||
super.copy();
|
||||
}
|
||||
|
||||
|
|
@ -122,6 +142,56 @@ public class BootJar extends Jar implements BootArchive {
|
|||
action.execute(enableLaunchScriptIfNecessary());
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the archive to have layers.
|
||||
*/
|
||||
public void layered() {
|
||||
this.layers = Layers.IMPLICIT;
|
||||
this.bootInf.eachFile((details) -> {
|
||||
Layer layer = layerForFileDetails(details);
|
||||
if (layer != null) {
|
||||
details.setPath(
|
||||
BOOT_INF_LAYERS + "/" + layer + "/" + details.getPath().substring("BOOT-INF/".length()));
|
||||
}
|
||||
}).setIncludeEmptyDirs(false);
|
||||
this.bootInf.into("", (spec) -> spec.from(createLayersIndex()));
|
||||
}
|
||||
|
||||
private Layer layerForFileDetails(FileCopyDetails details) {
|
||||
String path = details.getPath();
|
||||
if (path.startsWith("BOOT-INF/lib/")) {
|
||||
return this.layers.getLayer(new Library(details.getFile(), null));
|
||||
}
|
||||
if (path.startsWith("BOOT-INF/classes/")) {
|
||||
return this.layers.getLayer(details.getSourcePath());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private File createLayersIndex() {
|
||||
try {
|
||||
StringWriter content = new StringWriter();
|
||||
BufferedWriter writer = new BufferedWriter(content);
|
||||
for (Layer layer : this.layers) {
|
||||
writer.write(layer.toString());
|
||||
writer.write("\n");
|
||||
}
|
||||
writer.flush();
|
||||
File source = getProject().getResources().getText().fromString(content.toString()).asFile();
|
||||
File indexFile = new File(source.getParentFile(), "layers.idx");
|
||||
source.renameTo(indexFile);
|
||||
return indexFile;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException("Failed to create layers.idx", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Input
|
||||
boolean isLayered() {
|
||||
return this.layers != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileCollection getClasspath() {
|
||||
return this.classpath;
|
||||
|
|
|
|||
|
|
@ -178,6 +178,18 @@ class PackagingDocumentationTests {
|
|||
assertThat(bootJar).isFile();
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void bootJarLayered() throws IOException {
|
||||
this.gradleBuild.script("src/docs/gradle/packaging/boot-jar-layered").build("bootJar");
|
||||
File file = new File(this.gradleBuild.getProjectDir(),
|
||||
"build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar");
|
||||
assertThat(file).isFile();
|
||||
try (JarFile jar = new JarFile(file)) {
|
||||
JarEntry entry = jar.getJarEntry("BOOT-INF/layers.idx");
|
||||
assertThat(entry).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
protected void jarFile(File file) throws IOException {
|
||||
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(file))) {
|
||||
jar.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -16,6 +16,15 @@
|
|||
|
||||
package org.springframework.boot.gradle.tasks.bundling;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.gradle.testkit.runner.InvalidRunnerConfigurationException;
|
||||
import org.gradle.testkit.runner.TaskOutcome;
|
||||
import org.gradle.testkit.runner.UnexpectedBuildFailure;
|
||||
import org.junit.jupiter.api.TestTemplate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link BootJar}.
|
||||
*
|
||||
|
|
@ -27,4 +36,21 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
|
|||
super("bootJar");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void upToDateWhenBuiltTwiceWithLayers()
|
||||
throws InvalidRunnerConfigurationException, UnexpectedBuildFailure, IOException {
|
||||
assertThat(this.gradleBuild.build("-Playered=true", "bootJar").task(":bootJar").getOutcome())
|
||||
.isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(this.gradleBuild.build("-Playered=true", "bootJar").task(":bootJar").getOutcome())
|
||||
.isEqualTo(TaskOutcome.UP_TO_DATE);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void notUpToDateWhenBuiltWithoutLayersAndThenWithLayers()
|
||||
throws InvalidRunnerConfigurationException, UnexpectedBuildFailure, IOException {
|
||||
assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(this.gradleBuild.build("-Playered=true", "bootJar").task(":bootJar").getOutcome())
|
||||
.isEqualTo(TaskOutcome.SUCCESS);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ package org.springframework.boot.gradle.tasks.bundling;
|
|||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
|
@ -28,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
* Tests for {@link BootJar}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
class BootJarTests extends AbstractBootArchiveTests<BootJar> {
|
||||
|
||||
|
|
@ -57,6 +60,48 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void layers() throws IOException {
|
||||
BootJar bootJar = getTask();
|
||||
bootJar.setMainClassName("com.example.Main");
|
||||
bootJar.layered();
|
||||
File classesJavaMain = new File(this.temp, "classes/java/main");
|
||||
File applicationClass = new File(classesJavaMain, "com/example/Application.class");
|
||||
applicationClass.getParentFile().mkdirs();
|
||||
applicationClass.createNewFile();
|
||||
File resourcesMain = new File(this.temp, "resources/main");
|
||||
File applicationProperties = new File(resourcesMain, "application.properties");
|
||||
applicationProperties.getParentFile().mkdirs();
|
||||
applicationProperties.createNewFile();
|
||||
File staticResources = new File(resourcesMain, "static");
|
||||
staticResources.mkdir();
|
||||
File css = new File(staticResources, "test.css");
|
||||
css.createNewFile();
|
||||
bootJar.classpath(classesJavaMain, resourcesMain, jarFile("first-library.jar"), jarFile("second-library.jar"),
|
||||
jarFile("third-library-SNAPSHOT.jar"));
|
||||
bootJar.requiresUnpack("second-library.jar");
|
||||
executeTask();
|
||||
List<String> entryNames = getEntryNames(bootJar.getArchiveFile().get().getAsFile());
|
||||
assertThat(entryNames).containsSubsequence("org/springframework/boot/loader/",
|
||||
"BOOT-INF/layers/application/classes/com/example/Application.class",
|
||||
"BOOT-INF/layers/resources/classes/static/test.css",
|
||||
"BOOT-INF/layers/application/classes/application.properties",
|
||||
"BOOT-INF/layers/dependencies/lib/first-library.jar",
|
||||
"BOOT-INF/layers/dependencies/lib/second-library.jar",
|
||||
"BOOT-INF/layers/snapshot-dependencies/lib/third-library-SNAPSHOT.jar");
|
||||
assertThat(entryNames).doesNotContain("BOOT-INF/classes").doesNotContain("BOOT-INF/lib")
|
||||
.doesNotContain("BOOT-INF/com/");
|
||||
try (JarFile jarFile = new JarFile(bootJar.getArchiveFile().get().getAsFile())) {
|
||||
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes")).isEqualTo(null);
|
||||
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo(null);
|
||||
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Layers-Index"))
|
||||
.isEqualTo("BOOT-INF/layers.idx");
|
||||
try (InputStream input = jarFile.getInputStream(jarFile.getEntry("BOOT-INF/layers.idx"))) {
|
||||
assertThat(input).hasContent("dependencies\nsnapshot-dependencies\nresources\napplication\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void executeTask() {
|
||||
getTask().copy();
|
||||
|
|
|
|||
|
|
@ -10,4 +10,7 @@ bootJar {
|
|||
properties 'prop' : project.hasProperty('launchScriptProperty') ? launchScriptProperty : 'default'
|
||||
}
|
||||
}
|
||||
if (project.hasProperty('layered') && project.getProperty('layered')) {
|
||||
layered()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue