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:
Scott Frederick
2020-03-10 16:09:44 -07:00
parent c4a55a5fb4
commit f2dadf5a87
23 changed files with 643 additions and 59 deletions

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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);
}
}
}