Add support for customizing layers in Maven

This commit adds an additional 'layers/configuration' property that can
be used to refer to a separate layers configuration file. This separate
file defines:

* The layers and their order of precedence,
* How libraries are handled using filters that match against the
coordinates of each library, and
* How classes are handled using filters that match against the location
of the entry

An XSD to validate the XML configuration file is available.

Closes gh-20295

Co-authored-by: Stephane Nicoll <snicoll@pivotal.io>
This commit is contained in:
Madhura Bhave
2020-03-11 17:02:48 -07:00
committed by Stephane Nicoll
parent 896d2c8579
commit e49e62df5c
38 changed files with 2024 additions and 37 deletions

View File

@@ -71,6 +71,129 @@ The `layout` property defaults to a guess based on the archive type (`jar` or `w
* `ZIP` (alias to `DIR`): similar to the `JAR` layout using `PropertiesLauncher`.
* `NONE`: Bundle all dependencies and project resources. Does not bundle a bootstrap loader.
[[repackage-layers]]
=== Layered jar
By default, a repackaged jar contains the application's classes and dependencies in `BOOT-INF/classes` and `BOOT-INF/lib` respectively.
For cases where a docker image needs to be built from the contents of the jar, the jar format can be enhanced to support layer folders.
To use this feature, the layering feature must be enabled:
[source,xml,indent=0,subs="verbatim,attributes"]
----
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>{gradle-project-version}</version>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
</plugins>
</build>
</project>
----
By default, the following layers are created:
* `application` for any other classes and resources.
* `resources` for static resources at the default locations, i.e. `META-INF/resources/`, `resources/`, `static/`, `public/`.
* `snapshot-dependencies` for any dependency whose version contains `SNAPSHOT`.
* `dependencies` for any other dependency.
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 `application`, `resources`, `snapshot-dependencies` and `dependencies`.
Content that is likely to change should be added first, followed by layers that are less likely to change.
[[repackage-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 a separate configuration file that should be registered as shown in the following example:
[source,xml,indent=0,subs="verbatim,attributes"]
----
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>{gradle-project-version}</version>
<configuration>
<layers>
<enabled>true</enabled>
<configuration>${project.basedir}/src/layers.xml</configuration>
</layers>
</configuration>
</plugin>
</plugins>
</build>
</project>
----
The configuration file 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,xml,indent=0,subs="verbatim,attributes"]
----
<layers-configuration xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-configuration.xsd">
<layers>
<layer>application</layer>
<layer>resources</layer>
<layer>snapshots</layer>
<layer>dependencies</layer>
</layers>
<libraries>
<layer-content layer="snapshot-dependencies">
<coordinates>
<include>*:*:*SNAPSHOT</include>
</coordinates>
</layer-content>
<layer-content layer="dependencies">
<coordinates>
<include>*:*</include>
</coordinates>
</layer-content>
</libraries>
<classes>
<layer-content layer="resources">
<locations>
<include>META-INF/resources/**</include>
<include>resources/**</include>
<include>static/**</include>
<include>public/**</include>
</locations>
</layer-content>
<layer-content layer="application">
<locations>
<include>**</include>
</locations>
</layer-content>
</classes>
</layers-configuration>
----
Each `layer-content` element defines a strategy to include 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 on the 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 on location of the entry using Ant-style pattern matching.
include::goals/repackage.adoc[leveloffset=+1]
@@ -400,31 +523,8 @@ This example excludes any artifact belonging to the `com.foo` group:
[[repackage-layered-jars]]
==== Packaging layered jars
By default, the repackaged jar contains the application's classes and dependencies in `BOOT-INF/classes` and `BOOT-INF/lib` respectively.
For cases where a docker image needs to be built from the contents of the jar, the jar format can be enhanced to support layer folders.
To use this feature, the layering feature must be enabled:
[source,xml,indent=0,subs="verbatim,attributes"]
----
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>{gradle-project-version}</version>
<configuration>
<layered>
<enabled>true</enabled>
</layered>
</configuration>
</plugin>
</plugins>
</build>
</project>
----
[[repackage-layered-jars-tools]]
==== Layered jar tools
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.
@@ -440,13 +540,59 @@ If you wish to exclude this dependency, you can do so in the following manner:
<artifactId>spring-boot-maven-plugin</artifactId>
<version>{gradle-project-version}</version>
<configuration>
<layered>
<layers>
<enabled>true</enabled>
<includeLayerTools>false</enabled>
</layered>
</layers>
</configuration>
</plugin>
</plugins>
</build>
</project>
----
----
[[repackage-layered-jars-additional-layers]]
==== Custom layers configuration
While the default setup creates two layers for libraries, you may want to isolate the dependencies of your project in a dedicated layer.
This allows to reuse the cache for external dependencies when an internal dependency has changed, as shown by the following example:
[source,xml,indent=0,subs="verbatim,attributes"]
----
<layers-configuration xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-configuration.xsd">
<layers>
<layer>application</layer>
<layer>resources</layer>
<layer>snapshots</layer>
<layer>company-dependencies</layer>
<layer>dependencies</layer>
</layers>
<libraries>
<layer-content layer="snapshot-dependencies">
<coordinates>
<include>*:*:*SNAPSHOT</include>
</coordinates>
</layer-content>
<layer-content layer="company-dependencies">
<coordinates>
<include>com.acme:*</include>
</coordinates>
</layer-content>
<layer-content layer="dependencies">
<coordinates>
<include>*:*</include>
</coordinates>
</layer-content>
</libraries>
<classes>
...
</classes>
</layers-configuration>
----
The configuration above creates an additional `company-dependencies` layer with all libraries with the `com.acme` groupId.

View File

@@ -305,4 +305,15 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
});
}
@TestTemplate
void whenJarIsRepackagedWithTheCustomLayeredLayout(MavenBuild mavenBuild) {
mavenBuild.project("jar-layered-custom").execute((project) -> {
File repackaged = new File(project, "jar/target/jar-layered-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/layers/application/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/layers/my-dependencies-name/lib/jar-release")
.hasEntryWithNameStartingWith("BOOT-INF/layers/snapshot-dependencies/lib/jar-snapshot")
.hasEntryWithNameStartingWith("BOOT-INF/layers/configuration/classes/application.yml");
});
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-release</artifactId>
<version>0.0.1.RELEASE</version>
<packaging>jar</packaging>
<name>jar</name>
<description>Release Jar dependency</description>
</project>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-snapshot</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<packaging>jar</packaging>
<name>jar</name>
<description>Snapshot Jar dependency</description>
</project>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-layered</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<layers>
<enabled>true</enabled>
<configuration>${project.basedir}/src/layers.xml</configuration>
</layers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-snapshot</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-release</artifactId>
<version>0.0.1.RELEASE</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,35 @@
<layers-configuration xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/layers/layers-configuration.xsd">
<layers>
<layer>configuration</layer>
<layer>application</layer>
<layer>snapshot-dependencies</layer>
<layer>my-dependencies-name</layer>
</layers>
<libraries>
<layer-content layer="snapshot-dependencies">
<coordinates>
<include>*:*:*-SNAPSHOT</include>
</coordinates>
</layer-content>
<layer-content layer="my-dependencies-name">
<coordinates>
<include>*:*:*</include>
</coordinates>
</layer-content>
</libraries>
<classes>
<layer-content layer="configuration">
<locations>
<include>**/application*.*</include>
</locations>
</layer-content>
<layer-content layer="application">
<locations>
<include>**</include>
</locations>
</layer-content>
</classes>
</layers-configuration>

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.test;
public class SampleApplication {
public static void main(String[] args) {
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>aggregator</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<modules>
<module>jar-snapshot</module>
<module>jar-release</module>
<module>jar</module>
</modules>
</project>

View File

@@ -22,10 +22,10 @@
<goal>repackage</goal>
</goals>
<configuration>
<layered>
<layers>
<enabled>true</enabled>
<includeLayerTools>false</includeLayerTools>
</layered>
</layers>
</configuration>
</execution>
</executions>

View File

@@ -22,9 +22,9 @@
<goal>repackage</goal>
</goals>
<configuration>
<layered>
<layers>
<enabled>true</enabled>
</layered>
</layers>
</configuration>
</execution>
</executions>

View File

@@ -16,12 +16,17 @@
package org.springframework.boot.maven;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.MojoExecutionException;
@@ -31,6 +36,8 @@ import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter;
import org.apache.maven.shared.artifact.filter.collection.ScopeFilter;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.springframework.boot.loader.tools.Layout;
import org.springframework.boot.loader.tools.LayoutFactory;
@@ -109,7 +116,7 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
* @since 2.3.0
*/
@Parameter
private Layered layered;
private Layers layers;
/**
* Return a {@link Packager} configured for this MOJO.
@@ -126,13 +133,31 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
getLog().info("Layout: " + this.layout);
packager.setLayout(this.layout.layout());
}
if (this.layered != null && this.layered.isEnabled()) {
if (this.layers != null && this.layers.isEnabled()) {
if (this.layers.getConfiguration() != null) {
try {
Document document = getDocumentIfAvailable(this.layers.getConfiguration());
CustomLayersProvider customLayersProvider = new CustomLayersProvider();
packager.setLayers(customLayersProvider.getLayers(document));
}
catch (Exception ex) {
throw new IllegalStateException("Failed to process custom layers configuration "
+ this.layers.getConfiguration().getAbsolutePath(), ex);
}
}
packager.setLayout(new LayeredJar());
packager.setIncludeRelevantJarModeJars(this.layered.isIncludeLayerTools());
packager.setIncludeRelevantJarModeJars(this.layers.isIncludeLayerTools());
}
return packager;
}
private Document getDocumentIfAvailable(File configurationFile) throws Exception {
InputSource inputSource = new InputSource(new FileInputStream(configurationFile));
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(inputSource);
}
/**
* Return {@link Libraries} that the packager can use.
* @param unpacks any libraries that require unpack

View File

@@ -31,6 +31,7 @@ import org.apache.maven.plugin.logging.Log;
import org.springframework.boot.loader.tools.Libraries;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryCallback;
import org.springframework.boot.loader.tools.LibraryCoordinates;
import org.springframework.boot.loader.tools.LibraryScope;
/**
@@ -39,6 +40,7 @@ import org.springframework.boot.loader.tools.LibraryScope;
* @author Phillip Webb
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Scott Frederick
* @since 1.0.0
*/
public class ArtifactsLibraries implements Libraries {
@@ -78,7 +80,9 @@ public class ArtifactsLibraries implements Libraries {
name = artifact.getGroupId() + "-" + name;
this.log.debug("Renamed to: " + name);
}
callback.library(new Library(name, artifact.getFile(), scope, isUnpackRequired(artifact)));
LibraryCoordinates coordinates = new LibraryCoordinates(artifact.getGroupId(), artifact.getArtifactId(),
artifact.getVersion());
callback.library(new Library(name, artifact.getFile(), scope, isUnpackRequired(artifact), coordinates));
}
}
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.maven;
import java.util.ArrayList;
import java.util.List;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.springframework.boot.loader.tools.Layer;
import org.springframework.boot.loader.tools.layer.CustomLayers;
import org.springframework.boot.loader.tools.layer.classes.FilteredResourceStrategy;
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;
/**
* Produces a {@link CustomLayers} based on the given {@link Document}.
*
* @author Madhura Bhave
* @since 2.3.0
*/
public class CustomLayersProvider {
public CustomLayers getLayers(Document document) {
Element root = document.getDocumentElement();
NodeList nl = root.getChildNodes();
List<Layer> layers = new ArrayList<>();
List<LibraryStrategy> libraryStrategies = new ArrayList<>();
List<ResourceStrategy> resourceStrategies = new ArrayList<>();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
String nodeName = ele.getNodeName();
if ("layers".equals(nodeName)) {
layers.addAll(getLayers(ele));
}
if ("libraries".equals(nodeName)) {
libraryStrategies.addAll(getLibraryStrategies(ele.getChildNodes()));
}
if ("classes".equals(nodeName)) {
resourceStrategies.addAll(getResourceStrategies(ele.getChildNodes()));
}
}
}
return new CustomLayers(layers, resourceStrategies, libraryStrategies);
}
private List<LibraryStrategy> getLibraryStrategies(NodeList nodes) {
List<LibraryStrategy> strategy = new ArrayList<>();
for (int i = 0; i < nodes.getLength(); i++) {
Node item = nodes.item(i);
if (item instanceof Element) {
Element element = (Element) item;
String layer = element.getAttribute("layer");
if ("layer-content".equals(element.getTagName())) {
List<LibraryFilter> filters = new ArrayList<>();
NodeList filterList = item.getChildNodes();
if (filterList.getLength() == 0) {
throw new IllegalArgumentException("Filters for layer-content must not be empty.");
}
for (int k = 0; k < filterList.getLength(); k++) {
Node filter = filterList.item(k);
if (filter instanceof Element) {
List<String> includeList = getPatterns((Element) filter, "include");
List<String> excludeList = getPatterns((Element) filter, "exclude");
addLibraryFilter(filters, filter, includeList, excludeList);
}
}
strategy.add(new FilteredLibraryStrategy(layer, filters));
}
}
}
return strategy;
}
private void addLibraryFilter(List<LibraryFilter> filters, Node filter, List<String> includeList,
List<String> excludeList) {
if ("coordinates".equals(filter.getNodeName())) {
filters.add(new CoordinateFilter(includeList, excludeList));
}
}
private List<ResourceStrategy> getResourceStrategies(NodeList strategies) {
List<ResourceStrategy> strategy = new ArrayList<>();
for (int i = 0; i < strategies.getLength(); i++) {
Node item = strategies.item(i);
List<ResourceFilter> filters = new ArrayList<>();
if (item instanceof Element) {
Element element = (Element) item;
String layer = element.getAttribute("layer");
if ("layer-content".equals(element.getTagName())) {
NodeList filterList = item.getChildNodes();
if (filterList.getLength() == 0) {
throw new IllegalArgumentException("Filters for layer-content must not be empty.");
}
for (int k = 0; k < filterList.getLength(); k++) {
Node filter = filterList.item(k);
if (filter instanceof Element) {
List<String> includeList = getPatterns((Element) filter, "include");
List<String> excludeList = getPatterns((Element) filter, "exclude");
addFilter(filters, filter, includeList, excludeList);
}
}
strategy.add(new FilteredResourceStrategy(layer, filters));
}
}
}
return strategy;
}
private void addFilter(List<ResourceFilter> filters, Node filter, List<String> includeList,
List<String> excludeList) {
if ("locations".equals(filter.getNodeName())) {
filters.add(
new org.springframework.boot.loader.tools.layer.classes.LocationFilter(includeList, excludeList));
}
}
private List<String> getPatterns(Element element, String key) {
NodeList patterns = element.getElementsByTagName(key);
List<String> values = new ArrayList<>();
for (int j = 0; j < patterns.getLength(); j++) {
Node item = patterns.item(j);
if (item instanceof Element) {
values.add(item.getTextContent());
}
}
return values;
}
private List<Layer> getLayers(Element element) {
List<Layer> layers = new ArrayList<>();
NodeList nl = element.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
String nodeName = ele.getNodeName();
if ("layer".equals(nodeName)) {
layers.add(new Layer(ele.getTextContent()));
}
}
}
return layers;
}
}

View File

@@ -16,18 +16,22 @@
package org.springframework.boot.maven;
import java.io.File;
/**
* Layer configuration options.
*
* @author Madhura Bhave
* @since 2.3.0
*/
public class Layered {
public class Layers {
private boolean enabled;
private boolean includeLayerTools = true;
private File configuration;
/**
* Whether layered jar layout is enabled.
* @return true if the layered layout is enabled.
@@ -44,4 +48,18 @@ public class Layered {
return this.includeLayerTools;
}
/**
* The location of the layers configuration file. If no file is provided, a default
* configuration is used with four layers: {@code application}, {@code resources},
* {@code snapshot-dependencies} and {@code dependencies}.
* @return the layers configuration file
*/
public File getConfiguration() {
return this.configuration;
}
public void setConfiguration(File configuration) {
this.configuration = configuration;
}
}

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema elementFormDefault="qualified"
xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.springframework.org/schema/boot/layers">
<xsd:element name="layers-configuration" type="layersConfigurationType"/>
<xsd:complexType name="layersConfigurationType">
<xsd:sequence>
<xsd:element type="layersType" name="layers"/>
<xsd:element type="librariesType" name="libraries" minOccurs="0"/>
<xsd:element type="classesType" name="classes" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="layersType">
<xsd:sequence>
<xsd:element type="xsd:string" name="layer" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
The name of a layer. Each layer in the configuration must be referenced once and the
order matches how the content is likely to change. Put layers that are frequently
updated first, layers that are more stable (such as non-snapshot dependencies) last.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="librariesType">
<xsd:annotation>
<xsd:documentation><![CDATA[
Strategies that should be applied to libraries. If no strategies are defined, two
layers are created out-of-the-box. A "snapshot-dependencies" layer with SNAPSHOT
libraries and a "dependencies" layer with all the other libraries.
]]></xsd:documentation>
</xsd:annotation>
<xsd:choice maxOccurs="unbounded">
<xsd:element type="librariesLayerContentType" name="layer-content">
<xsd:annotation>
<xsd:documentation><![CDATA[
Strategy to apply on libraries.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="librariesLayerContentType" mixed="true">
<xsd:sequence>
<xsd:element type="filterType" name="coordinates" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute type="xsd:string" name="layer" use="required"/>
</xsd:complexType>
<xsd:complexType name="classesType">
<xsd:annotation>
<xsd:documentation><![CDATA[
Strategies that should be applied to classes. If no strategies are defined, a single
"application" layer is created out-of-the-box.
]]></xsd:documentation>
</xsd:annotation>
<xsd:choice maxOccurs="unbounded">
<xsd:element type="classesLayerContentType" name="layer-content" maxOccurs="unbounded"
minOccurs="0">
<xsd:annotation>
<xsd:documentation><![CDATA[
Strategy to apply on classes.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="classesLayerContentType" mixed="true">
<xsd:sequence>
<xsd:element type="filterType" name="locations" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute type="xsd:string" name="layer" use="required"/>
</xsd:complexType>
<xsd:complexType name="filterType">
<xsd:sequence>
<xsd:element type="xsd:string" name="exclude" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
Pattern of the elements to exclude. An exclude pattern takes precedence over an
include pattern and must be declared first.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element type="xsd:string" name="include" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
Pattern of the elements to include.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.maven;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
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.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link CustomLayersProvider}.
*
* @author Madhura Bhave
* @author Scott Frederick
*/
public class CustomLayersProviderTests {
private CustomLayersProvider customLayersProvider;
@BeforeEach
void setup() {
this.customLayersProvider = new CustomLayersProvider();
}
@Test
void getLayerResolverWhenDocumentValid() throws Exception {
CustomLayers layers = this.customLayersProvider.getLayers(getDocument("layers.xml"));
assertThat(layers).extracting("name").containsExactly("configuration", "application", "my-resources",
"snapshot-dependencies", "my-deps", "my-dependencies-name");
Library snapshot = mockLibrary("test-SNAPSHOT.jar", "org.foo", "1.0.0-SNAPSHOT");
Library groupId = mockLibrary("my-library", "com.acme", null);
Library otherDependency = mockLibrary("other-library", "org.foo", null);
assertThat(layers.getLayer(snapshot).toString()).isEqualTo("snapshot-dependencies");
assertThat(layers.getLayer(groupId).toString()).isEqualTo("my-deps");
assertThat(layers.getLayer(otherDependency).toString()).isEqualTo("my-dependencies-name");
assertThat(layers.getLayer("META-INF/resources/test.css").toString()).isEqualTo("my-resources");
assertThat(layers.getLayer("application.yml").toString()).isEqualTo("configuration");
assertThat(layers.getLayer("test").toString()).isEqualTo("application");
}
private Library mockLibrary(String name, String groupId, String version) {
Library library = mock(Library.class);
given(library.getName()).willReturn(name);
given(library.getCoordinates()).willReturn(new LibraryCoordinates(groupId, null, version));
return library;
}
@Test
void getLayerResolverWhenDocumentContainsLibraryLayerWithNoFilters() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.customLayersProvider.getLayers(getDocument("library-layer-no-filter.xml")))
.withMessage("Filters for layer-content must not be empty.");
}
@Test
void getLayerResolverWhenDocumentContainsResourceLayerWithNoFilters() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.customLayersProvider.getLayers(getDocument("resource-layer-no-filter.xml")))
.withMessage("Filters for layer-content must not be empty.");
}
private Document getDocument(String resourceName) throws Exception {
ClassPathResource resource = new ClassPathResource(resourceName);
InputSource inputSource = new InputSource(resource.getInputStream());
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = factory.newDocumentBuilder();
return documentBuilder.parse(inputSource);
}
}

View File

@@ -0,0 +1,47 @@
<layers-configuration xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-configuration.xsd">
<layers>
<layer>configuration</layer>
<layer>application</layer>
<layer>my-resources</layer>
<layer>snapshot-dependencies</layer>
<layer>my-deps</layer>
<layer>my-dependencies-name</layer>
</layers>
<libraries>
<layer-content layer="snapshot-dependencies">
<coordinates>
<include>*:*:*-SNAPSHOT</include>
</coordinates>
</layer-content>
<layer-content layer="my-deps">
<coordinates>
<include>com.acme:*</include>
</coordinates>
</layer-content>
<layer-content layer="my-dependencies-name">
<coordinates>
<include>*:*:*</include>
</coordinates>
</layer-content>
</libraries>
<classes>
<layer-content layer="my-resources">
<locations>
<include>META-INF/resources/**</include>
</locations>
</layer-content>
<layer-content layer="configuration">
<locations>
<include>**/application*.*</include>
</locations>
</layer-content>
<layer-content layer="application">
<locations>
<include>**</include>
</locations>
</layer-content>
</classes>
</layers-configuration>

View File

@@ -0,0 +1,11 @@
<layers-configuration xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-configuration.xsd">
<layers>
<layer>my-deps</layer>
</layers>
<libraries>
<layer-content layer="my-deps"/>
</libraries>
</layers-configuration>

View File

@@ -0,0 +1,11 @@
<layers-configuration xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-configuration.xsd">
<layers>
<layer>my-layer</layer>
</layers>
<classes>
<layer-content layer="my-layer"/>
</classes>
</layers-configuration>