Refine layer customization for Maven and Gradle

Simplify layer customization logic for both Maven and Gradle and
refactor some internals of the Gradle plugin.

Both Maven and Gradle now use a simpler customization format that
consists of `application`, `dependencies` and `layer order` sections.
The `application`, `dependencies` configurations support one or more
`into` blocks that are used to select content for a specific layer.

Closes gh-20526
This commit is contained in:
Phillip Webb
2020-03-24 17:00:39 -07:00
parent 14718f3e8a
commit 7bc7d86ad4
61 changed files with 2047 additions and 2054 deletions

View File

@@ -9,27 +9,17 @@ bootJar {
// tag::layered[]
bootJar {
layers {
layersOrder "dependencies", "snapshot-dependencies", "application"
libraries {
layerContent("snapshot-dependencies") {
coordinates {
include "*:*:*SNAPSHOT"
}
}
layerContent("dependencies") {
coordinates {
include "*:*"
}
}
}
layered {
application {
layerContent("application") {
locations {
include "**"
}
}
intoLayer("application")
}
dependencies {
intoLayer("snapshot-dependencies") {
include "*:*:*SNAPSHOT"
}
intoLayer("dependencies")
}
layerOrder "dependencies", "snapshot-dependencies", "application"
}
}
// end::layered[]

View File

@@ -7,27 +7,17 @@ plugins {
// tag::layered[]
tasks.getByName<BootJar>("bootJar") {
layers {
layersOrder("dependencies", "snapshot-dependencies", "application")
libraries {
layerContent("snapshot-dependencies") {
coordinates {
include("*:*:*SNAPSHOT")
}
}
layerContent("dependencies") {
coordinates {
include("*:*")
}
}
}
layered {
application {
layerContent("application") {
locations {
include("**")
}
}
intoLayer("application")
}
dependencies {
intoLayer("snapshot-dependencies") {
include("*:*:*SNAPSHOT")
}
intoLayer("dependencies") {
}
layersOrder("dependencies", "snapshot-dependencies", "application")
}
}
// end::layered[]

View File

@@ -9,6 +9,6 @@ bootJar {
// tag::layered[]
bootJar {
layers()
layered()
}
// end::layered[]

View File

@@ -11,6 +11,6 @@ tasks.getByName<BootJar>("bootJar") {
// tag::layered[]
tasks.getByName<BootJar>("bootJar") {
layers()
layered()
}
// end::layered[]

View File

@@ -27,7 +27,6 @@ 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.dsl.LazyPublishArtifact;
import org.gradle.api.plugins.ApplicationPlugin;
@@ -94,9 +93,6 @@ final class JavaPluginAction implements PluginApplicationAction {
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));
});
}

View File

@@ -27,6 +27,7 @@ import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import org.gradle.api.file.CopySpec;
import org.gradle.api.file.FileCopyDetails;
import org.gradle.api.file.FileTreeElement;
import org.gradle.api.file.RelativePath;
@@ -34,6 +35,7 @@ import org.gradle.api.internal.file.copy.CopyAction;
import org.gradle.api.internal.file.copy.CopyActionProcessingStream;
import org.gradle.api.internal.file.copy.FileCopyDetailsInternal;
import org.gradle.api.java.archives.Attributes;
import org.gradle.api.java.archives.Manifest;
import org.gradle.api.specs.Spec;
import org.gradle.api.specs.Specs;
import org.gradle.api.tasks.WorkResult;
@@ -44,6 +46,9 @@ import org.gradle.api.tasks.util.PatternSet;
* Support class for implementations of {@link BootArchive}.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @see BootJar
* @see BootWar
*/
class BootArchiveSupport {
@@ -61,46 +66,71 @@ class BootArchiveSupport {
private final PatternSet requiresUnpack = new PatternSet();
private final Function<FileCopyDetails, ZipCompression> compressionResolver;
private final PatternSet exclusions = new PatternSet();
private final String loaderMainClass;
private final Spec<FileCopyDetails> librarySpec;
private final Function<FileCopyDetails, ZipCompression> compressionResolver;
private LaunchScriptConfiguration launchScript;
private boolean excludeDevtools = true;
BootArchiveSupport(String loaderMainClass, Function<FileCopyDetails, ZipCompression> compressionResolver) {
BootArchiveSupport(String loaderMainClass, Spec<FileCopyDetails> librarySpec,
Function<FileCopyDetails, ZipCompression> compressionResolver) {
this.loaderMainClass = loaderMainClass;
this.librarySpec = librarySpec;
this.compressionResolver = compressionResolver;
this.requiresUnpack.include(Specs.satisfyNone());
configureExclusions();
}
void configureManifest(Jar jar, String mainClassName, String springBootClasses, String springBootLib) {
Attributes attributes = jar.getManifest().getAttributes();
void configureManifest(Manifest manifest, String mainClass, String classes, String lib, String classPathIndex,
String layersIndex) {
Attributes attributes = manifest.getAttributes();
attributes.putIfAbsent("Main-Class", this.loaderMainClass);
attributes.putIfAbsent("Start-Class", mainClassName);
attributes.computeIfAbsent("Spring-Boot-Version", (key) -> determineSpringBootVersion());
attributes.putIfAbsent("Spring-Boot-Classes", springBootClasses);
attributes.putIfAbsent("Spring-Boot-Lib", springBootLib);
attributes.putIfAbsent("Start-Class", mainClass);
attributes.computeIfAbsent("Spring-Boot-Version", (name) -> determineSpringBootVersion());
if (classes != null) {
attributes.putIfAbsent("Spring-Boot-Classes", classes);
}
if (lib != null) {
attributes.putIfAbsent("Spring-Boot-Lib", lib);
}
if (classPathIndex != null) {
attributes.putIfAbsent("Spring-Boot-Classpath-Index", classPathIndex);
}
if (layersIndex != null) {
attributes.putIfAbsent("Spring-Boot-Layers-Index", layersIndex);
}
}
private String determineSpringBootVersion() {
String implementationVersion = getClass().getPackage().getImplementationVersion();
return (implementationVersion != null) ? implementationVersion : "unknown";
String version = getClass().getPackage().getImplementationVersion();
return (version != null) ? version : "unknown";
}
CopyAction createCopyAction(Jar jar) {
CopyAction copyAction = new BootZipCopyAction(jar.getArchiveFile().get().getAsFile(),
jar.isPreserveFileTimestamps(), isUsingDefaultLoader(jar), this.requiresUnpack.getAsSpec(),
this.exclusions.getAsExcludeSpec(), this.launchScript, this.compressionResolver,
jar.getMetadataCharset());
if (!jar.isReproducibleFileOrder()) {
return copyAction;
}
return new ReproducibleOrderingCopyAction(copyAction);
return createCopyAction(jar, null, false);
}
CopyAction createCopyAction(Jar jar, LayerResolver layerResolver, boolean includeLayerTools) {
File output = jar.getArchiveFile().get().getAsFile();
Manifest manifest = jar.getManifest();
boolean preserveFileTimestamps = jar.isPreserveFileTimestamps();
boolean includeDefaultLoader = isUsingDefaultLoader(jar);
Spec<FileTreeElement> requiresUnpack = this.requiresUnpack.getAsSpec();
Spec<FileTreeElement> exclusions = this.exclusions.getAsExcludeSpec();
LaunchScriptConfiguration launchScript = this.launchScript;
Spec<FileCopyDetails> librarySpec = this.librarySpec;
Function<FileCopyDetails, ZipCompression> compressionResolver = this.compressionResolver;
String encoding = jar.getMetadataCharset();
CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, includeDefaultLoader,
includeLayerTools, requiresUnpack, exclusions, launchScript, librarySpec, compressionResolver, encoding,
layerResolver);
return jar.isReproducibleFileOrder() ? new ReproducibleOrderingCopyAction(action) : action;
}
private boolean isUsingDefaultLoader(Jar jar) {
@@ -132,7 +162,19 @@ class BootArchiveSupport {
configureExclusions();
}
boolean isZip(File file) {
void excludeNonZipLibraryFiles(FileCopyDetails details) {
if (this.librarySpec.isSatisfiedBy(details)) {
excludeNonZipFiles(details);
}
}
void excludeNonZipFiles(FileCopyDetails details) {
if (!isZip(details.getFile())) {
details.exclude();
}
}
private boolean isZip(File file) {
try {
try (FileInputStream fileInputStream = new FileInputStream(file)) {
return isZip(fileInputStream);
@@ -160,6 +202,17 @@ class BootArchiveSupport {
this.exclusions.setExcludes(excludes);
}
void moveModuleInfoToRoot(CopySpec spec) {
spec.filesMatching("module-info.class", BootArchiveSupport::moveToRoot);
}
private static void moveToRoot(FileCopyDetails details) {
details.setRelativePath(details.getRelativeSourcePath());
}
/**
* {@link CopyAction} variant that sorts entries to ensure reproducible ordering.
*/
private static final class ReproducibleOrderingCopyAction implements CopyAction {
private final CopyAction delegate;

View File

@@ -16,45 +16,24 @@
package org.springframework.boot.gradle.tasks.bundling;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
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 groovy.lang.Closure;
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.artifacts.Configuration;
import org.gradle.api.file.CopySpec;
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.Nested;
import org.gradle.api.tasks.Optional;
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;
import org.gradle.util.ConfigureUtil;
/**
* A custom {@link Jar} task that produces a Spring Boot executable jar.
@@ -62,82 +41,88 @@ import org.springframework.util.FileCopyUtils;
* @author Andy Wilkinson
* @author Madhura Bhave
* @author Scott Frederick
* @author Phillip Webb
* @since 2.0.0
*/
public class BootJar extends Jar implements BootArchive {
private final BootArchiveSupport support = new BootArchiveSupport("org.springframework.boot.loader.JarLauncher",
this::resolveZipCompression);
private static final String LAUNCHER = "org.springframework.boot.loader.JarLauncher";
private final CopySpec bootInf;
private static final String CLASSES_FOLDER = "BOOT-INF/classes/";
private static final String LIB_FOLDER = "BOOT-INF/lib/";
private static final String LAYERS_INDEX = "BOOT-INF/layers.idx";
private static final String CLASSPATH_INDEX = "BOOT-INF/classpath.idx";
private final BootArchiveSupport support;
private final CopySpec bootInfSpec;
private String mainClassName;
private FileCollection classpath;
private Layers layers;
private LayerConfiguration layerConfiguration;
private static final String BOOT_INF_LAYERS = "BOOT-INF/layers";
private final List<String> dependencies = new ArrayList<>();
private final Map<String, String> coordinatesByFileName = new HashMap<>();
private LayeredSpec layered;
/**
* Creates a new {@code BootJar} task.
*/
public BootJar() {
this.bootInf = getProject().copySpec().into("BOOT-INF");
getMainSpec().with(this.bootInf);
this.bootInf.into("classes", classpathFiles(File::isDirectory));
this.bootInf.into("lib", classpathFiles(File::isFile))
.eachFile((details) -> BootJar.this.dependencies.add(details.getPath()));
this.bootInf.into("",
(spec) -> spec.from((Callable<File>) () -> createClasspathIndex(BootJar.this.dependencies)));
this.bootInf.filesMatching("module-info.class",
(details) -> details.setRelativePath(details.getRelativeSourcePath()));
getRootSpec().eachFile((details) -> {
String pathString = details.getRelativePath().getPathString();
if (pathString.startsWith("BOOT-INF/lib/") && !this.support.isZip(details.getFile())) {
details.exclude();
}
});
this.support = new BootArchiveSupport(LAUNCHER, this::isLibrary, this::resolveZipCompression);
this.bootInfSpec = getProject().copySpec().into("BOOT-INF");
configureBootInfSpec(this.bootInfSpec);
getMainSpec().with(this.bootInfSpec);
}
private Action<CopySpec> classpathFiles(Spec<File> filter) {
return (copySpec) -> copySpec.from((Callable<Iterable<File>>) () -> (this.classpath != null)
? this.classpath.filter(filter) : Collections.emptyList());
private void configureBootInfSpec(CopySpec bootInfSpec) {
bootInfSpec.into("classes", fromCallTo(this::classpathDirectories));
bootInfSpec.into("lib", fromCallTo(this::classpathFiles)).eachFile(this.support::excludeNonZipFiles);
bootInfSpec.filesMatching("module-info.class",
(details) -> details.setRelativePath(details.getRelativeSourcePath()));
}
private Iterable<File> classpathDirectories() {
return classpathEntries(File::isDirectory);
}
private Iterable<File> classpathFiles() {
return classpathEntries(File::isFile);
}
private Iterable<File> classpathEntries(Spec<File> filter) {
return (this.classpath != null) ? this.classpath.filter(filter) : Collections.emptyList();
}
@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");
if (this.layered != null) {
this.support.configureManifest(getManifest(), getMainClassName(), null, null, CLASSPATH_INDEX,
LAYERS_INDEX);
}
else {
this.support.configureManifest(getManifest(), getMainClassName(), CLASSES_FOLDER, LIB_FOLDER,
CLASSPATH_INDEX, null);
}
attributes.putIfAbsent("Spring-Boot-Classpath-Index", "BOOT-INF/classpath.idx");
super.copy();
}
private File createClasspathIndex(List<String> dependencies) {
String content = dependencies.stream().map((name) -> name.substring(name.lastIndexOf('/') + 1))
.collect(Collectors.joining("\n", "", "\n"));
File source = getProject().getResources().getText().fromString(content).asFile();
File indexFile = new File(source.getParentFile(), "classpath.idx");
source.renameTo(indexFile);
return indexFile;
}
@Override
protected CopyAction createCopyAction() {
if (this.layered != null) {
LayerResolver layerResolver = new LayerResolver(getConfigurations(), this.layered, this::isLibrary);
boolean includeLayerTools = this.layered.isIncludeLayerTools();
return this.support.createCopyAction(this, layerResolver, includeLayerTools);
}
return this.support.createCopyAction(this);
}
@Internal
protected Iterable<Configuration> getConfigurations() {
return getProject().getConfigurations();
}
@Override
public String getMainClassName() {
if (this.mainClassName == null) {
@@ -179,110 +164,28 @@ public class BootJar extends Jar implements BootArchive {
action.execute(enableLaunchScriptIfNecessary());
}
@Optional
@Nested
public LayerConfiguration getLayerConfiguration() {
return this.layerConfiguration;
@Optional
public LayeredSpec getLayered() {
return this.layered;
}
/**
* Configures the archive to have layers.
*/
public void layers() {
enableLayers();
public void layered() {
layered(true);
}
public void layers(Action<LayerConfiguration> action) {
action.execute(enableLayers());
public void layered(boolean layered) {
this.layered = layered ? new LayeredSpec() : null;
}
private LayerConfiguration enableLayers() {
if (this.layerConfiguration == null) {
this.layerConfiguration = new LayerConfiguration();
}
return this.layerConfiguration;
public void layered(Closure<?> closure) {
layered(ConfigureUtil.configureUsing(closure));
}
private void applyLayers() {
if (this.layerConfiguration == null) {
return;
}
if (this.layerConfiguration.getLayersOrder() == null || this.layerConfiguration.getLayersOrder().isEmpty()) {
this.layers = Layers.IMPLICIT;
}
else {
List<Layer> customLayers = this.layerConfiguration.getLayersOrder().stream().map(Layer::new)
.collect(Collectors.toList());
this.layers = new CustomLayers(customLayers, this.layerConfiguration.getApplication(),
this.layerConfiguration.getLibraries());
}
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) {
String relativePath = details.getPath().substring("BOOT-INF/".length());
details.setPath(BOOT_INF_LAYERS + "/" + layer + "/" + relativePath);
}
}).setIncludeEmptyDirs(false);
this.bootInf.into("", (spec) -> spec.from(createLayersIndex()));
}
private Layer layerForFileDetails(FileCopyDetails details) {
String path = details.getPath();
if (path.startsWith("BOOT-INF/lib/")) {
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, libraryCoordinates, false));
}
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);
}
}
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.layerConfiguration != null;
public void layered(Action<LayeredSpec> action) {
LayeredSpec layered = new LayeredSpec();
action.execute(layered);
this.layered = layered;
}
@Override
@@ -317,13 +220,6 @@ 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.
@@ -333,7 +229,7 @@ public class BootJar extends Jar implements BootArchive {
@Internal
public CopySpec getBootInf() {
CopySpec child = getProject().copySpec();
this.bootInf.with(child);
this.bootInfSpec.with(child);
return child;
}
@@ -352,30 +248,26 @@ public class BootJar extends Jar implements BootArchive {
}
/**
* Returns the {@link ZipCompression} that should be used when adding the file
* represented by the given {@code details} to the jar.
* <p>
* By default, any file in {@code BOOT-INF/lib/} or
* {@code BOOT-INF/layers/<layer>/lib} is stored and all other files are deflated.
* @param details the details
* Return the {@link ZipCompression} that should be used when adding the file
* represented by the given {@code details} to the jar. By default, any
* {@link #isLibrary(FileCopyDetails) library} is {@link ZipCompression#STORED stored}
* and all other files are {@link ZipCompression#DEFLATED deflated}.
* @param details the file copy details
* @return the compression to use
*/
protected ZipCompression resolveZipCompression(FileCopyDetails details) {
String path = details.getRelativePath().getPathString();
for (String prefix : getLibPathPrefixes()) {
if (path.startsWith(prefix)) {
return ZipCompression.STORED;
}
}
return ZipCompression.DEFLATED;
return isLibrary(details) ? ZipCompression.STORED : ZipCompression.DEFLATED;
}
private Set<String> getLibPathPrefixes() {
if (this.layers == null) {
return Collections.singleton("BOOT-INF/lib/");
}
return StreamSupport.stream(this.layers.spliterator(), false)
.map((layer) -> "BOOT-INF/layers/" + layer + "/lib/").collect(Collectors.toSet());
/**
* Return if the {@link FileCopyDetails} are for a library. By default any file in
* {@code BOOT-INF/lib} is considered to be a library.
* @param details the file copy details
* @return {@code true} if the details are for a library
*/
protected boolean isLibrary(FileCopyDetails details) {
String path = details.getRelativePath().getPathString();
return path.startsWith(LIB_FOLDER);
}
private LaunchScriptConfiguration enableLaunchScriptIfNecessary() {
@@ -387,4 +279,24 @@ public class BootJar extends Jar implements BootArchive {
return launchScript;
}
/**
* Syntactic sugar that makes {@link CopySpec#into} calls a little easier to read.
* @param <T> the result type
* @param callable the callable
* @return an action to add the callable to the spec
*/
private static <T> Action<CopySpec> fromCallTo(Callable<T> callable) {
return (spec) -> spec.from(callTo(callable));
}
/**
* Syntactic sugar that makes {@link CopySpec#from} calls a little easier to read.
* @param <T> the result type
* @param callable the callable
* @return the callable
*/
private static <T> Callable<T> callTo(Callable<T> callable) {
return callable;
}
}

View File

@@ -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,12 +16,12 @@
package org.springframework.boot.gradle.tasks.bundling;
import java.io.File;
import java.util.Collections;
import java.util.concurrent.Callable;
import org.gradle.api.Action;
import org.gradle.api.Project;
import org.gradle.api.file.CopySpec;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileCopyDetails;
import org.gradle.api.file.FileTreeElement;
@@ -35,12 +35,20 @@ import org.gradle.api.tasks.bundling.War;
* A custom {@link War} task that produces a Spring Boot executable war.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.0.0
*/
public class BootWar extends War implements BootArchive {
private final BootArchiveSupport support = new BootArchiveSupport("org.springframework.boot.loader.WarLauncher",
this::resolveZipCompression);
private static final String LAUNCHER = "org.springframework.boot.loader.WarLauncher";
private static final String CLASSES_FOLDER = "WEB-INF/classes/";
private static final String LIB_PROVIDED_FOLDER = "WEB-INF/lib-provided/";
private static final String LIB_FOLDER = "WEB-INF/lib/";
private final BootArchiveSupport support;
private String mainClassName;
@@ -50,23 +58,19 @@ public class BootWar extends War implements BootArchive {
* Creates a new {@code BootWar} task.
*/
public BootWar() {
getWebInf().into("lib-provided",
(copySpec) -> copySpec.from((Callable<Iterable<File>>) () -> (this.providedClasspath != null)
? this.providedClasspath : Collections.emptyList()));
getRootSpec().filesMatching("module-info.class",
(details) -> details.setRelativePath(details.getRelativeSourcePath()));
getRootSpec().eachFile((details) -> {
String pathString = details.getRelativePath().getPathString();
if ((pathString.startsWith("WEB-INF/lib/") || pathString.startsWith("WEB-INF/lib-provided/"))
&& !this.support.isZip(details.getFile())) {
details.exclude();
}
});
this.support = new BootArchiveSupport(LAUNCHER, this::isLibrary, this::resolveZipCompression);
getWebInf().into("lib-provided", fromCallTo(this::getProvidedLibFiles));
this.support.moveModuleInfoToRoot(getRootSpec());
getRootSpec().eachFile(this.support::excludeNonZipLibraryFiles);
}
private Object getProvidedLibFiles() {
return (this.providedClasspath != null) ? this.providedClasspath : Collections.emptyList();
}
@Override
public void copy() {
this.support.configureManifest(this, getMainClassName(), "WEB-INF/classes/", "WEB-INF/lib/");
this.support.configureManifest(getManifest(), getMainClassName(), CLASSES_FOLDER, LIB_FOLDER, null, null);
super.copy();
}
@@ -171,20 +175,26 @@ public class BootWar extends War implements BootArchive {
}
/**
* Returns the {@link ZipCompression} that should be used when adding the file
* represented by the given {@code details} to the jar.
* <p>
* By default, any file in {@code WEB-INF/lib/} or {@code WEB-INF/lib-provided/} is
* stored and all other files are deflated.
* @param details the details
* Return the {@link ZipCompression} that should be used when adding the file
* represented by the given {@code details} to the jar. By default, any
* {@link #isLibrary(FileCopyDetails) library} is {@link ZipCompression#STORED stored}
* and all other files are {@link ZipCompression#DEFLATED deflated}.
* @param details the file copy details
* @return the compression to use
*/
protected ZipCompression resolveZipCompression(FileCopyDetails details) {
String relativePath = details.getRelativePath().getPathString();
if (relativePath.startsWith("WEB-INF/lib/") || relativePath.startsWith("WEB-INF/lib-provided/")) {
return ZipCompression.STORED;
}
return ZipCompression.DEFLATED;
return isLibrary(details) ? ZipCompression.STORED : ZipCompression.DEFLATED;
}
/**
* Return if the {@link FileCopyDetails} are for a library. By default any file in
* {@code WEB-INF/lib} or {@code WEB-INF/lib-provided} is considered to be a library.
* @param details the file copy details
* @return {@code true} if the details are for a library
*/
protected boolean isLibrary(FileCopyDetails details) {
String path = details.getRelativePath().getPathString();
return path.startsWith(LIB_FOLDER) || path.startsWith(LIB_PROVIDED_FOLDER);
}
private LaunchScriptConfiguration enableLaunchScriptIfNecessary() {
@@ -196,4 +206,24 @@ public class BootWar extends War implements BootArchive {
return launchScript;
}
/**
* Syntactic sugar that makes {@link CopySpec#into} calls a little easier to read.
* @param <T> the result type
* @param callable the callable
* @return an action to add the callable to the spec
*/
private static <T> Action<CopySpec> fromCallTo(Callable<T> callable) {
return (spec) -> spec.from(callTo(callable));
}
/**
* Syntactic sugar that makes {@link CopySpec#from} calls a little easier to read.
* @param <T> the result type
* @param callable the callable
* @return the callable
*/
private static <T> Callable<T> callTo(Callable<T> callable) {
return callable;
}
}

View File

@@ -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.
@@ -19,10 +19,15 @@ package org.springframework.boot.gradle.tasks.bundling;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.io.OutputStreamWriter;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.zip.CRC32;
@@ -34,11 +39,16 @@ import org.gradle.api.file.FileCopyDetails;
import org.gradle.api.file.FileTreeElement;
import org.gradle.api.internal.file.copy.CopyAction;
import org.gradle.api.internal.file.copy.CopyActionProcessingStream;
import org.gradle.api.java.archives.Attributes;
import org.gradle.api.java.archives.Manifest;
import org.gradle.api.specs.Spec;
import org.gradle.api.tasks.WorkResult;
import org.gradle.api.tasks.WorkResults;
import org.springframework.boot.loader.tools.DefaultLaunchScript;
import org.springframework.boot.loader.tools.FileUtils;
import org.springframework.boot.loader.tools.JarModeLibrary;
import org.springframework.util.StreamUtils;
/**
* A {@link CopyAction} for creating a Spring Boot zip archive (typically a jar or war).
@@ -49,69 +59,84 @@ import org.springframework.boot.loader.tools.FileUtils;
*/
class BootZipCopyAction implements CopyAction {
static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = new GregorianCalendar(1980, Calendar.FEBRUARY, 1, 0, 0, 0)
.getTimeInMillis();
static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = OffsetDateTime.of(1980, 2, 1, 0, 0, 0, 0, ZoneOffset.UTC)
.toInstant().toEpochMilli();
private final File output;
private final Manifest manifest;
private final boolean preserveFileTimestamps;
private final boolean includeDefaultLoader;
private final boolean includeLayerTools;
private final Spec<FileTreeElement> requiresUnpack;
private final Spec<FileTreeElement> exclusions;
private final LaunchScriptConfiguration launchScript;
private final Spec<FileCopyDetails> librarySpec;
private final Function<FileCopyDetails, ZipCompression> compressionResolver;
private final String encoding;
BootZipCopyAction(File output, boolean preserveFileTimestamps, boolean includeDefaultLoader,
Spec<FileTreeElement> requiresUnpack, Spec<FileTreeElement> exclusions,
LaunchScriptConfiguration launchScript, Function<FileCopyDetails, ZipCompression> compressionResolver,
String encoding) {
private final LayerResolver layerResolver;
BootZipCopyAction(File output, Manifest manifest, boolean preserveFileTimestamps, boolean includeDefaultLoader,
boolean includeLayerTools, Spec<FileTreeElement> requiresUnpack, Spec<FileTreeElement> exclusions,
LaunchScriptConfiguration launchScript, Spec<FileCopyDetails> librarySpec,
Function<FileCopyDetails, ZipCompression> compressionResolver, String encoding,
LayerResolver layerResolver) {
this.output = output;
this.manifest = manifest;
this.preserveFileTimestamps = preserveFileTimestamps;
this.includeDefaultLoader = includeDefaultLoader;
this.includeLayerTools = includeLayerTools;
this.requiresUnpack = requiresUnpack;
this.exclusions = exclusions;
this.launchScript = launchScript;
this.librarySpec = librarySpec;
this.compressionResolver = compressionResolver;
this.encoding = encoding;
this.layerResolver = layerResolver;
}
@Override
public WorkResult execute(CopyActionProcessingStream stream) {
public WorkResult execute(CopyActionProcessingStream copyActions) {
try {
writeArchive(stream);
return () -> true;
writeArchive(copyActions);
return WorkResults.didWork(true);
}
catch (IOException ex) {
throw new GradleException("Failed to create " + this.output, ex);
}
}
private void writeArchive(CopyActionProcessingStream stream) throws IOException {
OutputStream outputStream = new FileOutputStream(this.output);
private void writeArchive(CopyActionProcessingStream copyActions) throws IOException {
OutputStream output = new FileOutputStream(this.output);
try {
writeLaunchScriptIfNecessary(outputStream);
ZipArchiveOutputStream zipOutputStream = new ZipArchiveOutputStream(outputStream);
try {
if (this.encoding != null) {
zipOutputStream.setEncoding(this.encoding);
}
Processor processor = new Processor(zipOutputStream);
stream.process(processor::process);
processor.finish();
}
finally {
closeQuietly(zipOutputStream);
}
writeArchive(copyActions, output);
}
finally {
closeQuietly(outputStream);
closeQuietly(output);
}
}
private void writeArchive(CopyActionProcessingStream copyActions, OutputStream output) throws IOException {
writeLaunchScriptIfNecessary(output);
ZipArchiveOutputStream zipOutput = new ZipArchiveOutputStream(output);
try {
setEncodingIfNecessary(zipOutput);
Processor processor = new Processor(zipOutput);
copyActions.process(processor::process);
processor.finish();
}
finally {
closeQuietly(zipOutput);
}
}
@@ -131,6 +156,12 @@ class BootZipCopyAction implements CopyAction {
}
}
private void setEncodingIfNecessary(ZipArchiveOutputStream zipOutputStream) {
if (this.encoding != null) {
zipOutputStream.setEncoding(this.encoding);
}
}
private void closeQuietly(OutputStream outputStream) {
try {
outputStream.close();
@@ -144,17 +175,20 @@ class BootZipCopyAction implements CopyAction {
*/
private class Processor {
private ZipArchiveOutputStream outputStream;
private ZipArchiveOutputStream out;
private Spec<FileTreeElement> writtenLoaderEntries;
Processor(ZipArchiveOutputStream outputStream) {
this.outputStream = outputStream;
private Set<String> writtenDirectories = new LinkedHashSet<>();
private Set<String> writtenLibraries = new LinkedHashSet<>();
Processor(ZipArchiveOutputStream out) {
this.out = out;
}
void process(FileCopyDetails details) {
if (BootZipCopyAction.this.exclusions.isSatisfiedBy(details)
|| (this.writtenLoaderEntries != null && this.writtenLoaderEntries.isSatisfiedBy(details))) {
if (skipProcessing(details)) {
return;
}
try {
@@ -171,8 +205,90 @@ class BootZipCopyAction implements CopyAction {
}
}
private boolean skipProcessing(FileCopyDetails details) {
return BootZipCopyAction.this.exclusions.isSatisfiedBy(details)
|| (this.writtenLoaderEntries != null && this.writtenLoaderEntries.isSatisfiedBy(details));
}
private void processDirectory(FileCopyDetails details) throws IOException {
String name = getEntryName(details);
long time = getTime(details);
writeParentDirectoriesIfNecessary(name, time);
ZipArchiveEntry entry = new ZipArchiveEntry(name + '/');
entry.setUnixMode(UnixStat.DIR_FLAG | details.getMode());
entry.setTime(time);
this.out.putArchiveEntry(entry);
this.out.closeArchiveEntry();
this.writtenDirectories.add(name);
}
private void processFile(FileCopyDetails details) throws IOException {
String name = getEntryName(details);
long time = getTime(details);
writeParentDirectoriesIfNecessary(name, time);
ZipArchiveEntry entry = new ZipArchiveEntry(name);
entry.setUnixMode(UnixStat.FILE_FLAG | details.getMode());
entry.setTime(time);
ZipCompression compression = BootZipCopyAction.this.compressionResolver.apply(details);
if (compression == ZipCompression.STORED) {
prepareStoredEntry(details, entry);
}
this.out.putArchiveEntry(entry);
details.copyTo(this.out);
this.out.closeArchiveEntry();
if (BootZipCopyAction.this.librarySpec.isSatisfiedBy(details)) {
this.writtenLibraries.add(name.substring(name.lastIndexOf('/') + 1));
}
}
private void writeParentDirectoriesIfNecessary(String name, long time) throws IOException {
String parentDirectory = getParentDirectory(name);
if (parentDirectory != null && this.writtenDirectories.add(parentDirectory)) {
writeParentDirectoriesIfNecessary(parentDirectory, time);
ZipArchiveEntry entry = new ZipArchiveEntry(parentDirectory + '/');
entry.setUnixMode(UnixStat.DIR_FLAG);
entry.setTime(time);
this.out.putArchiveEntry(entry);
this.out.closeArchiveEntry();
}
}
private String getParentDirectory(String name) {
int lastSlash = name.lastIndexOf('/');
if (lastSlash == -1) {
return null;
}
return name.substring(0, lastSlash);
}
private String getEntryName(FileCopyDetails details) {
if (BootZipCopyAction.this.layerResolver == null) {
return details.getRelativePath().getPathString();
}
return BootZipCopyAction.this.layerResolver.getPath(details);
}
private void prepareStoredEntry(FileCopyDetails details, ZipArchiveEntry archiveEntry) throws IOException {
archiveEntry.setMethod(java.util.zip.ZipEntry.STORED);
archiveEntry.setSize(details.getSize());
archiveEntry.setCompressedSize(details.getSize());
archiveEntry.setCrc(getCrc(details));
if (BootZipCopyAction.this.requiresUnpack.isSatisfiedBy(details)) {
archiveEntry.setComment("UNPACK:" + FileUtils.sha1Hash(details.getFile()));
}
}
private long getCrc(FileCopyDetails details) {
Crc32OutputStream crcStream = new Crc32OutputStream();
details.copyTo(crcStream);
return crcStream.getCrc();
}
void finish() throws IOException {
writeLoaderEntriesIfNecessary(null);
writeJarToolsIfNecessary();
writeLayersIndexIfNecessary();
writeClassPathIndexIfNecessary();
}
private void writeLoaderEntriesIfNecessary(FileCopyDetails details) throws IOException {
@@ -183,9 +299,9 @@ class BootZipCopyAction implements CopyAction {
// Don't write loader entries until after META-INF folder (see gh-16698)
return;
}
LoaderZipEntries loaderEntries = new LoaderZipEntries(
LoaderZipEntries entries = new LoaderZipEntries(
BootZipCopyAction.this.preserveFileTimestamps ? null : CONSTANT_TIME_FOR_ZIP_ENTRIES);
this.writtenLoaderEntries = loaderEntries.writeTo(this.outputStream);
this.writtenLoaderEntries = entries.writeTo(this.out);
}
private boolean isInMetaInf(FileCopyDetails details) {
@@ -196,40 +312,47 @@ class BootZipCopyAction implements CopyAction {
return segments.length > 0 && "META-INF".equals(segments[0]);
}
private void processDirectory(FileCopyDetails details) throws IOException {
ZipArchiveEntry archiveEntry = new ZipArchiveEntry(details.getRelativePath().getPathString() + '/');
archiveEntry.setUnixMode(UnixStat.DIR_FLAG | details.getMode());
archiveEntry.setTime(getTime(details));
this.outputStream.putArchiveEntry(archiveEntry);
this.outputStream.closeArchiveEntry();
private void writeJarToolsIfNecessary() throws IOException {
if (BootZipCopyAction.this.layerResolver == null || !BootZipCopyAction.this.includeLayerTools) {
return;
}
writeJarModeLibrary(JarModeLibrary.LAYER_TOOLS);
}
private void processFile(FileCopyDetails details) throws IOException {
String relativePath = details.getRelativePath().getPathString();
ZipArchiveEntry archiveEntry = new ZipArchiveEntry(relativePath);
archiveEntry.setUnixMode(UnixStat.FILE_FLAG | details.getMode());
archiveEntry.setTime(getTime(details));
ZipCompression compression = BootZipCopyAction.this.compressionResolver.apply(details);
if (compression == ZipCompression.STORED) {
prepareStoredEntry(details, archiveEntry);
}
this.outputStream.putArchiveEntry(archiveEntry);
details.copyTo(this.outputStream);
this.outputStream.closeArchiveEntry();
private void writeJarModeLibrary(JarModeLibrary jarModeLibrary) throws IOException {
String name = BootZipCopyAction.this.layerResolver.getPath(jarModeLibrary);
writeFile(name, ZipEntryWriter.fromInputStream(jarModeLibrary.openStream()));
}
private void prepareStoredEntry(FileCopyDetails details, ZipArchiveEntry archiveEntry) throws IOException {
archiveEntry.setMethod(java.util.zip.ZipEntry.STORED);
archiveEntry.setSize(details.getSize());
archiveEntry.setCompressedSize(details.getSize());
Crc32OutputStream crcStream = new Crc32OutputStream();
details.copyTo(crcStream);
archiveEntry.setCrc(crcStream.getCrc());
if (BootZipCopyAction.this.requiresUnpack.isSatisfiedBy(details)) {
archiveEntry.setComment("UNPACK:" + FileUtils.sha1Hash(details.getFile()));
private void writeLayersIndexIfNecessary() throws IOException {
Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes();
String layersIndex = (String) manifestAttributes.get("Spring-Boot-Layers-Index");
if (layersIndex != null && BootZipCopyAction.this.layerResolver != null) {
writeFile(layersIndex, ZipEntryWriter.fromLines(BootZipCopyAction.this.encoding,
BootZipCopyAction.this.layerResolver.getLayerNames()));
}
}
private void writeClassPathIndexIfNecessary() throws IOException {
Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes();
String classPathIndex = (String) manifestAttributes.get("Spring-Boot-Classpath-Index");
if (classPathIndex != null) {
writeFile(classPathIndex,
ZipEntryWriter.fromLines(BootZipCopyAction.this.encoding, this.writtenLibraries));
}
}
private void writeFile(String name, ZipEntryWriter entryWriter) throws IOException {
writeParentDirectoriesIfNecessary(name, CONSTANT_TIME_FOR_ZIP_ENTRIES);
ZipArchiveEntry entry = new ZipArchiveEntry(name);
entry.setUnixMode(UnixStat.FILE_FLAG);
entry.setTime(CONSTANT_TIME_FOR_ZIP_ENTRIES);
this.out.putArchiveEntry(entry);
entryWriter.writeTo(entry, this.out);
this.out.closeArchiveEntry();
}
private long getTime(FileCopyDetails details) {
return BootZipCopyAction.this.preserveFileTimestamps ? details.getLastModified()
: CONSTANT_TIME_FOR_ZIP_ENTRIES;
@@ -237,6 +360,52 @@ class BootZipCopyAction implements CopyAction {
}
/**
* Callback used to write a zip entry data.
*/
@FunctionalInterface
private interface ZipEntryWriter {
/**
* Write the entry data.
* @param entry the entry being written
* @param out the output stream used to write the data
* @throws IOException on IO error
*/
void writeTo(ZipArchiveEntry entry, ZipArchiveOutputStream out) throws IOException;
/**
* Create a new {@link ZipEntryWriter} that will copy content from the given
* {@link InputStream}.
* @param in the source input stream
* @return a new {@link ZipEntryWriter} instance
*/
static ZipEntryWriter fromInputStream(InputStream in) {
return (entry, out) -> {
StreamUtils.copy(in, out);
in.close();
};
}
/**
* Create a new {@link ZipEntryWriter} that will copy content from the given
* lines.
* @param encoding the required character encoding
* @param lines the lines to write
* @return a new {@link ZipEntryWriter} instance
*/
static ZipEntryWriter fromLines(String encoding, Collection<String> lines) {
return (entry, out) -> {
OutputStreamWriter writer = new OutputStreamWriter(out, encoding);
for (String line : lines) {
writer.append(line + "\n");
}
writer.flush();
};
}
}
/**
* An {@code OutputStream} that provides a CRC-32 of the data that is written to it.
*/

View File

@@ -1,237 +0,0 @@
/*
* 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 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.application.FilteredResourceStrategy;
import org.springframework.boot.loader.tools.layer.application.LocationFilter;
import org.springframework.boot.loader.tools.layer.application.ResourceFilter;
import org.springframework.boot.loader.tools.layer.application.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> layersOrder = 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
*/
@Input
public boolean isIncludeLayerTools() {
return this.includeLayerTools;
}
public void setIncludeLayerTools(boolean includeLayerTools) {
this.includeLayerTools = includeLayerTools;
}
@Input
public List<String> getLayersOrder() {
return this.layersOrder;
}
public void layersOrder(String... layers) {
this.layersOrder = Arrays.asList(layers);
}
public void layersOrder(List<String> layers) {
this.layersOrder = layers;
}
@Input
public List<ResourceStrategy> getApplication() {
return this.resourceStrategies;
}
public void application(ResourceStrategy... resourceStrategies) {
assertLayersOrderConfigured();
this.resourceStrategies = Arrays.asList(resourceStrategies);
}
public void application(Action<LayerConfiguration> config) {
assertLayersOrderConfigured();
this.strategySpec = StrategySpec.forResources();
config.execute(this);
}
@Input
public List<LibraryStrategy> getLibraries() {
return this.libraryStrategies;
}
public void libraries(LibraryStrategy... strategies) {
assertLayersOrderConfigured();
this.libraryStrategies = Arrays.asList(strategies);
}
public void libraries(Action<LayerConfiguration> configure) {
assertLayersOrderConfigured();
this.strategySpec = StrategySpec.forLibraries();
configure.execute(this);
}
private void assertLayersOrderConfigured() {
Assert.state(!this.layersOrder.isEmpty(), "'layersOrder' must be configured before filters can be applied.");
}
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 application");
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 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);
}
private enum TYPE {
LIBRARIES, RESOURCES;
}
}
}

View File

@@ -0,0 +1,197 @@
/*
* 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 java.io.File;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.gradle.api.artifacts.ArtifactCollection;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.component.ComponentIdentifier;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
import org.gradle.api.file.FileCopyDetails;
import org.gradle.api.specs.Spec;
import org.springframework.boot.loader.tools.JarModeLibrary;
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;
/**
* Resolver backed by a {@link LayeredSpec} that provides the destination {@link Layer}
* for each copied {@link FileCopyDetails}.
*
* @author Madhura Bhave
* @author Scott Frederick
* @author Phillip Webb
* @see BootZipCopyAction
*/
class LayerResolver {
private static final String BOOT_INF_FOLDER = "BOOT-INF/";
private final ResolvedDependencies resolvedDependencies;
private final LayeredSpec layeredConfiguration;
private final Spec<FileCopyDetails> librarySpec;
LayerResolver(Iterable<Configuration> configurations, LayeredSpec layeredConfiguration,
Spec<FileCopyDetails> librarySpec) {
this.resolvedDependencies = new ResolvedDependencies(configurations);
this.layeredConfiguration = layeredConfiguration;
this.librarySpec = librarySpec;
}
String getPath(JarModeLibrary jarModeLibrary) {
Layers layers = this.layeredConfiguration.asLayers();
Layer layer = layers.getLayer(jarModeLibrary);
if (layer != null) {
return BOOT_INF_FOLDER + "layers/" + layer + "/lib/" + jarModeLibrary.getName();
}
return BOOT_INF_FOLDER + "lib/" + jarModeLibrary.getName();
}
String getPath(FileCopyDetails details) {
String path = details.getRelativePath().getPathString();
Layer layer = getLayer(details);
if (layer == null || !path.startsWith(BOOT_INF_FOLDER)) {
return path;
}
path = path.substring(BOOT_INF_FOLDER.length());
return BOOT_INF_FOLDER + "layers/" + layer + "/" + path;
}
Layer getLayer(FileCopyDetails details) {
Layers layers = this.layeredConfiguration.asLayers();
try {
if (this.librarySpec.isSatisfiedBy(details)) {
return layers.getLayer(asLibrary(details));
}
return layers.getLayer(details.getSourcePath());
}
catch (UnsupportedOperationException ex) {
return null;
}
}
List<String> getLayerNames() {
return this.layeredConfiguration.asLayers().stream().map(Layer::toString).collect(Collectors.toList());
}
private Library asLibrary(FileCopyDetails details) {
File file = details.getFile();
LibraryCoordinates coordinates = this.resolvedDependencies.find(file);
return new Library(null, file, null, coordinates, false);
}
/**
* Tracks and provides details of resolved dependencies in the project so we can find
* {@link LibraryCoordinates}.
*/
private static class ResolvedDependencies {
private final Map<Configuration, ResolvedConfigurationDependencies> configurationDependencies = new LinkedHashMap<>();
ResolvedDependencies(Iterable<Configuration> configurations) {
configurations.forEach(this::processConfiguration);
}
private void processConfiguration(Configuration configuration) {
if (configuration.isCanBeResolved()) {
this.configurationDependencies.put(configuration,
new ResolvedConfigurationDependencies(configuration.getIncoming().getArtifacts()));
}
}
LibraryCoordinates find(File file) {
for (ResolvedConfigurationDependencies dependencies : this.configurationDependencies.values()) {
LibraryCoordinates coordinates = dependencies.find(file);
if (coordinates != null) {
return coordinates;
}
}
return null;
}
}
/**
* Stores details of resolved configuration dependencies.
*/
private static class ResolvedConfigurationDependencies {
private final Map<File, LibraryCoordinates> artifactCoordinates = new LinkedHashMap<>();
ResolvedConfigurationDependencies(ArtifactCollection resolvedDependencies) {
if (resolvedDependencies != null) {
for (ResolvedArtifactResult resolvedArtifact : resolvedDependencies.getArtifacts()) {
ComponentIdentifier identifier = resolvedArtifact.getId().getComponentIdentifier();
if (identifier instanceof ModuleComponentIdentifier) {
this.artifactCoordinates.put(resolvedArtifact.getFile(),
new ModuleComponentIdentifierLibraryCoordinates(
(ModuleComponentIdentifier) identifier));
}
}
}
}
LibraryCoordinates find(File file) {
return this.artifactCoordinates.get(file);
}
}
/**
* Adapts a {@link ModuleComponentIdentifier} to {@link LibraryCoordinates}.
*/
private static class ModuleComponentIdentifierLibraryCoordinates implements LibraryCoordinates {
private final ModuleComponentIdentifier identifier;
ModuleComponentIdentifierLibraryCoordinates(ModuleComponentIdentifier identifier) {
this.identifier = identifier;
}
@Override
public String getGroupId() {
return this.identifier.getGroup();
}
@Override
public String getArtifactId() {
return this.identifier.getModule();
}
@Override
public String getVersion() {
return this.identifier.getVersion();
}
@Override
public String toString() {
return this.identifier.toString();
}
}
}

View File

@@ -0,0 +1,228 @@
/*
* 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 java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import groovy.lang.Closure;
import org.gradle.api.Action;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Optional;
import org.gradle.util.ConfigureUtil;
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.layer.ApplicationContentFilter;
import org.springframework.boot.loader.tools.layer.ContentFilter;
import org.springframework.boot.loader.tools.layer.ContentSelector;
import org.springframework.boot.loader.tools.layer.CustomLayers;
import org.springframework.boot.loader.tools.layer.IncludeExcludeContentSelector;
import org.springframework.boot.loader.tools.layer.LibraryContentFilter;
import org.springframework.util.Assert;
/**
* Encapsulates the configuration for a layered jar.
*
* @author Madhura Bhave
* @author Scott Frederick
* @author Phillip Webb
* @since 2.3.0
*/
public class LayeredSpec {
private boolean includeLayerTools = true;
private ApplicationSpec application = new ApplicationSpec();
private DependenciesSpec dependencies = new DependenciesSpec();
@Optional
private List<String> layerOrder;
private Layers layers;
@Input
public boolean isIncludeLayerTools() {
return this.includeLayerTools;
}
public void setIncludeLayerTools(boolean includeLayerTools) {
this.includeLayerTools = includeLayerTools;
}
@Input
public ApplicationSpec getApplication() {
return this.application;
}
public void application(ApplicationSpec spec) {
this.application = spec;
}
public void application(Closure<?> closure) {
application(ConfigureUtil.configureUsing(closure));
}
public void application(Action<ApplicationSpec> action) {
action.execute(this.application);
}
@Input
public DependenciesSpec getDependencies() {
return this.dependencies;
}
public void dependencies(DependenciesSpec spec) {
this.dependencies = spec;
}
public void dependencies(Closure<?> closure) {
dependencies(ConfigureUtil.configureUsing(closure));
}
public void dependencies(Action<DependenciesSpec> action) {
action.execute(this.dependencies);
}
@Input
public List<String> getLayerOrder() {
return this.layerOrder;
}
public void layerOrder(String... layerOrder) {
this.layerOrder = Arrays.asList(layerOrder);
}
public void layerOrder(List<String> layerOrder) {
this.layerOrder = layerOrder;
}
/**
* Return this configuration as a {@link Layers} instance. This method should only be
* called when the configuration is complete and will no longer be changed.
* @return the layers
*/
Layers asLayers() {
Layers layers = this.layers;
if (layers == null) {
layers = createLayers();
this.layers = layers;
}
return layers;
}
private Layers createLayers() {
if (this.layerOrder == null || this.layerOrder.isEmpty()) {
Assert.state(this.application.isEmpty() && this.dependencies.isEmpty(),
"The 'layerOrder' must be defined when using custom layering");
return Layers.IMPLICIT;
}
List<Layer> layers = this.layerOrder.stream().map(Layer::new).collect(Collectors.toList());
return new CustomLayers(layers, this.application.asSelectors(), this.dependencies.asSelectors());
}
public abstract static class IntoLayersSpec implements Serializable {
private final List<IntoLayerSpec> intoLayers;
boolean isEmpty() {
return this.intoLayers.isEmpty();
}
IntoLayersSpec(IntoLayerSpec... spec) {
this.intoLayers = new ArrayList<>(Arrays.asList(spec));
}
public void intoLayer(String layer) {
this.intoLayers.add(new IntoLayerSpec(layer));
}
public void intoLayer(String layer, Closure<?> closure) {
intoLayer(layer, ConfigureUtil.configureUsing(closure));
}
public void intoLayer(String layer, Action<IntoLayerSpec> action) {
IntoLayerSpec spec = new IntoLayerSpec(layer);
action.execute(spec);
this.intoLayers.add(spec);
}
<T> List<ContentSelector<T>> asSelectors(Function<String, ContentFilter<T>> filterFactory) {
return this.intoLayers.stream().map((content) -> content.asSelector(filterFactory))
.collect(Collectors.toList());
}
}
public static class IntoLayerSpec implements Serializable {
private final String intoLayer;
private final List<String> includes = new ArrayList<>();
private final List<String> excludes = new ArrayList<>();
public IntoLayerSpec(String intoLayer) {
this.intoLayer = intoLayer;
}
public void include(String... patterns) {
this.includes.addAll(Arrays.asList(patterns));
}
public void exclude(String... patterns) {
this.includes.addAll(Arrays.asList(patterns));
}
<T> ContentSelector<T> asSelector(Function<String, ContentFilter<T>> filterFactory) {
Layer layer = new Layer(this.intoLayer);
return new IncludeExcludeContentSelector<>(layer, this.includes, this.excludes, filterFactory);
}
}
public static class ApplicationSpec extends IntoLayersSpec {
public ApplicationSpec(IntoLayerSpec... contents) {
super(contents);
}
List<ContentSelector<String>> asSelectors() {
return asSelectors(ApplicationContentFilter::new);
}
}
public static class DependenciesSpec extends IntoLayersSpec {
public DependenciesSpec(IntoLayerSpec... contents) {
super(contents);
}
List<ContentSelector<Library>> asSelectors() {
return asSelectors(LibraryContentFilter::new);
}
}
}

View File

@@ -44,18 +44,18 @@ class LoaderZipEntries {
this.entryTime = entryTime;
}
Spec<FileTreeElement> writeTo(ZipArchiveOutputStream zipOutputStream) throws IOException {
Spec<FileTreeElement> writeTo(ZipArchiveOutputStream out) throws IOException {
WrittenDirectoriesSpec writtenDirectoriesSpec = new WrittenDirectoriesSpec();
try (ZipInputStream loaderJar = new ZipInputStream(
getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) {
java.util.zip.ZipEntry entry = loaderJar.getNextEntry();
while (entry != null) {
if (entry.isDirectory() && !entry.getName().equals("META-INF/")) {
writeDirectory(new ZipArchiveEntry(entry), zipOutputStream);
writeDirectory(new ZipArchiveEntry(entry), out);
writtenDirectoriesSpec.add(entry);
}
else if (entry.getName().endsWith(".class")) {
writeClass(new ZipArchiveEntry(entry), loaderJar, zipOutputStream);
writeClass(new ZipArchiveEntry(entry), loaderJar, out);
}
entry = loaderJar.getNextEntry();
}

View File

@@ -25,11 +25,14 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.jar.JarFile;
import org.gradle.testkit.runner.BuildResult;
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 org.springframework.boot.loader.tools.JarModeLibrary;
import static org.assertj.core.api.Assertions.assertThat;
/**
@@ -75,8 +78,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
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(jarModeLayerTools())).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();
@@ -89,10 +91,11 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
void customLayers() throws IOException {
writeMainClass();
writeResource();
assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
BuildResult build = this.gradleBuild.build("bootJar");
System.out.println(build.getOutput());
assertThat(build.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(jarModeLayerTools())).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();
@@ -101,6 +104,13 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
}
}
private String jarModeLayerTools() {
JarModeLibrary library = JarModeLibrary.LAYER_TOOLS;
String version = library.getCoordinates().getVersion();
String layer = (version == null || !version.contains("SNAPSHOT")) ? "dependencies" : "snapshot-dependencies";
return "BOOT-INF/layers/" + layer + "/lib/" + library.getName();
}
private void writeMainClass() {
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example");
examplePackage.mkdirs();

View File

@@ -20,9 +20,8 @@ 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.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarFile;
@@ -31,16 +30,15 @@ import java.util.zip.ZipEntry;
import org.gradle.api.Action;
import org.gradle.api.artifacts.ArtifactCollection;
import org.gradle.api.artifacts.Configuration;
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.component.ModuleComponentIdentifier;
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
import org.junit.jupiter.api.Test;
import org.springframework.boot.loader.tools.layer.application.FilteredResourceStrategy;
import org.springframework.boot.loader.tools.layer.application.LocationFilter;
import org.springframework.boot.loader.tools.layer.library.CoordinateFilter;
import org.springframework.boot.loader.tools.layer.library.FilteredLibraryStrategy;
import org.springframework.boot.gradle.tasks.bundling.BootJarTests.TestBootJar;
import org.springframework.boot.loader.tools.JarModeLibrary;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
@@ -53,10 +51,10 @@ import static org.mockito.Mockito.mock;
* @author Madhura Bhave
* @author Scott Frederick
*/
class BootJarTests extends AbstractBootArchiveTests<BootJar> {
class BootJarTests extends AbstractBootArchiveTests<TestBootJar> {
BootJarTests() {
super(BootJar.class, "org.springframework.boot.loader.JarLauncher", "BOOT-INF/lib/", "BOOT-INF/classes/");
super(TestBootJar.class, "org.springframework.boot.loader.JarLauncher", "BOOT-INF/lib/", "BOOT-INF/classes/");
}
@Test
@@ -121,13 +119,17 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
@Test
void whenJarIsLayeredWithCustomStrategiesThenContentsAreMovedToLayerDirectories() throws IOException {
File jar = createLayeredJar((configuration) -> {
configuration.layersOrder("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.application(createResourceStrategy("resources", "static/**"),
createResourceStrategy("application", "**"));
File jar = createLayeredJar((layered) -> {
layered.application((application) -> {
application.intoLayer("resources", (spec) -> spec.include("static/**"));
application.intoLayer("application");
});
layered.dependencies((dependencies) -> {
dependencies.intoLayer("my-snapshot-deps", (spec) -> spec.include("com.example:*:*.SNAPSHOT"));
dependencies.intoLayer("my-internal-deps", (spec) -> spec.include("com.example:*:*"));
dependencies.intoLayer("my-deps");
});
layered.layerOrder("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application");
});
List<String> entryNames = getEntryNames(jar);
assertThat(entryNames)
@@ -139,16 +141,6 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
.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())) {
@@ -172,7 +164,14 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
@Test
void whenJarIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException {
List<String> entryNames = getEntryNames(createLayeredJar());
assertThat(entryNames).contains("BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar");
assertThat(entryNames).contains(jarModeLayerTools());
}
private String jarModeLayerTools() {
JarModeLibrary library = JarModeLibrary.LAYER_TOOLS;
String version = library.getCoordinates().getVersion();
String layer = (version == null || !version.contains("SNAPSHOT")) ? "dependencies" : "snapshot-dependencies";
return "BOOT-INF/layers/" + layer + "/lib/" + library.getName();
}
@Test
@@ -198,24 +197,24 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
return getTask().getArchiveFile().get().getAsFile();
}
private File createLayeredJar(Action<LayerConfiguration> action) throws IOException {
private File createLayeredJar() throws IOException {
return createLayeredJar(null);
}
private File createLayeredJar(Action<LayeredSpec> action) throws IOException {
if (action != null) {
getTask().layers(action);
getTask().layered(action);
}
else {
getTask().layers();
getTask().layered();
}
addContent();
executeTask();
return getTask().getArchiveFile().get().getAsFile();
}
private File createLayeredJar() throws IOException {
return createLayeredJar(null);
}
private void addContent() throws IOException {
BootJar bootJar = getTask();
TestBootJar bootJar = getTask();
bootJar.setMainClassName("com.example.Main");
File classesJavaMain = new File(this.temp, "classes/java/main");
File applicationClass = new File(classesJavaMain, "com/example/Application.class");
@@ -231,32 +230,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);
Set<ResolvedArtifactResult> artifacts = new LinkedHashSet<>();
artifacts.add(mockLibraryArtifact("first-library.jar", "com.example", "first-library", "1.0.0"));
artifacts.add(mockLibraryArtifact("second-library.jar", "com.example", "second-library", "1.0.0"));
artifacts.add(
mockLibraryArtifact("third-library-SNAPSHOT.jar", "com.example", "third-library", "1.0.0.SNAPSHOT"));
ArtifactCollection resolvedDependencies = mock(ArtifactCollection.class);
given(resolvedDependencies.getArtifacts()).willReturn(artifacts);
ResolvableDependencies resolvableDependencies = mock(ResolvableDependencies.class);
given(resolvableDependencies.getArtifacts()).willReturn(resolvedDependencies);
Configuration configuration = mock(Configuration.class);
given(configuration.isCanBeResolved()).willReturn(true);
given(configuration.getIncoming()).willReturn(resolvableDependencies);
bootJar.setConfiguration(Collections.singleton(configuration));
}
private ResolvedArtifactResult mockLibraryArtifact(String fileName, String coordinates) {
ComponentIdentifier libraryId = mock(ComponentIdentifier.class);
given(libraryId.getDisplayName()).willReturn(coordinates);
private ResolvedArtifactResult mockLibraryArtifact(String fileName, String group, String module, String version) {
ModuleComponentIdentifier identifier = mock(ModuleComponentIdentifier.class);
given(identifier.getGroup()).willReturn(group);
given(identifier.getModule()).willReturn(module);
given(identifier.getVersion()).willReturn(version);
ComponentArtifactIdentifier libraryArtifactId = mock(ComponentArtifactIdentifier.class);
given(libraryArtifactId.getComponentIdentifier()).willReturn(libraryId);
given(libraryArtifactId.getComponentIdentifier()).willReturn(identifier);
ResolvedArtifactResult libraryArtifact = mock(ResolvedArtifactResult.class);
given(libraryArtifact.getFile()).willReturn(new File(fileName));
File file = new File(this.temp, fileName).getAbsoluteFile();
System.out.println(file);
given(libraryArtifact.getFile()).willReturn(file);
given(libraryArtifact.getId()).willReturn(libraryArtifactId);
return libraryArtifact;
}
@@ -272,4 +272,19 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
getTask().copy();
}
public static class TestBootJar extends BootJar {
private Iterable<Configuration> configurations = Collections.emptySet();
@Override
protected Iterable<Configuration> getConfigurations() {
return this.configurations;
}
void setConfiguration(Iterable<Configuration> configurations) {
this.configurations = configurations;
}
}
}

View File

@@ -5,26 +5,23 @@ plugins {
bootJar {
mainClassName = 'com.example.Application'
layers {
layersOrder "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 "*:*" } }
}
layered {
application {
layerContent("static") {
locations {
include "META-INF/resources/**", "resources/**"
include "static/**", "public/**"
}
}
layerContent("app") {
locations {
include "**"
}
intoLayer("static") {
include "META-INF/resources/**", "resources/**", "static/**", "public/**"
}
intoLayer("app")
}
dependencies {
intoLayer("snapshot-dependencies") {
include "*:*:*SNAPSHOT"
}
intoLayer("commons-dependencies") {
include "org.apache.commons:*"
}
intoLayer("dependencies")
}
layerOrder "dependencies", "commons-dependencies", "snapshot-dependencies", "static", "app"
}
}

View File

@@ -5,7 +5,7 @@ plugins {
bootJar {
mainClassName = 'com.example.Application'
layers()
layered()
}
repositories {

View File

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