Skip to content

Commit b2dad7f

Browse files
committed
Rework entry ordering of repackaged archives
Previously, the Repackager would write entries in the following order: - Libraries that require unpacking - Existing entries - Application classes - WEB-INF/lib jars in a war - Libraries that do not require unpacking - Loader classes Libraries that require unpacking were written before existing entries so that, when repackaging a war, an entry in WEB-INF/lib would not get in first and prevent a library with same location from being unpacked. However, this had the unwanted side-effect of changing the classpath order when an entry requires unpacking. This commit reworks the handling of existing entries and libraries that require unpacking so that existing entries can be written first while also marking any that match a library that requires unpacking as requiring unpacking. Additionally, loader classes are now written first. They are the first classes in the jar that will be used so it seems to make sense for them to appear first. This aligns Maven-based repackaging with the Gradle plugin's behaviour and with the structure documented in the reference documentation's "The Executable Jar Format" appendix. The net result of the changes described above is that entries are now written in the following order: - Loader classes - Existing entries - Application classes - WEB-INF/lib jars in a war marked for unpacking if needed - Libraries Closes spring-projectsgh-11695 Closes spring-projectsgh-11696
1 parent 9542f51 commit b2dad7f

File tree

3 files changed

+214
-61
lines changed

3 files changed

+214
-61
lines changed

spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java

+82-12
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.io.BufferedInputStream;
2020
import java.io.ByteArrayInputStream;
21+
import java.io.ByteArrayOutputStream;
2122
import java.io.File;
2223
import java.io.FileInputStream;
2324
import java.io.FileNotFoundException;
@@ -54,6 +55,8 @@
5455
*/
5556
public class JarWriter implements LoaderClassesWriter, AutoCloseable {
5657

58+
private static final UnpackHandler NEVER_UNPACK = new NeverUnpackHandler();
59+
5760
private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar";
5861

5962
private static final int BUFFER_SIZE = 32 * 1024;
@@ -119,11 +122,15 @@ public void writeManifest(Manifest manifest) throws IOException {
119122
* @throws IOException if the entries cannot be written
120123
*/
121124
public void writeEntries(JarFile jarFile) throws IOException {
122-
this.writeEntries(jarFile, new IdentityEntryTransformer());
125+
this.writeEntries(jarFile, new IdentityEntryTransformer(), NEVER_UNPACK);
123126
}
124127

125-
void writeEntries(JarFile jarFile, EntryTransformer entryTransformer)
126-
throws IOException {
128+
void writeEntries(JarFile jarFile, UnpackHandler unpackHandler) throws IOException {
129+
this.writeEntries(jarFile, new IdentityEntryTransformer(), unpackHandler);
130+
}
131+
132+
void writeEntries(JarFile jarFile, EntryTransformer entryTransformer,
133+
UnpackHandler unpackHandler) throws IOException {
127134
Enumeration<JarEntry> entries = jarFile.entries();
128135
while (entries.hasMoreElements()) {
129136
JarArchiveEntry entry = new JarArchiveEntry(entries.nextElement());
@@ -133,7 +140,7 @@ void writeEntries(JarFile jarFile, EntryTransformer entryTransformer)
133140
EntryWriter entryWriter = new InputStreamEntryWriter(inputStream, true);
134141
JarArchiveEntry transformedEntry = entryTransformer.transform(entry);
135142
if (transformedEntry != null) {
136-
writeEntry(transformedEntry, entryWriter);
143+
writeEntry(transformedEntry, entryWriter, unpackHandler);
137144
}
138145
}
139146
}
@@ -172,11 +179,9 @@ public void writeNestedLibrary(String destination, Library library)
172179
File file = library.getFile();
173180
JarArchiveEntry entry = new JarArchiveEntry(destination + library.getName());
174181
entry.setTime(getNestedLibraryTime(file));
175-
if (library.isUnpackRequired()) {
176-
entry.setComment("UNPACK:" + FileUtils.sha1Hash(file));
177-
}
178182
new CrcAndSize(file).setupStoredEntry(entry);
179-
writeEntry(entry, new InputStreamEntryWriter(new FileInputStream(file), true));
183+
writeEntry(entry, new InputStreamEntryWriter(new FileInputStream(file), true),
184+
new LibraryUnpackHandler(library));
180185
}
181186

182187
private long getNestedLibraryTime(File file) {
@@ -236,15 +241,21 @@ public void close() throws IOException {
236241
this.jarOutput.close();
237242
}
238243

244+
private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter)
245+
throws IOException {
246+
writeEntry(entry, entryWriter, NEVER_UNPACK);
247+
}
248+
239249
/**
240-
* Perform the actual write of a {@link JarEntry}. All other {@code write} method
250+
* Perform the actual write of a {@link JarEntry}. All other {@code write} methods
241251
* delegate to this one.
242252
* @param entry the entry to write
243253
* @param entryWriter the entry writer or {@code null} if there is no content
254+
* @param unpackHandler handles possible unpacking for the entry
244255
* @throws IOException in case of I/O errors
245256
*/
246-
private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter)
247-
throws IOException {
257+
private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter,
258+
UnpackHandler unpackHandler) throws IOException {
248259
String parent = entry.getName();
249260
if (parent.endsWith("/")) {
250261
parent = parent.substring(0, parent.length() - 1);
@@ -256,11 +267,12 @@ private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter)
256267
if (parent.lastIndexOf('/') != -1) {
257268
parent = parent.substring(0, parent.lastIndexOf('/') + 1);
258269
if (!parent.isEmpty()) {
259-
writeEntry(new JarArchiveEntry(parent), null);
270+
writeEntry(new JarArchiveEntry(parent), null, unpackHandler);
260271
}
261272
}
262273

263274
if (this.writtenEntries.add(entry.getName())) {
275+
entryWriter = addUnpackCommentIfNecessary(entry, entryWriter, unpackHandler);
264276
this.jarOutput.putArchiveEntry(entry);
265277
if (entryWriter != null) {
266278
entryWriter.write(this.jarOutput);
@@ -269,6 +281,18 @@ private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter)
269281
}
270282
}
271283

284+
private EntryWriter addUnpackCommentIfNecessary(JarArchiveEntry entry,
285+
EntryWriter entryWriter, UnpackHandler unpackHandler) throws IOException {
286+
if (entryWriter == null || !unpackHandler.requiresUnpack(entry.getName())) {
287+
return entryWriter;
288+
}
289+
ByteArrayOutputStream output = new ByteArrayOutputStream();
290+
entryWriter.write(output);
291+
entry.setComment("UNPACK:" + unpackHandler.sha1Hash(entry.getName()));
292+
return new InputStreamEntryWriter(new ByteArrayInputStream(output.toByteArray()),
293+
true);
294+
}
295+
272296
/**
273297
* Interface used to write jar entry date.
274298
*/
@@ -421,4 +445,50 @@ public JarArchiveEntry transform(JarArchiveEntry jarEntry) {
421445

422446
}
423447

448+
/**
449+
* An {@code UnpackHandler} determines whether or not unpacking is required and
450+
* provides a SHA1 hash if required.
451+
*/
452+
interface UnpackHandler {
453+
454+
boolean requiresUnpack(String name);
455+
456+
String sha1Hash(String name) throws IOException;
457+
458+
}
459+
460+
private static final class NeverUnpackHandler implements UnpackHandler {
461+
462+
@Override
463+
public boolean requiresUnpack(String name) {
464+
return false;
465+
}
466+
467+
@Override
468+
public String sha1Hash(String name) {
469+
throw new UnsupportedOperationException();
470+
}
471+
472+
}
473+
474+
private static final class LibraryUnpackHandler implements UnpackHandler {
475+
476+
private final Library library;
477+
478+
private LibraryUnpackHandler(Library library) {
479+
this.library = library;
480+
}
481+
482+
@Override
483+
public boolean requiresUnpack(String name) {
484+
return this.library.isUnpackRequired();
485+
}
486+
487+
@Override
488+
public String sha1Hash(String name) throws IOException {
489+
return FileUtils.sha1Hash(this.library.getFile());
490+
}
491+
492+
}
493+
424494
}

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

+66-48
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2017 the original author or authors.
2+
* Copyright 2012-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,16 +21,18 @@
2121
import java.io.IOException;
2222
import java.io.InputStream;
2323
import java.util.ArrayList;
24-
import java.util.HashSet;
24+
import java.util.LinkedHashMap;
2525
import java.util.List;
26-
import java.util.Set;
26+
import java.util.Map;
27+
import java.util.Map.Entry;
2728
import java.util.concurrent.TimeUnit;
2829
import java.util.jar.JarFile;
2930
import java.util.jar.Manifest;
3031

3132
import org.apache.commons.compress.archivers.jar.JarArchiveEntry;
3233

3334
import org.springframework.boot.loader.tools.JarWriter.EntryTransformer;
35+
import org.springframework.boot.loader.tools.JarWriter.UnpackHandler;
3436
import org.springframework.core.io.support.SpringFactoriesLoader;
3537
import org.springframework.util.Assert;
3638
import org.springframework.util.StringUtils;
@@ -231,53 +233,19 @@ private boolean alreadyRepackaged() throws IOException {
231233

232234
private void repackage(JarFile sourceJar, File destination, Libraries libraries,
233235
LaunchScript launchScript) throws IOException {
236+
WritableLibraries writeableLibraries = new WritableLibraries(libraries);
234237
try (JarWriter writer = new JarWriter(destination, launchScript)) {
235-
final List<Library> unpackLibraries = new ArrayList<>();
236-
final List<Library> standardLibraries = new ArrayList<>();
237-
libraries.doWithLibraries((library) -> {
238-
File file = library.getFile();
239-
if (isZip(file)) {
240-
if (library.isUnpackRequired()) {
241-
unpackLibraries.add(library);
242-
}
243-
else {
244-
standardLibraries.add(library);
245-
}
246-
}
247-
});
248-
repackage(sourceJar, writer, unpackLibraries, standardLibraries);
249-
}
250-
}
251-
252-
private void repackage(JarFile sourceJar, JarWriter writer,
253-
final List<Library> unpackLibraries, final List<Library> standardLibraries)
254-
throws IOException {
255-
writer.writeManifest(buildManifest(sourceJar));
256-
Set<String> seen = new HashSet<>();
257-
writeNestedLibraries(unpackLibraries, seen, writer);
258-
if (this.layout instanceof RepackagingLayout) {
259-
writer.writeEntries(sourceJar, new RenamingEntryTransformer(
260-
((RepackagingLayout) this.layout).getRepackagedClassesLocation()));
261-
}
262-
else {
263-
writer.writeEntries(sourceJar);
264-
}
265-
writeNestedLibraries(standardLibraries, seen, writer);
266-
writeLoaderClasses(writer);
267-
}
268-
269-
private void writeNestedLibraries(List<Library> libraries, Set<String> alreadySeen,
270-
JarWriter writer) throws IOException {
271-
for (Library library : libraries) {
272-
String destination = Repackager.this.layout
273-
.getLibraryDestination(library.getName(), library.getScope());
274-
if (destination != null) {
275-
if (!alreadySeen.add(destination + library.getName())) {
276-
throw new IllegalStateException(
277-
"Duplicate library " + library.getName());
278-
}
279-
writer.writeNestedLibrary(destination, library);
238+
writer.writeManifest(buildManifest(sourceJar));
239+
writeLoaderClasses(writer);
240+
if (this.layout instanceof RepackagingLayout) {
241+
writer.writeEntries(sourceJar, new RenamingEntryTransformer(
242+
((RepackagingLayout) this.layout).getRepackagedClassesLocation()),
243+
writeableLibraries);
280244
}
245+
else {
246+
writer.writeEntries(sourceJar, writeableLibraries);
247+
}
248+
writeableLibraries.write(writer);
281249
}
282250
}
283251

@@ -443,4 +411,54 @@ public JarArchiveEntry transform(JarArchiveEntry entry) {
443411

444412
}
445413

414+
/**
415+
* An {@link UnpackHandler} that determines that an entry needs to be unpacked if a
416+
* library that requires unpacking has a matching entry name.
417+
*/
418+
private final class WritableLibraries implements UnpackHandler {
419+
420+
private final Map<String, Library> libraryEntryNames = new LinkedHashMap<>();
421+
422+
private WritableLibraries(Libraries libraries) throws IOException {
423+
libraries.doWithLibraries((library) -> {
424+
if (isZip(library.getFile())) {
425+
String libraryDestination = Repackager.this.layout
426+
.getLibraryDestination(library.getName(), library.getScope())
427+
+ library.getName();
428+
Library existing = this.libraryEntryNames
429+
.putIfAbsent(libraryDestination, library);
430+
if (existing != null) {
431+
throw new IllegalStateException(
432+
"Duplicate library " + library.getName());
433+
}
434+
}
435+
});
436+
}
437+
438+
@Override
439+
public boolean requiresUnpack(String name) {
440+
Library library = this.libraryEntryNames.get(name);
441+
return library != null && library.isUnpackRequired();
442+
}
443+
444+
@Override
445+
public String sha1Hash(String name) throws IOException {
446+
Library library = this.libraryEntryNames.get(name);
447+
if (library == null) {
448+
throw new IllegalArgumentException(
449+
"No library found for entry name '" + name + "'");
450+
}
451+
return FileUtils.sha1Hash(library.getFile());
452+
}
453+
454+
private void write(JarWriter writer) throws IOException {
455+
for (Entry<String, Library> entry : this.libraryEntryNames.entrySet()) {
456+
writer.writeNestedLibrary(
457+
entry.getKey().substring(0, entry.getKey().lastIndexOf('/') + 1),
458+
entry.getValue());
459+
}
460+
}
461+
462+
}
463+
446464
}

0 commit comments

Comments
 (0)