Skip to content

Commit e9d61ba

Browse files
mbhavephilwebb
andcommitted
Support generation and loading of layered jars
Support an alternative fat jar format that is more amenable to Docker image layers. The new format arranges files in the following structure: BOOT-INF/ layers/ <layer-name #1> /classes /lib <layer-name #2> /classes /lib The `BOOT-INF/layers.idx` file provides the names of the layers and the order in which they should be added (starting with the least changed). The `JarLauncher` class can load layered jars in both fat and exploded forms. Closes gh-19767 Co-authored-by: Phillip Webb <pwebb@pivotal.io>
1 parent 45b1ab4 commit e9d61ba

File tree

11 files changed

+614
-7
lines changed

11 files changed

+614
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.loader.tools;
18+
19+
/**
20+
* Implementation of {@link Layers} that uses implicit rules.
21+
*
22+
* @author Madhura Bhave
23+
* @author Phillip Webb
24+
*/
25+
class ImplicitLayerResolver extends StandardLayers {
26+
27+
private static final String[] RESOURCE_LOCATIONS = { "META-INF/resources/", "resources/", "static/", "public/" };
28+
29+
@Override
30+
public Layer getLayer(String name) {
31+
if (!isClassFile(name) && isInResourceLocation(name)) {
32+
return RESOURCES;
33+
}
34+
return APPLICATION;
35+
}
36+
37+
@Override
38+
public Layer getLayer(Library library) {
39+
if (library.getName().contains("SNAPSHOT.")) {
40+
return SNAPSHOT_DEPENDENCIES;
41+
}
42+
return DEPENDENCIES;
43+
}
44+
45+
private boolean isClassFile(String name) {
46+
return name.endsWith(".class");
47+
}
48+
49+
private boolean isInResourceLocation(String name) {
50+
for (String resourceLocation : RESOURCE_LOCATIONS) {
51+
if (name.startsWith(resourceLocation)) {
52+
return true;
53+
}
54+
}
55+
return false;
56+
}
57+
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.loader.tools;
18+
19+
import java.util.regex.Pattern;
20+
21+
import org.springframework.util.Assert;
22+
23+
/**
24+
* A named layer used to separate the jar when creating a Docker image.
25+
*
26+
* @author Madhura Bhave
27+
* @author Phillip Webb
28+
* @since 2.3.0
29+
* @see Layers
30+
*/
31+
public class Layer {
32+
33+
private static final Pattern PATTERN = Pattern.compile("^[a-zA-Z0-9-]+$");
34+
35+
private final String name;
36+
37+
/**
38+
* Create a new {@link Layer} instance with the specified name.
39+
* @param name the name of the layer.
40+
*/
41+
public Layer(String name) {
42+
Assert.hasText(name, "Name must not be empty");
43+
Assert.isTrue(PATTERN.matcher(name).matches(), "Malformed layer name '" + name + "'");
44+
this.name = name;
45+
}
46+
47+
@Override
48+
public boolean equals(Object obj) {
49+
if (this == obj) {
50+
return true;
51+
}
52+
if (obj == null || getClass() != obj.getClass()) {
53+
return false;
54+
}
55+
return this.name.equals(((Layer) obj).name);
56+
}
57+
58+
@Override
59+
public int hashCode() {
60+
return this.name.hashCode();
61+
}
62+
63+
@Override
64+
public String toString() {
65+
return this.name;
66+
}
67+
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.loader.tools;
18+
19+
/**
20+
* A specialization of {@link RepackagingLayout} that supports layers in the repackaged
21+
* archive.
22+
*
23+
* @author Madhura Bhave
24+
* @author Phillip Webb
25+
* @since 2.3.0
26+
*/
27+
public interface LayeredLayout extends RepackagingLayout {
28+
29+
/**
30+
* Returns the location of the layers index file that should be written or
31+
* {@code null} if not index is required. The result should include the filename and
32+
* is relative to the root of the jar.
33+
* @return the layers index file location
34+
*/
35+
String getLayersIndexFileLocation();
36+
37+
/**
38+
* Returns the location to which classes should be moved within the context of a
39+
* layer.
40+
* @param layer the destination layer for the content
41+
* @return the repackaged classes location
42+
*/
43+
String getRepackagedClassesLocation(Layer layer);
44+
45+
/**
46+
* Returns the destination path for a given library within the context of a layer.
47+
* @param libraryName the name of the library (excluding any path)
48+
* @param scope the scope of the library
49+
* @param layer the destination layer for the content
50+
* @return the location of the library relative to the root of the archive (should end
51+
* with '/') or {@code null} if the library should not be included.
52+
*/
53+
String getLibraryLocation(String libraryName, LibraryScope scope, Layer layer);
54+
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.loader.tools;
18+
19+
import java.util.Iterator;
20+
21+
/**
22+
* Interface to provide information about layers to the {@link Repackager}.
23+
*
24+
* @author Madhura Bhave
25+
* @author Phillip Webb
26+
* @since 2.3.0
27+
* @see Layer
28+
*/
29+
public interface Layers extends Iterable<Layer> {
30+
31+
/**
32+
* The default layer resolver.
33+
*/
34+
Layers IMPLICIT = new ImplicitLayerResolver();
35+
36+
/**
37+
* Return the jar layers in the order that they should be added (starting with the
38+
* least frequently changed layer).
39+
*/
40+
@Override
41+
Iterator<Layer> iterator();
42+
43+
/**
44+
* Return the layer that contains the given resource name.
45+
* @param resourceName the name of the resource (for example a {@code .class} file).
46+
* @return the layer that contains the resource (must never be {@code null})
47+
*/
48+
Layer getLayer(String resourceName);
49+
50+
/**
51+
* Return the layer that contains the given library.
52+
* @param library the library to consider
53+
* @return the layer that contains the resource (must never be {@code null})
54+
*/
55+
Layer getLayer(Library library);
56+
57+
}

Diff for: spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java

+22
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,28 @@ public boolean isExecutable() {
101101

102102
}
103103

104+
/**
105+
* Executable JAR layout with support for layers.
106+
*/
107+
public static class LayeredJar extends Jar implements LayeredLayout {
108+
109+
@Override
110+
public String getLayersIndexFileLocation() {
111+
return "BOOT-INF/layers.idx";
112+
}
113+
114+
@Override
115+
public String getRepackagedClassesLocation(Layer layer) {
116+
return "BOOT-INF/layers/" + layer + "/classes/";
117+
}
118+
119+
@Override
120+
public String getLibraryLocation(String libraryName, LibraryScope scope, Layer layer) {
121+
return "BOOT-INF/layers/" + layer + "/lib/";
122+
}
123+
124+
}
125+
104126
/**
105127
* Executable expanded archive layout.
106128
*/

Diff for: spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java

+47-5
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public class Repackager {
6262

6363
private static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";
6464

65+
private static final String BOOT_LAYERS_INDEX_ATTRIBUTE = "Spring-Boot-Layers-Index";
66+
6567
private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 };
6668

6769
private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
@@ -80,6 +82,8 @@ public class Repackager {
8082

8183
private LayoutFactory layoutFactory;
8284

85+
private Layers layers = Layers.IMPLICIT;
86+
8387
public Repackager(File source) {
8488
this(source, null);
8589
}
@@ -128,6 +132,16 @@ public void setLayout(Layout layout) {
128132
this.layout = layout;
129133
}
130134

135+
/**
136+
* Sets the layers that should be used in the jar.
137+
* @param layers the jar layers
138+
* @see LayeredLayout
139+
*/
140+
public void setLayers(Layers layers) {
141+
Assert.notNull(layers, "Layers must not be null");
142+
this.layers = layers;
143+
}
144+
131145
/**
132146
* Sets the layout factory for the jar. The factory can be used when no specific
133147
* layout is specified.
@@ -244,7 +258,7 @@ else if (this.layout.isExecutable()) {
244258

245259
private EntryTransformer getEntityTransformer() {
246260
if (this.layout instanceof RepackagingLayout) {
247-
return new RepackagingEntryTransformer((RepackagingLayout) this.layout);
261+
return new RepackagingEntryTransformer((RepackagingLayout) this.layout, this.layers);
248262
}
249263
return EntryTransformer.NONE;
250264
}
@@ -328,14 +342,23 @@ protected String findMainMethod(JarFile source) throws IOException {
328342

329343
private void addBootAttributes(Attributes attributes) {
330344
attributes.putValue(BOOT_VERSION_ATTRIBUTE, getClass().getPackage().getImplementationVersion());
331-
if (this.layout instanceof RepackagingLayout) {
345+
if (this.layout instanceof LayeredLayout) {
346+
addBootBootAttributesForLayeredLayout(attributes, (LayeredLayout) this.layout);
347+
}
348+
else if (this.layout instanceof RepackagingLayout) {
332349
addBootBootAttributesForRepackagingLayout(attributes, (RepackagingLayout) this.layout);
333350
}
334351
else {
335352
addBootBootAttributesForPlainLayout(attributes, this.layout);
336353
}
337354
}
338355

356+
private void addBootBootAttributesForLayeredLayout(Attributes attributes, LayeredLayout layout) {
357+
String layersIndexFileLocation = layout.getLayersIndexFileLocation();
358+
putIfHasLength(attributes, BOOT_LAYERS_INDEX_ATTRIBUTE, layersIndexFileLocation);
359+
putIfHasLength(attributes, BOOT_CLASSPATH_INDEX_ATTRIBUTE, layout.getClasspathIndexFileLocation());
360+
}
361+
339362
private void addBootBootAttributesForRepackagingLayout(Attributes attributes, RepackagingLayout layout) {
340363
attributes.putValue(BOOT_CLASSES_ATTRIBUTE, layout.getRepackagedClassesLocation());
341364
putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, this.layout.getLibraryLocation("", LibraryScope.COMPILE));
@@ -388,8 +411,11 @@ private static final class RepackagingEntryTransformer implements EntryTransform
388411

389412
private final RepackagingLayout layout;
390413

391-
private RepackagingEntryTransformer(RepackagingLayout layout) {
414+
private final Layers layers;
415+
416+
private RepackagingEntryTransformer(RepackagingLayout layout, Layers layers) {
392417
this.layout = layout;
418+
this.layers = layers;
393419
}
394420

395421
@Override
@@ -400,7 +426,7 @@ public JarArchiveEntry transform(JarArchiveEntry entry) {
400426
if (!isTransformable(entry)) {
401427
return entry;
402428
}
403-
String transformedName = this.layout.getRepackagedClassesLocation() + entry.getName();
429+
String transformedName = transformName(entry.getName());
404430
JarArchiveEntry transformedEntry = new JarArchiveEntry(transformedName);
405431
transformedEntry.setTime(entry.getTime());
406432
transformedEntry.setSize(entry.getSize());
@@ -425,6 +451,15 @@ public JarArchiveEntry transform(JarArchiveEntry entry) {
425451
return transformedEntry;
426452
}
427453

454+
private String transformName(String name) {
455+
if (this.layout instanceof LayeredLayout) {
456+
Layer layer = this.layers.getLayer(name);
457+
Assert.state(layer != null, "Invalid 'null' layer from " + this.layers.getClass().getName());
458+
return ((LayeredLayout) this.layout).getRepackagedClassesLocation(layer) + name;
459+
}
460+
return this.layout.getRepackagedClassesLocation() + name;
461+
}
462+
428463
private boolean isTransformable(JarArchiveEntry entry) {
429464
String name = entry.getName();
430465
if (name.startsWith("META-INF/")) {
@@ -456,7 +491,14 @@ private WritableLibraries(Libraries libraries) throws IOException {
456491
}
457492

458493
private String getLocation(Library library) {
459-
return Repackager.this.layout.getLibraryLocation(library.getName(), library.getScope());
494+
Layout layout = Repackager.this.layout;
495+
if (layout instanceof LayeredLayout) {
496+
Layers layers = Repackager.this.layers;
497+
Layer layer = layers.getLayer(library);
498+
Assert.state(layer != null, "Invalid 'null' library layer from " + layers.getClass().getName());
499+
return ((LayeredLayout) layout).getLibraryLocation(library.getName(), library.getScope(), layer);
500+
}
501+
return layout.getLibraryLocation(library.getName(), library.getScope());
460502
}
461503

462504
@Override

0 commit comments

Comments
 (0)