Skip to content

Commit 87fe0b2

Browse files
committed
Use a conventional delegation model in LaunchedURLClassLoader
When an application is run as an executable archive with nested jars, the application's own classes need to be able to load classes from within the nested jars. This means that the application's classes need to be loaded by the same class loader as is used for the nested jars. When an application is launched with java -jar the contents of the jar are on the class path of the app class loader, which is the parent of the LaunchedURLClassLoader that is used to load classes from within the nested jars. If the root of the jar includes the application's classes, they would be loaded by the app class loader and, therefore, would not be able to load classes from within the nested jars. Previously, this problem was resolved by LaunchedURLClassLoader being created with a copy of all of the app class laoder's URLs and by using an unconventional delegation model that caused it to skip its parent (the app class loader) and jump straight to its root class loader. This ensured that the LaunchedURLClassLoader would load both the application's own classes and those from within any nested jars. Unfortunately, this unusual delegation model has proved to be problematic. We have seen and worked around some problems with Java Agents (see gh-4911 and gh-863), but there are others (see gh-4868) that cannot be made to work with the current delegation model. This commit reworks LaunchedURLClassLoader to use a conventional delegate model with the app class loader as its parent. With this change in place, the application's own classes need to be hidden from the app class loader via some other means. This is now achieved by packaging application classes in BOOT-INF/classes (and, for symmetry, nested jars are now packaged in BOOT-INF/lib). Both the JarLauncher and the PropertiesLauncher (which supports the executable jar layout) have been updated to look for classes and nested jars in these new locations. Closes gh-4897 Fixes gh-4868
1 parent 9be69f1 commit 87fe0b2

File tree

27 files changed

+227
-735
lines changed

27 files changed

+227
-735
lines changed

spring-boot-cli/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@
220220
<goal>copy-dependencies</goal>
221221
</goals>
222222
<configuration>
223-
<outputDirectory>${project.build.directory}/assembly/lib</outputDirectory>
223+
<outputDirectory>${project.build.directory}/assembly/BOOT-INF/lib</outputDirectory>
224224
<includeScope>runtime</includeScope>
225225
</configuration>
226226
</execution>

spring-boot-cli/src/main/assembly/jar-with-dependencies.xml

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<include>${project.groupId}:${project.artifactId}</include>
1616
</includes>
1717
<unpack>true</unpack>
18+
<outputDirectory>BOOT-INF/classes/</outputDirectory>
1819
</dependencySet>
1920
</dependencySets>
2021
<fileSets>

spring-boot-cli/src/main/java/org/springframework/boot/cli/archive/PackagedSpringApplicationLauncher.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2014 the original author or authors.
2+
* Copyright 2012-2016 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.
@@ -52,7 +52,7 @@ private void run(String[] args) throws Exception {
5252
}
5353

5454
private Object[] getSources(URLClassLoader classLoader) throws Exception {
55-
Enumeration<URL> urls = classLoader.findResources("META-INF/MANIFEST.MF");
55+
Enumeration<URL> urls = classLoader.getResources("META-INF/MANIFEST.MF");
5656
while (urls.hasMoreElements()) {
5757
URL url = urls.nextElement();
5858
Manifest manifest = new Manifest(url.openStream());

spring-boot-integration-tests/spring-boot-gradle-tests/src/test/java/org/springframework/boot/gradle/MultiProjectRepackagingTests.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2015 the original author or authors.
2+
* Copyright 2012-2016 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.
@@ -42,8 +42,9 @@ public void repackageWithTransitiveFileDependency() throws Exception {
4242
File buildLibs = new File(
4343
"target/multi-project-transitive-file-dependency/main/build/libs");
4444
JarFile jarFile = new JarFile(new File(buildLibs, "main.jar"));
45-
assertThat(jarFile.getEntry("lib/commons-logging-1.1.3.jar")).isNotNull();
46-
assertThat(jarFile.getEntry("lib/foo.jar")).isNotNull();
45+
assertThat(jarFile.getEntry("BOOT-INF/lib/commons-logging-1.1.3.jar"))
46+
.isNotNull();
47+
assertThat(jarFile.getEntry("BOOT-INF/lib/foo.jar")).isNotNull();
4748
jarFile.close();
4849
}
4950

@@ -57,7 +58,7 @@ public void repackageWithCommonFileDependency() throws Exception {
5758
"target/multi-project-common-file-dependency/build/libs");
5859
JarFile jarFile = new JarFile(
5960
new File(buildLibs, "multi-project-common-file-dependency.jar"));
60-
assertThat(jarFile.getEntry("lib/foo.jar")).isNotNull();
61+
assertThat(jarFile.getEntry("BOOT-INF/lib/foo.jar")).isNotNull();
6162
jarFile.close();
6263
}
6364

@@ -70,7 +71,7 @@ public void repackageWithRuntimeProjectDependency() throws Exception {
7071
File buildLibs = new File(
7172
"target/multi-project-runtime-project-dependency/projectA/build/libs");
7273
JarFile jarFile = new JarFile(new File(buildLibs, "projectA.jar"));
73-
assertThat(jarFile.getEntry("lib/projectB.jar")).isNotNull();
74+
assertThat(jarFile.getEntry("BOOT-INF/lib/projectB.jar")).isNotNull();
7475
jarFile.close();
7576
}
7677

spring-boot-integration-tests/spring-boot-gradle-tests/src/test/java/org/springframework/boot/gradle/RepackagingTests.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2015 the original author or authors.
2+
* Copyright 2012-2016 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.
@@ -144,7 +144,7 @@ public void repackageWithFileDependency() throws Exception {
144144
.run();
145145
File buildLibs = new File("target/repackage/build/libs");
146146
JarFile jarFile = new JarFile(new File(buildLibs, "repackage.jar"));
147-
assertThat(jarFile.getEntry("lib/foo.jar")).isNotNull();
147+
assertThat(jarFile.getEntry("BOOT-INF/lib/foo.jar")).isNotNull();
148148
jarFile.close();
149149
}
150150

@@ -166,7 +166,7 @@ public void repackagingEnabledExcludeDevtools() throws IOException {
166166
private boolean isDevToolsJarIncluded(File repackageFile) throws IOException {
167167
JarFile jarFile = new JarFile(repackageFile);
168168
try {
169-
String name = "lib/spring-boot-devtools-" + BOOT_VERSION + ".jar";
169+
String name = "BOOT-INF/lib/spring-boot-devtools-" + BOOT_VERSION + ".jar";
170170
return jarFile.getEntry(name) != null;
171171
}
172172
finally {

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

+28-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2015 the original author or authors.
2+
* Copyright 2012-2016 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.
@@ -120,6 +120,11 @@ public void write(OutputStream outputStream) throws IOException {
120120
* @throws IOException if the entries cannot be written
121121
*/
122122
public void writeEntries(JarFile jarFile) throws IOException {
123+
this.writeEntries(jarFile, new IdentityEntryTransformer());
124+
}
125+
126+
void writeEntries(JarFile jarFile, EntryTransformer entryTransformer)
127+
throws IOException {
123128
Enumeration<JarEntry> entries = jarFile.entries();
124129
while (entries.hasMoreElements()) {
125130
JarEntry entry = entries.nextElement();
@@ -133,7 +138,7 @@ public void writeEntries(JarFile jarFile) throws IOException {
133138
jarFile.getInputStream(entry));
134139
}
135140
EntryWriter entryWriter = new InputStreamEntryWriter(inputStream, true);
136-
writeEntry(entry, entryWriter);
141+
writeEntry(entryTransformer.transform(entry), entryWriter);
137142
}
138143
finally {
139144
inputStream.close();
@@ -377,4 +382,25 @@ public void setupStoredEntry(JarEntry entry) {
377382
}
378383
}
379384

385+
/**
386+
* An {@code EntryTransformer} enables the transformation of {@link JarEntry jar
387+
* entries} during the writing process.
388+
*/
389+
interface EntryTransformer {
390+
391+
JarEntry transform(JarEntry jarEntry);
392+
}
393+
394+
/**
395+
* An {@code EntryTransformer} that returns the entry unchanged.
396+
*/
397+
private static final class IdentityEntryTransformer implements EntryTransformer {
398+
399+
@Override
400+
public JarEntry transform(JarEntry jarEntry) {
401+
return jarEntry;
402+
}
403+
404+
}
405+
380406
}

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2013 the original author or authors.
2+
* Copyright 2012-2016 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.
@@ -60,7 +60,7 @@ public static Layout forFile(File file) {
6060
/**
6161
* Executable JAR layout.
6262
*/
63-
public static class Jar implements Layout {
63+
public static class Jar implements RepackagingLayout {
6464

6565
@Override
6666
public String getLauncherClassName() {
@@ -69,14 +69,19 @@ public String getLauncherClassName() {
6969

7070
@Override
7171
public String getLibraryDestination(String libraryName, LibraryScope scope) {
72-
return "lib/";
72+
return "BOOT-INF/lib/";
7373
}
7474

7575
@Override
7676
public String getClassesLocation() {
7777
return "";
7878
}
7979

80+
@Override
81+
public String getRepackagedClassesLocation() {
82+
return "BOOT-INF/classes/";
83+
}
84+
8085
@Override
8186
public boolean isExecutable() {
8287
return true;

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

+55-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2015 the original author or authors.
2+
* Copyright 2012-2016 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.
@@ -24,9 +24,12 @@
2424
import java.util.HashSet;
2525
import java.util.List;
2626
import java.util.Set;
27+
import java.util.jar.JarEntry;
2728
import java.util.jar.JarFile;
2829
import java.util.jar.Manifest;
2930

31+
import org.springframework.boot.loader.tools.JarWriter.EntryTransformer;
32+
3033
/**
3134
* Utility class that can be used to repackage an archive so that it can be executed using
3235
* '{@literal java -jar}'.
@@ -189,7 +192,14 @@ public void library(Library library) throws IOException {
189192
writer.writeManifest(buildManifest(sourceJar));
190193
Set<String> seen = new HashSet<String>();
191194
writeNestedLibraries(unpackLibraries, seen, writer);
192-
writer.writeEntries(sourceJar);
195+
if (this.layout instanceof RepackagingLayout) {
196+
writer.writeEntries(sourceJar,
197+
new RenamingEntryTransformer(((RepackagingLayout) this.layout)
198+
.getRepackagedClassesLocation()));
199+
}
200+
else {
201+
writer.writeEntries(sourceJar);
202+
}
193203
writeNestedLibraries(standardLibraries, seen, writer);
194204
if (this.layout.isExecutable()) {
195205
writer.writeLoaderClasses();
@@ -293,4 +303,47 @@ private void deleteFile(File file) {
293303
}
294304
}
295305

306+
/**
307+
* An {@code EntryTransformer} that renames entries by applying a prefix.
308+
*/
309+
private static final class RenamingEntryTransformer implements EntryTransformer {
310+
311+
private final String namePrefix;
312+
313+
private RenamingEntryTransformer(String namePrefix) {
314+
this.namePrefix = namePrefix;
315+
}
316+
317+
@Override
318+
public JarEntry transform(JarEntry entry) {
319+
if (entry.getName().startsWith("META-INF/")
320+
|| entry.getName().startsWith("BOOT-INF/")) {
321+
return entry;
322+
}
323+
JarEntry renamedEntry = new JarEntry(this.namePrefix + entry.getName());
324+
renamedEntry.setTime(entry.getTime());
325+
renamedEntry.setSize(entry.getSize());
326+
renamedEntry.setMethod(entry.getMethod());
327+
if (entry.getComment() != null) {
328+
renamedEntry.setComment(entry.getComment());
329+
}
330+
renamedEntry.setCompressedSize(entry.getCompressedSize());
331+
renamedEntry.setCrc(entry.getCrc());
332+
if (entry.getCreationTime() != null) {
333+
renamedEntry.setCreationTime(entry.getCreationTime());
334+
}
335+
if (entry.getExtra() != null) {
336+
renamedEntry.setExtra(entry.getExtra());
337+
}
338+
if (entry.getLastAccessTime() != null) {
339+
renamedEntry.setLastAccessTime(entry.getLastAccessTime());
340+
}
341+
if (entry.getLastModifiedTime() != null) {
342+
renamedEntry.setLastModifiedTime(entry.getLastModifiedTime());
343+
}
344+
return renamedEntry;
345+
}
346+
347+
}
348+
296349
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2015 the original author or authors.
2+
* Copyright 2012-2016 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.
@@ -14,24 +14,21 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.boot.loader;
18-
19-
import java.net.URL;
17+
package org.springframework.boot.loader.tools;
2018

2119
/**
22-
* A strategy for detecting Java agents.
20+
* A specialization of {@link Layout} that repackages an existing archive by moving its
21+
* content to a new location.
2322
*
2423
* @author Andy Wilkinson
25-
* @since 1.1.0
24+
* @since 1.4.0
2625
*/
27-
public interface JavaAgentDetector {
26+
public interface RepackagingLayout extends Layout {
2827

2928
/**
30-
* Returns {@code true} if {@code url} points to a Java agent jar file, otherwise
31-
* {@code false} is returned.
32-
* @param url The url to examine
33-
* @return if the URL points to a Java agent
29+
* Returns the location to which classes should be moved.
30+
* @return the repackaged classes location
3431
*/
35-
boolean isJavaAgentJar(URL url);
32+
String getRepackagedClassesLocation();
3633

3734
}

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2013 the original author or authors.
2+
* Copyright 2012-2016 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.
@@ -64,13 +64,13 @@ public void unknownFile() throws Exception {
6464
public void jarLayout() throws Exception {
6565
Layout layout = new Layouts.Jar();
6666
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.COMPILE))
67-
.isEqualTo("lib/");
67+
.isEqualTo("BOOT-INF/lib/");
6868
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.CUSTOM))
69-
.isEqualTo("lib/");
69+
.isEqualTo("BOOT-INF/lib/");
7070
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.PROVIDED))
71-
.isEqualTo("lib/");
71+
.isEqualTo("BOOT-INF/lib/");
7272
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.RUNTIME))
73-
.isEqualTo("lib/");
73+
.isEqualTo("BOOT-INF/lib/");
7474
}
7575

7676
@Test

0 commit comments

Comments
 (0)