Skip to content

Commit 45b1ab4

Browse files
mbhavephilwebb
andcommitted
Add classpath index support for exploded archives
Update the `Repackager` class so that an additional `classpath.idx` file is written into the jar that provides the original order of the classpath. The `JarLauncher` class now uses this file when running as an exploded archive to ensure that the classpath order is the same as when running from the far jar. Closes gh-9128 Co-authored-by: Phillip Webb <[email protected]>
1 parent ad72f86 commit 45b1ab4

File tree

12 files changed

+442
-3
lines changed

12 files changed

+442
-3
lines changed

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

+27
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.loader.tools;
1818

1919
import java.io.BufferedInputStream;
20+
import java.io.BufferedWriter;
2021
import java.io.ByteArrayInputStream;
2122
import java.io.ByteArrayOutputStream;
2223
import java.io.File;
@@ -27,13 +28,16 @@
2728
import java.io.IOException;
2829
import java.io.InputStream;
2930
import java.io.OutputStream;
31+
import java.io.OutputStreamWriter;
3032
import java.net.URL;
33+
import java.nio.charset.StandardCharsets;
3134
import java.nio.file.Files;
3235
import java.nio.file.Path;
3336
import java.nio.file.attribute.PosixFilePermission;
3437
import java.util.Arrays;
3538
import java.util.Enumeration;
3639
import java.util.HashSet;
40+
import java.util.List;
3741
import java.util.Set;
3842
import java.util.jar.JarEntry;
3943
import java.util.jar.JarFile;
@@ -52,6 +56,7 @@
5256
*
5357
* @author Phillip Webb
5458
* @author Andy Wilkinson
59+
* @author Madhura Bhave
5560
* @since 1.0.0
5661
*/
5762
public class JarWriter implements LoaderClassesWriter, AutoCloseable {
@@ -190,6 +195,28 @@ public void writeNestedLibrary(String destination, Library library) throws IOExc
190195
}
191196
}
192197

198+
/**
199+
* Write a simple index file containing the specified UTF-8 lines.
200+
* @param location the location of the index file
201+
* @param lines the lines to write
202+
* @throws IOException if the write fails
203+
* @since 2.3.0
204+
*/
205+
public void writeIndexFile(String location, List<String> lines) throws IOException {
206+
if (location != null) {
207+
JarArchiveEntry entry = new JarArchiveEntry(location);
208+
writeEntry(entry, (outputStream) -> {
209+
BufferedWriter writer = new BufferedWriter(
210+
new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
211+
for (String line : lines) {
212+
writer.write(line);
213+
writer.write("\n");
214+
}
215+
writer.flush();
216+
});
217+
}
218+
}
219+
193220
private long getNestedLibraryTime(File file) {
194221
try {
195222
try (JarFile jarFile = new JarFile(file)) {

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

+6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
* @author Phillip Webb
2929
* @author Dave Syer
3030
* @author Andy Wilkinson
31+
* @author Madhura Bhave
3132
* @since 1.0.0
3233
*/
3334
public final class Layouts {
@@ -88,6 +89,11 @@ public String getRepackagedClassesLocation() {
8889
return "BOOT-INF/classes/";
8990
}
9091

92+
@Override
93+
public String getClasspathIndexFileLocation() {
94+
return "BOOT-INF/classpath.idx";
95+
}
96+
9197
@Override
9298
public boolean isExecutable() {
9399
return true;

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

+8
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
* @author Phillip Webb
4646
* @author Andy Wilkinson
4747
* @author Stephane Nicoll
48+
* @author Madhura Bhave
4849
* @since 1.0.0
4950
*/
5051
public class Repackager {
@@ -59,6 +60,8 @@ public class Repackager {
5960

6061
private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib";
6162

63+
private static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";
64+
6265
private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 };
6366

6467
private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
@@ -336,6 +339,7 @@ private void addBootAttributes(Attributes attributes) {
336339
private void addBootBootAttributesForRepackagingLayout(Attributes attributes, RepackagingLayout layout) {
337340
attributes.putValue(BOOT_CLASSES_ATTRIBUTE, layout.getRepackagedClassesLocation());
338341
putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, this.layout.getLibraryLocation("", LibraryScope.COMPILE));
342+
putIfHasLength(attributes, BOOT_CLASSPATH_INDEX_ATTRIBUTE, layout.getClasspathIndexFileLocation());
339343
}
340344

341345
private void addBootBootAttributesForPlainLayout(Attributes attributes, Layout layout) {
@@ -473,6 +477,10 @@ private void write(JarWriter writer) throws IOException {
473477
writer.writeNestedLibrary(entry.getKey().substring(0, entry.getKey().lastIndexOf('/') + 1),
474478
entry.getValue());
475479
}
480+
if (Repackager.this.layout instanceof RepackagingLayout) {
481+
String location = ((RepackagingLayout) (Repackager.this.layout)).getClasspathIndexFileLocation();
482+
writer.writeIndexFile(location, new ArrayList<>(this.libraryEntryNames.keySet()));
483+
}
476484
}
477485

478486
}

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

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 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.
@@ -31,4 +31,15 @@ public interface RepackagingLayout extends Layout {
3131
*/
3232
String getRepackagedClassesLocation();
3333

34+
/**
35+
* Returns the location of the classpath index file that should be written or
36+
* {@code null} if not index is required. The result should include the filename and
37+
* is relative to the root of the jar.
38+
* @return the classpath index file location
39+
* @since 2.3.0
40+
*/
41+
default String getClasspathIndexFileLocation() {
42+
return null;
43+
}
44+
3445
}

spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java

+33
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@
1818

1919
import java.io.ByteArrayInputStream;
2020
import java.io.File;
21+
import java.io.FileInputStream;
2122
import java.io.FileOutputStream;
2223
import java.io.IOException;
24+
import java.nio.charset.StandardCharsets;
2325
import java.nio.file.Files;
2426
import java.nio.file.attribute.PosixFilePermission;
2527
import java.util.ArrayList;
28+
import java.util.Arrays;
2629
import java.util.Calendar;
2730
import java.util.Enumeration;
2831
import java.util.List;
@@ -45,6 +48,7 @@
4548
import org.springframework.boot.loader.tools.sample.ClassWithMainMethod;
4649
import org.springframework.boot.loader.tools.sample.ClassWithoutMainMethod;
4750
import org.springframework.util.FileCopyUtils;
51+
import org.springframework.util.StreamUtils;
4852

4953
import static org.assertj.core.api.Assertions.assertThat;
5054
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -59,6 +63,7 @@
5963
*
6064
* @author Phillip Webb
6165
* @author Andy Wilkinson
66+
* @author Madhura Bhave
6267
*/
6368
class RepackagerTests {
6469

@@ -299,6 +304,34 @@ void libraries() throws Exception {
299304
assertThat(entry.getComment()).hasSize(47);
300305
}
301306

307+
@Test
308+
void index() throws Exception {
309+
TestJarFile libJar1 = new TestJarFile(this.tempDir);
310+
libJar1.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
311+
File libJarFile1 = libJar1.getFile();
312+
TestJarFile libJar2 = new TestJarFile(this.tempDir);
313+
libJar2.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
314+
File libJarFile2 = libJar2.getFile();
315+
TestJarFile libJar3 = new TestJarFile(this.tempDir);
316+
libJar3.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
317+
File libJarFile3 = libJar3.getFile();
318+
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
319+
File file = this.testJarFile.getFile();
320+
Repackager repackager = new Repackager(file);
321+
repackager.repackage((callback) -> {
322+
callback.library(new Library(libJarFile1, LibraryScope.COMPILE));
323+
callback.library(new Library(libJarFile2, LibraryScope.COMPILE));
324+
callback.library(new Library(libJarFile3, LibraryScope.COMPILE));
325+
});
326+
assertThat(hasEntry(file, "BOOT-INF/classpath.idx")).isTrue();
327+
ZipUtil.unpack(file, new File(file.getParent()));
328+
FileInputStream inputStream = new FileInputStream(new File(file.getParent() + "/BOOT-INF/classpath.idx"));
329+
String index = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
330+
String[] libraries = index.split("\\r?\\n");
331+
assertThat(Arrays.asList(libraries)).contains("BOOT-INF/lib/" + libJarFile1.getName(),
332+
"BOOT-INF/lib/" + libJarFile2.getName(), "BOOT-INF/lib/" + libJarFile3.getName());
333+
}
334+
302335
@Test
303336
void duplicateLibraries() throws Exception {
304337
TestJarFile libJar = new TestJarFile(this.tempDir);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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;
18+
19+
import java.io.BufferedReader;
20+
import java.io.File;
21+
import java.io.FileInputStream;
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.io.InputStreamReader;
25+
import java.net.MalformedURLException;
26+
import java.net.URISyntaxException;
27+
import java.net.URL;
28+
import java.nio.charset.StandardCharsets;
29+
import java.util.ArrayList;
30+
import java.util.Collections;
31+
import java.util.List;
32+
import java.util.Objects;
33+
import java.util.Set;
34+
import java.util.stream.Collectors;
35+
36+
/**
37+
* A class path index file that provides ordering information for JARs.
38+
*
39+
* @author Madhura Bhave
40+
* @author Phillip Webb
41+
*/
42+
final class ClassPathIndexFile {
43+
44+
private final File root;
45+
46+
private final List<String> lines;
47+
48+
private final Set<String> folders;
49+
50+
private ClassPathIndexFile(File root, List<String> lines) {
51+
this.root = root;
52+
this.lines = lines;
53+
this.folders = this.lines.stream().map(this::getFolder).filter(Objects::nonNull).collect(Collectors.toSet());
54+
}
55+
56+
private String getFolder(String name) {
57+
int lastSlash = name.lastIndexOf("/");
58+
return (lastSlash != -1) ? name.substring(0, lastSlash) : null;
59+
}
60+
61+
int size() {
62+
return this.lines.size();
63+
}
64+
65+
boolean containsFolder(String name) {
66+
if (name == null || name.isEmpty()) {
67+
return false;
68+
}
69+
if (name.endsWith("/")) {
70+
return containsFolder(name.substring(0, name.length() - 1));
71+
}
72+
return this.folders.contains(name);
73+
}
74+
75+
List<URL> getUrls() {
76+
return Collections.unmodifiableList(this.lines.stream().map(this::asUrl).collect(Collectors.toList()));
77+
}
78+
79+
private URL asUrl(String line) {
80+
try {
81+
return new File(this.root, line).toURI().toURL();
82+
}
83+
catch (MalformedURLException ex) {
84+
throw new IllegalStateException(ex);
85+
}
86+
}
87+
88+
static ClassPathIndexFile loadIfPossible(URL root, String location) throws IOException {
89+
return loadIfPossible(asFile(root), location);
90+
}
91+
92+
private static ClassPathIndexFile loadIfPossible(File root, String location) throws IOException {
93+
return loadIfPossible(root, new File(root, location));
94+
}
95+
96+
private static ClassPathIndexFile loadIfPossible(File root, File indexFile) throws IOException {
97+
if (indexFile.exists() && indexFile.isFile()) {
98+
try (InputStream inputStream = new FileInputStream(indexFile)) {
99+
return new ClassPathIndexFile(root, loadLines(inputStream));
100+
}
101+
}
102+
return null;
103+
}
104+
105+
private static List<String> loadLines(InputStream inputStream) throws IOException {
106+
List<String> lines = new ArrayList<>();
107+
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
108+
String line = reader.readLine();
109+
while (line != null) {
110+
if (!line.trim().isEmpty()) {
111+
lines.add(line);
112+
}
113+
line = reader.readLine();
114+
}
115+
return Collections.unmodifiableList(lines);
116+
}
117+
118+
private static File asFile(URL url) {
119+
if (!"file".equals(url.getProtocol())) {
120+
throw new IllegalArgumentException("URL does not reference a file");
121+
}
122+
try {
123+
return new File(url.toURI());
124+
}
125+
catch (URISyntaxException ex) {
126+
return new File(url.getPath());
127+
}
128+
}
129+
130+
}

0 commit comments

Comments
 (0)