Add support for customizing layers in Gradle
This commit adds configuration to the Spring Boot Gradle plugin that allows the names and contents of layers to be customized in the build configuration. Fixes gh-20296
This commit is contained in:
@@ -264,7 +264,7 @@ include::../gradle/packaging/boot-war-properties-launcher.gradle.kts[tags=proper
|
||||
|
||||
[[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.
|
||||
By default, the `bootJar` task 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:
|
||||
|
||||
@@ -280,12 +280,16 @@ include::../gradle/packaging/boot-jar-layered.gradle[tags=layered]
|
||||
include::../gradle/packaging/boot-jar-layered.gradle.kts[tags=layered]
|
||||
----
|
||||
|
||||
The jar will then be split into layer folders which may include:
|
||||
By default, the following layers are created:
|
||||
|
||||
* `application`
|
||||
* `resources`
|
||||
* `snapshots-dependencies`
|
||||
* `dependencies`
|
||||
* `dependencies` for any dependency whose version does not contain `SNAPSHOT`.
|
||||
* `snapshot-dependencies` for any dependency whose version contains `SNAPSHOT`.
|
||||
* `resources` for static resources at the default locations, i.e. `META-INF/resources/`, `resources/`, `static/`, `public/`.
|
||||
* `application` for any other classes and resources.
|
||||
|
||||
The layers order is important as it determines how likely previous layers can be cached when part of the application changes.
|
||||
The default order is `dependencies`, `snapshot-dependencies`, `resources`, and `application`.
|
||||
Content that is least likely to change should be added first, followed by layers that are more likely to change.
|
||||
|
||||
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.
|
||||
@@ -301,4 +305,33 @@ include::../gradle/packaging/boot-jar-layered-exclude-tools.gradle[tags=layered]
|
||||
.Kotlin
|
||||
----
|
||||
include::../gradle/packaging/boot-jar-layered-exclude-tools.gradle.kts[tags=layered]
|
||||
----
|
||||
----
|
||||
|
||||
[[packaging-layers-configuration]]
|
||||
===== Custom Layers configuration
|
||||
Depending on your application, you may want to tune how layers are created and add new ones.
|
||||
This can be done using configuration that lists the layers and their order as well as the strategies to apply to libraries and classes.
|
||||
|
||||
The following example shows what the implicit layer configuration described above does:
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Groovy
|
||||
----
|
||||
include::../gradle/packaging/boot-jar-layered-custom.gradle[tags=layered]
|
||||
----
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
include::../gradle/packaging/boot-jar-layered-custom.gradle.kts[tags=layered]
|
||||
----
|
||||
|
||||
Each `layerContent` closure defines a strategy to include or exclude an entry of the jar in a layer.
|
||||
When an entry matches a strategy, it is included in the layer and further strategies are ignored.
|
||||
This is illustrated by the `dependencies` and `application` layers that have a "catch-all" include filter used to add any libraries or classes that were not processed by previous strategies.
|
||||
|
||||
The content of a `libraries` layer can be customized using filters to `include` or `exclude` based on the dependency coordinates.
|
||||
The format is `groupId:artifactId[:version]`.
|
||||
In the example above, any artifact whose version ends with `SNAPSHOT` is going to be included in the `snapshot-dependencies` layer.
|
||||
|
||||
The content of a `classes` layer can be customized using filters to `included` or `exclude` based on location of the entry using Ant-style pattern matching.
|
||||
@@ -0,0 +1,41 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
}
|
||||
|
||||
bootJar {
|
||||
mainClassName 'com.example.ExampleApplication'
|
||||
}
|
||||
|
||||
// tag::layered[]
|
||||
bootJar {
|
||||
layers {
|
||||
layers "dependencies", "snapshot-dependencies", "resources", "application"
|
||||
libraries {
|
||||
layerContent("snapshot-dependencies") {
|
||||
coordinates {
|
||||
include "*:*:*SNAPSHOT"
|
||||
}
|
||||
}
|
||||
layerContent("dependencies") {
|
||||
coordinates {
|
||||
include "*:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
classes {
|
||||
layerContent("resources") {
|
||||
locations {
|
||||
include "META-INF/resources/**", "resources/**"
|
||||
include "static/**", "public/**"
|
||||
}
|
||||
}
|
||||
layerContent("application") {
|
||||
locations {
|
||||
include "**"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// end::layered[]
|
||||
@@ -0,0 +1,40 @@
|
||||
import org.springframework.boot.gradle.tasks.bundling.BootJar
|
||||
|
||||
plugins {
|
||||
java
|
||||
id("org.springframework.boot") version "{version}"
|
||||
}
|
||||
|
||||
// tag::layered[]
|
||||
tasks.getByName<BootJar>("bootJar") {
|
||||
layers {
|
||||
includeLayerTools = false
|
||||
layers("dependencies", "snapshot-dependencies", "resources", "application")
|
||||
libraries {
|
||||
layerContent("snapshot-dependencies") {
|
||||
coordinates {
|
||||
include("*:*:*SNAPSHOT")
|
||||
}
|
||||
}
|
||||
layerContent("dependencies") {
|
||||
coordinates {
|
||||
include("*:*")
|
||||
}
|
||||
}
|
||||
}
|
||||
classes {
|
||||
layerContent("resources") {
|
||||
locations {
|
||||
include("META-INF/resources/**", "resources/**")
|
||||
include("static/**", "public/**")
|
||||
}
|
||||
}
|
||||
layerContent("application") {
|
||||
locations {
|
||||
include("**")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// end::layered[]
|
||||
@@ -9,7 +9,7 @@ bootJar {
|
||||
|
||||
// tag::layered[]
|
||||
bootJar {
|
||||
layered {
|
||||
layers {
|
||||
includeLayerTools = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ tasks.getByName<BootJar>("bootJar") {
|
||||
|
||||
// tag::layered[]
|
||||
tasks.getByName<BootJar>("bootJar") {
|
||||
layered {
|
||||
layers {
|
||||
includeLayerTools = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,6 @@ bootJar {
|
||||
|
||||
// tag::layered[]
|
||||
bootJar {
|
||||
layered()
|
||||
layers()
|
||||
}
|
||||
// end::layered[]
|
||||
|
||||
@@ -11,6 +11,6 @@ tasks.getByName<BootJar>("bootJar") {
|
||||
|
||||
// tag::layered[]
|
||||
tasks.getByName<BootJar>("bootJar") {
|
||||
layered()
|
||||
layers()
|
||||
}
|
||||
// end::layered[]
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.gradle.api.Action;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.Task;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.internal.artifacts.publish.ArchivePublishArtifact;
|
||||
import org.gradle.api.plugins.ApplicationPlugin;
|
||||
@@ -87,11 +88,12 @@ final class JavaPluginAction implements PluginApplicationAction {
|
||||
bootJar.setDescription(
|
||||
"Assembles an executable jar archive containing the main classes and their dependencies.");
|
||||
bootJar.setGroup(BasePlugin.BUILD_GROUP);
|
||||
bootJar.classpath((Callable<FileCollection>) () -> {
|
||||
SourceSet mainSourceSet = javaPluginConvention(project).getSourceSets()
|
||||
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
|
||||
return mainSourceSet.getRuntimeClasspath();
|
||||
});
|
||||
SourceSet mainSourceSet = javaPluginConvention(project).getSourceSets()
|
||||
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
|
||||
bootJar.classpath((Callable<FileCollection>) () -> mainSourceSet.getRuntimeClasspath());
|
||||
Configuration runtimeClasspathConfiguration = project.getConfigurations()
|
||||
.getByName(mainSourceSet.getRuntimeClasspathConfigurationName());
|
||||
runtimeClasspathConfiguration.getIncoming().afterResolve(bootJar::resolvedDependencies);
|
||||
bootJar.conventionMapping("mainClassName", new MainClassConvention(project, bootJar::getClasspath));
|
||||
return bootJar;
|
||||
}
|
||||
|
||||
@@ -24,13 +24,18 @@ import java.io.InputStream;
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.gradle.api.Action;
|
||||
import org.gradle.api.artifacts.ArtifactCollection;
|
||||
import org.gradle.api.artifacts.ResolvableDependencies;
|
||||
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
|
||||
import org.gradle.api.file.CopySpec;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.file.FileCopyDetails;
|
||||
@@ -47,6 +52,8 @@ 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;
|
||||
import org.springframework.boot.loader.tools.LibraryCoordinates;
|
||||
import org.springframework.boot.loader.tools.layer.CustomLayers;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
/**
|
||||
@@ -54,6 +61,7 @@ import org.springframework.util.FileCopyUtils;
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Madhura Bhave
|
||||
* @author Scott Frederick
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class BootJar extends Jar implements BootArchive {
|
||||
@@ -75,6 +83,8 @@ public class BootJar extends Jar implements BootArchive {
|
||||
|
||||
private final List<String> dependencies = new ArrayList<>();
|
||||
|
||||
private final Map<String, String> coordinatesByFileName = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Creates a new {@code BootJar} task.
|
||||
*/
|
||||
@@ -177,12 +187,37 @@ public class BootJar extends Jar implements BootArchive {
|
||||
/**
|
||||
* Configures the archive to have layers.
|
||||
*/
|
||||
public void layered() {
|
||||
public void layers() {
|
||||
enableLayers();
|
||||
applyLayers();
|
||||
}
|
||||
|
||||
public void layers(Action<LayerConfiguration> action) {
|
||||
action.execute(enableLayers());
|
||||
}
|
||||
|
||||
private LayerConfiguration enableLayers() {
|
||||
if (this.layerConfiguration == null) {
|
||||
this.layerConfiguration = new LayerConfiguration();
|
||||
}
|
||||
|
||||
return this.layerConfiguration;
|
||||
}
|
||||
|
||||
private void applyLayers() {
|
||||
if (this.layerConfiguration == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.layerConfiguration.getLayers() == null || this.layerConfiguration.getLayers().isEmpty()) {
|
||||
this.layers = Layers.IMPLICIT;
|
||||
}
|
||||
else {
|
||||
List<Layer> customLayers = this.layerConfiguration.getLayers().stream().map(Layer::new)
|
||||
.collect(Collectors.toList());
|
||||
this.layers = new CustomLayers(customLayers, this.layerConfiguration.getClasses(),
|
||||
this.layerConfiguration.getLibraries());
|
||||
}
|
||||
|
||||
if (this.layerConfiguration.isIncludeLayerTools()) {
|
||||
this.bootInf.into("lib", (spec) -> spec.from((Callable<File>) () -> {
|
||||
String jarName = "spring-boot-jarmode-layertools.jar";
|
||||
@@ -204,24 +239,13 @@ 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/")) {
|
||||
return this.layers.getLayer(new Library(details.getFile(), null));
|
||||
String coordinates = this.coordinatesByFileName.get(details.getName());
|
||||
LibraryCoordinates libraryCoordinates = (coordinates != null) ? new LibraryCoordinates(coordinates)
|
||||
: new LibraryCoordinates("?:?:?");
|
||||
return this.layers.getLayer(new Library(null, details.getFile(), null, false, libraryCoordinates));
|
||||
}
|
||||
if (path.startsWith("BOOT-INF/classes/")) {
|
||||
return this.layers.getLayer(details.getSourcePath());
|
||||
@@ -248,9 +272,16 @@ public class BootJar extends Jar implements BootArchive {
|
||||
}
|
||||
}
|
||||
|
||||
private void resolveCoordinatesForFiles(ResolvableDependencies resolvableDependencies) {
|
||||
ArtifactCollection resolvedArtifactResults = resolvableDependencies.getArtifacts();
|
||||
Set<ResolvedArtifactResult> artifacts = resolvedArtifactResults.getArtifacts();
|
||||
artifacts.forEach((artifact) -> this.coordinatesByFileName.put(artifact.getFile().getName(),
|
||||
artifact.getId().getComponentIdentifier().getDisplayName()));
|
||||
}
|
||||
|
||||
@Input
|
||||
boolean isLayered() {
|
||||
return this.layers != null;
|
||||
return this.layerConfiguration != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -285,6 +316,13 @@ public class BootJar extends Jar implements BootArchive {
|
||||
this.support.setExcludeDevtools(excludeDevtools);
|
||||
}
|
||||
|
||||
public void resolvedDependencies(ResolvableDependencies resolvableDependencies) {
|
||||
if (resolvableDependencies != null) {
|
||||
resolveCoordinatesForFiles(resolvableDependencies);
|
||||
}
|
||||
applyLayers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code CopySpec} that can be used to add content to the {@code BOOT-INF}
|
||||
* directory of the jar.
|
||||
|
||||
@@ -16,18 +16,42 @@
|
||||
|
||||
package org.springframework.boot.gradle.tasks.bundling;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.gradle.api.Action;
|
||||
import org.gradle.api.tasks.Input;
|
||||
|
||||
import org.springframework.boot.loader.tools.layer.classes.FilteredResourceStrategy;
|
||||
import org.springframework.boot.loader.tools.layer.classes.LocationFilter;
|
||||
import org.springframework.boot.loader.tools.layer.classes.ResourceFilter;
|
||||
import org.springframework.boot.loader.tools.layer.classes.ResourceStrategy;
|
||||
import org.springframework.boot.loader.tools.layer.library.CoordinateFilter;
|
||||
import org.springframework.boot.loader.tools.layer.library.FilteredLibraryStrategy;
|
||||
import org.springframework.boot.loader.tools.layer.library.LibraryFilter;
|
||||
import org.springframework.boot.loader.tools.layer.library.LibraryStrategy;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Encapsulates the configuration for a layered jar.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @author Scott Frederick
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class LayerConfiguration {
|
||||
|
||||
private boolean includeLayerTools = true;
|
||||
|
||||
private List<String> layerNames = new ArrayList<>();
|
||||
|
||||
private List<ResourceStrategy> resourceStrategies = new ArrayList<>();
|
||||
|
||||
private List<LibraryStrategy> libraryStrategies = new ArrayList<>();
|
||||
|
||||
private StrategySpec strategySpec;
|
||||
|
||||
/**
|
||||
* Whether to include the layer tools jar.
|
||||
* @return true if layer tools is included
|
||||
@@ -41,4 +65,164 @@ public class LayerConfiguration {
|
||||
this.includeLayerTools = includeLayerTools;
|
||||
}
|
||||
|
||||
@Input
|
||||
public List<String> getLayers() {
|
||||
return this.layerNames;
|
||||
}
|
||||
|
||||
public void layers(String... layers) {
|
||||
this.layerNames = Arrays.asList(layers);
|
||||
}
|
||||
|
||||
public void layers(List<String> layers) {
|
||||
this.layerNames = layers;
|
||||
}
|
||||
|
||||
@Input
|
||||
public List<ResourceStrategy> getClasses() {
|
||||
return this.resourceStrategies;
|
||||
}
|
||||
|
||||
public void classes(ResourceStrategy... resourceStrategies) {
|
||||
this.resourceStrategies = Arrays.asList(resourceStrategies);
|
||||
}
|
||||
|
||||
public void classes(Action<LayerConfiguration> config) {
|
||||
this.strategySpec = StrategySpec.forResources();
|
||||
config.execute(this);
|
||||
}
|
||||
|
||||
@Input
|
||||
public List<LibraryStrategy> getLibraries() {
|
||||
return this.libraryStrategies;
|
||||
}
|
||||
|
||||
public void libraries(LibraryStrategy... strategies) {
|
||||
this.libraryStrategies = Arrays.asList(strategies);
|
||||
}
|
||||
|
||||
public void libraries(Action<LayerConfiguration> configure) {
|
||||
this.strategySpec = StrategySpec.forLibraries();
|
||||
configure.execute(this);
|
||||
}
|
||||
|
||||
public void layerContent(String layerName, Action<LayerConfiguration> config) {
|
||||
this.strategySpec.newStrategy();
|
||||
config.execute(this);
|
||||
if (this.strategySpec.isLibrariesStrategy()) {
|
||||
this.libraryStrategies.add(new FilteredLibraryStrategy(layerName, this.strategySpec.libraryFilters()));
|
||||
}
|
||||
else {
|
||||
this.resourceStrategies.add(new FilteredResourceStrategy(layerName, this.strategySpec.resourceFilters()));
|
||||
}
|
||||
}
|
||||
|
||||
public void coordinates(Action<LayerConfiguration> config) {
|
||||
Assert.state(this.strategySpec.isLibrariesStrategy(),
|
||||
"The 'coordinates' filter must be used only with libraries");
|
||||
this.strategySpec.newFilter();
|
||||
config.execute(this);
|
||||
this.strategySpec
|
||||
.addLibraryFilter(new CoordinateFilter(this.strategySpec.includes(), this.strategySpec.excludes()));
|
||||
}
|
||||
|
||||
public void locations(Action<LayerConfiguration> config) {
|
||||
Assert.state(this.strategySpec.isResourcesStrategy(), "The 'locations' filter must be used only with classes");
|
||||
this.strategySpec.newFilter();
|
||||
config.execute(this);
|
||||
this.strategySpec
|
||||
.addResourceFilter(new LocationFilter(this.strategySpec.includes(), this.strategySpec.excludes()));
|
||||
}
|
||||
|
||||
public void include(String... includes) {
|
||||
this.strategySpec.include(includes);
|
||||
}
|
||||
|
||||
public void exclude(String... excludes) {
|
||||
this.strategySpec.exclude(excludes);
|
||||
}
|
||||
|
||||
private static final class StrategySpec {
|
||||
|
||||
private enum TYPE {
|
||||
|
||||
LIBRARIES, RESOURCES;
|
||||
|
||||
}
|
||||
|
||||
private final TYPE type;
|
||||
|
||||
private List<LibraryFilter> libraryFilters;
|
||||
|
||||
private List<ResourceFilter> resourceFilters;
|
||||
|
||||
private List<String> filterIncludes;
|
||||
|
||||
private List<String> filterExcludes;
|
||||
|
||||
private StrategySpec(TYPE type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
private boolean isLibrariesStrategy() {
|
||||
return this.type == TYPE.LIBRARIES;
|
||||
}
|
||||
|
||||
private boolean isResourcesStrategy() {
|
||||
return this.type == TYPE.RESOURCES;
|
||||
}
|
||||
|
||||
private void newStrategy() {
|
||||
this.libraryFilters = new ArrayList<>();
|
||||
this.resourceFilters = new ArrayList<>();
|
||||
newFilter();
|
||||
}
|
||||
|
||||
private void newFilter() {
|
||||
this.filterIncludes = new ArrayList<>();
|
||||
this.filterExcludes = new ArrayList<>();
|
||||
}
|
||||
|
||||
private List<LibraryFilter> libraryFilters() {
|
||||
return this.libraryFilters;
|
||||
}
|
||||
|
||||
private void addLibraryFilter(LibraryFilter filter) {
|
||||
this.libraryFilters.add(filter);
|
||||
}
|
||||
|
||||
private List<ResourceFilter> resourceFilters() {
|
||||
return this.resourceFilters;
|
||||
}
|
||||
|
||||
private void addResourceFilter(ResourceFilter filter) {
|
||||
this.resourceFilters.add(filter);
|
||||
}
|
||||
|
||||
private List<String> includes() {
|
||||
return this.filterIncludes;
|
||||
}
|
||||
|
||||
private void include(String... includes) {
|
||||
this.filterIncludes.addAll(Arrays.asList(includes));
|
||||
}
|
||||
|
||||
private void exclude(String... excludes) {
|
||||
this.filterIncludes.addAll(Arrays.asList(excludes));
|
||||
}
|
||||
|
||||
private List<String> excludes() {
|
||||
return this.filterExcludes;
|
||||
}
|
||||
|
||||
private static StrategySpec forLibraries() {
|
||||
return new StrategySpec(TYPE.LIBRARIES);
|
||||
}
|
||||
|
||||
private static StrategySpec forResources() {
|
||||
return new StrategySpec(TYPE.RESOURCES);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,7 +16,14 @@
|
||||
|
||||
package org.springframework.boot.gradle.tasks.bundling;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
import org.gradle.testkit.runner.InvalidRunnerConfigurationException;
|
||||
import org.gradle.testkit.runner.TaskOutcome;
|
||||
@@ -38,8 +45,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void upToDateWhenBuiltTwiceWithLayers()
|
||||
throws InvalidRunnerConfigurationException, UnexpectedBuildFailure, IOException {
|
||||
void upToDateWhenBuiltTwiceWithLayers() throws InvalidRunnerConfigurationException, UnexpectedBuildFailure {
|
||||
assertThat(this.gradleBuild.build("-Playered=true", "bootJar").task(":bootJar").getOutcome())
|
||||
.isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(this.gradleBuild.build("-Playered=true", "bootJar").task(":bootJar").getOutcome())
|
||||
@@ -48,7 +54,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
|
||||
|
||||
@TestTemplate
|
||||
void notUpToDateWhenBuiltWithoutLayersAndThenWithLayers()
|
||||
throws InvalidRunnerConfigurationException, UnexpectedBuildFailure, IOException {
|
||||
throws InvalidRunnerConfigurationException, UnexpectedBuildFailure {
|
||||
assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(this.gradleBuild.build("-Playered=true", "bootJar").task(":bootJar").getOutcome())
|
||||
.isEqualTo(TaskOutcome.SUCCESS);
|
||||
@@ -56,11 +62,76 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
|
||||
|
||||
@TestTemplate
|
||||
void notUpToDateWhenBuiltWithLayersAndToolsAndThenWithLayersAndWithoutTools()
|
||||
throws InvalidRunnerConfigurationException, UnexpectedBuildFailure, IOException {
|
||||
throws InvalidRunnerConfigurationException, UnexpectedBuildFailure {
|
||||
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);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void implicitLayers() throws IOException {
|
||||
writeMainClass();
|
||||
writeResource();
|
||||
assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) {
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar"))
|
||||
.isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/commons-lang3-3.9.jar")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/snapshot-dependencies/lib/commons-io-2.7-SNAPSHOT.jar"))
|
||||
.isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/application/classes/example/Main.class")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/resources/classes/static/file.txt")).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void customLayers() throws IOException {
|
||||
writeMainClass();
|
||||
writeResource();
|
||||
assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) {
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar"))
|
||||
.isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/commons-dependencies/lib/commons-lang3-3.9.jar")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/snapshot-dependencies/lib/commons-io-2.7-SNAPSHOT.jar"))
|
||||
.isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/app/classes/example/Main.class")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/layers/static/classes/static/file.txt")).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeMainClass() {
|
||||
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example");
|
||||
examplePackage.mkdirs();
|
||||
File main = new File(examplePackage, "Main.java");
|
||||
try (PrintWriter writer = new PrintWriter(new FileWriter(main))) {
|
||||
writer.println("package example;");
|
||||
writer.println();
|
||||
writer.println("import java.io.IOException;");
|
||||
writer.println();
|
||||
writer.println("public class Main {");
|
||||
writer.println();
|
||||
writer.println(" public static void main(String[] args) {");
|
||||
writer.println(" }");
|
||||
writer.println();
|
||||
writer.println("}");
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeResource() {
|
||||
try {
|
||||
Path path = this.gradleBuild.getProjectDir().toPath()
|
||||
.resolve(Paths.get("src", "main", "resources", "static", "file.txt"));
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.createFile(path);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,21 +20,38 @@ import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.gradle.api.Action;
|
||||
import org.gradle.api.artifacts.ArtifactCollection;
|
||||
import org.gradle.api.artifacts.ResolvableDependencies;
|
||||
import org.gradle.api.artifacts.component.ComponentArtifactIdentifier;
|
||||
import org.gradle.api.artifacts.component.ComponentIdentifier;
|
||||
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.loader.tools.layer.classes.FilteredResourceStrategy;
|
||||
import org.springframework.boot.loader.tools.layer.classes.LocationFilter;
|
||||
import org.springframework.boot.loader.tools.layer.library.CoordinateFilter;
|
||||
import org.springframework.boot.loader.tools.layer.library.FilteredLibraryStrategy;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link BootJar}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Madhura Bhave
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class BootJarTests extends AbstractBootArchiveTests<BootJar> {
|
||||
|
||||
@@ -102,6 +119,36 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
|
||||
.contains("BOOT-INF/layers/resources/classes/static/test.css");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenJarIsLayeredWithCustomStrategiesThenContentsAreMovedToLayerDirectories() throws IOException {
|
||||
File jar = createLayeredJar((configuration) -> {
|
||||
configuration.layers("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application");
|
||||
configuration.libraries(createLibraryStrategy("my-snapshot-deps", "com.example:*:*.SNAPSHOT"),
|
||||
createLibraryStrategy("my-internal-deps", "com.example:*:*"),
|
||||
createLibraryStrategy("my-deps", "*:*"));
|
||||
configuration.classes(createResourceStrategy("resources", "static/**"),
|
||||
createResourceStrategy("application", "**"));
|
||||
});
|
||||
List<String> entryNames = getEntryNames(jar);
|
||||
assertThat(entryNames)
|
||||
.containsSubsequence("BOOT-INF/layers/my-internal-deps/lib/first-library.jar",
|
||||
"BOOT-INF/layers/my-internal-deps/lib/second-library.jar")
|
||||
.contains("BOOT-INF/layers/my-snapshot-deps/lib/third-library-SNAPSHOT.jar")
|
||||
.containsSubsequence("BOOT-INF/layers/application/classes/com/example/Application.class",
|
||||
"BOOT-INF/layers/application/classes/application.properties")
|
||||
.contains("BOOT-INF/layers/resources/classes/static/test.css");
|
||||
}
|
||||
|
||||
private FilteredLibraryStrategy createLibraryStrategy(String layerName, String... includes) {
|
||||
return new FilteredLibraryStrategy(layerName,
|
||||
Collections.singletonList(new CoordinateFilter(Arrays.asList(includes), Collections.emptyList())));
|
||||
}
|
||||
|
||||
private FilteredResourceStrategy createResourceStrategy(String layerName, String... includes) {
|
||||
return new FilteredResourceStrategy(layerName,
|
||||
Collections.singletonList(new LocationFilter(Arrays.asList(includes), Collections.emptyList())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenJarIsLayeredJarsInLibAreStored() throws IOException {
|
||||
try (JarFile jarFile = new JarFile(createLayeredJar())) {
|
||||
@@ -154,13 +201,13 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
|
||||
}
|
||||
|
||||
private File createLayeredJar(Action<LayerConfiguration> action) throws IOException {
|
||||
addContent();
|
||||
if (action != null) {
|
||||
getTask().layered(action);
|
||||
getTask().layers(action);
|
||||
}
|
||||
else {
|
||||
getTask().layered();
|
||||
getTask().layers();
|
||||
}
|
||||
addContent();
|
||||
executeTask();
|
||||
return getTask().getArchiveFile().get().getAsFile();
|
||||
}
|
||||
@@ -186,6 +233,33 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
|
||||
css.createNewFile();
|
||||
bootJar.classpath(classesJavaMain, resourcesMain, jarFile("first-library.jar"), jarFile("second-library.jar"),
|
||||
jarFile("third-library-SNAPSHOT.jar"));
|
||||
|
||||
Set<ResolvedArtifactResult> resolvedArtifacts = new HashSet<>();
|
||||
resolvedArtifacts.add(mockLibraryArtifact("first-library.jar", "com.example:first-library:1.0.0"));
|
||||
resolvedArtifacts.add(mockLibraryArtifact("second-library.jar", "com.example:second-library:1.0.0"));
|
||||
resolvedArtifacts
|
||||
.add(mockLibraryArtifact("third-library-SNAPSHOT.jar", "com.example:third-library:1.0.0.SNAPSHOT"));
|
||||
|
||||
ArtifactCollection artifacts = mock(ArtifactCollection.class);
|
||||
given(artifacts.getArtifacts()).willReturn(resolvedArtifacts);
|
||||
|
||||
ResolvableDependencies deps = mock(ResolvableDependencies.class);
|
||||
given(deps.getArtifacts()).willReturn(artifacts);
|
||||
bootJar.resolvedDependencies(deps);
|
||||
}
|
||||
|
||||
private ResolvedArtifactResult mockLibraryArtifact(String fileName, String coordinates) {
|
||||
ComponentIdentifier libraryId = mock(ComponentIdentifier.class);
|
||||
given(libraryId.getDisplayName()).willReturn(coordinates);
|
||||
|
||||
ComponentArtifactIdentifier libraryArtifactId = mock(ComponentArtifactIdentifier.class);
|
||||
given(libraryArtifactId.getComponentIdentifier()).willReturn(libraryId);
|
||||
|
||||
ResolvedArtifactResult libraryArtifact = mock(ResolvedArtifactResult.class);
|
||||
given(libraryArtifact.getFile()).willReturn(new File(fileName));
|
||||
given(libraryArtifact.getId()).willReturn(libraryArtifactId);
|
||||
|
||||
return libraryArtifact;
|
||||
}
|
||||
|
||||
private List<String> entryLines(JarFile jarFile, String entryName) throws IOException {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
}
|
||||
|
||||
bootJar {
|
||||
mainClassName = 'com.example.Application'
|
||||
layers {
|
||||
layers "dependencies", "commons-dependencies", "snapshot-dependencies", "static", "app"
|
||||
libraries {
|
||||
layerContent("snapshot-dependencies") { coordinates { include "*:*:*SNAPSHOT" } }
|
||||
layerContent("commons-dependencies") { coordinates { include "org.apache.commons:*" } }
|
||||
layerContent("dependencies") { coordinates { include "*:*" } }
|
||||
}
|
||||
classes {
|
||||
layerContent("static") {
|
||||
locations {
|
||||
include "META-INF/resources/**", "resources/**"
|
||||
include "static/**", "public/**"
|
||||
}
|
||||
}
|
||||
layerContent("app") {
|
||||
locations {
|
||||
include "**"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url "https://repository.apache.org/content/repositories/snapshots" }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.apache.commons:commons-lang3:3.9")
|
||||
implementation("commons-io:commons-io:2.7-SNAPSHOT")
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
}
|
||||
|
||||
bootJar {
|
||||
mainClassName = 'com.example.Application'
|
||||
layers()
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url "https://repository.apache.org/content/repositories/snapshots" }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.apache.commons:commons-lang3:3.9")
|
||||
implementation("commons-io:commons-io:2.7-SNAPSHOT")
|
||||
}
|
||||
@@ -11,7 +11,7 @@ bootJar {
|
||||
}
|
||||
}
|
||||
if (project.hasProperty('layered') && project.getProperty('layered')) {
|
||||
layered {
|
||||
layers {
|
||||
includeLayerTools = project.hasProperty('excludeTools') && project.getProperty('excludeTools') ? false : true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user