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:
committed by
Stephane Nicoll
parent
896d2c8579
commit
e49e62df5c
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -22,10 +22,10 @@
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<layered>
|
||||
<layers>
|
||||
<enabled>true</enabled>
|
||||
<includeLayerTools>false</includeLayerTools>
|
||||
</layered>
|
||||
</layers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<layered>
|
||||
<layers>
|
||||
<enabled>true</enabled>
|
||||
</layered>
|
||||
</layers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user