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
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
* Copyright 2012-2016 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.
|
||||
@@ -120,6 +120,11 @@ public class JarWriter {
|
||||
* @throws IOException if the entries cannot be written
|
||||
*/
|
||||
public void writeEntries(JarFile jarFile) throws IOException {
|
||||
this.writeEntries(jarFile, new IdentityEntryTransformer());
|
||||
}
|
||||
|
||||
void writeEntries(JarFile jarFile, EntryTransformer entryTransformer)
|
||||
throws IOException {
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
@@ -133,7 +138,7 @@ public class JarWriter {
|
||||
jarFile.getInputStream(entry));
|
||||
}
|
||||
EntryWriter entryWriter = new InputStreamEntryWriter(inputStream, true);
|
||||
writeEntry(entry, entryWriter);
|
||||
writeEntry(entryTransformer.transform(entry), entryWriter);
|
||||
}
|
||||
finally {
|
||||
inputStream.close();
|
||||
@@ -377,4 +382,25 @@ public class JarWriter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@code EntryTransformer} enables the transformation of {@link JarEntry jar
|
||||
* entries} during the writing process.
|
||||
*/
|
||||
interface EntryTransformer {
|
||||
|
||||
JarEntry transform(JarEntry jarEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@code EntryTransformer} that returns the entry unchanged.
|
||||
*/
|
||||
private static final class IdentityEntryTransformer implements EntryTransformer {
|
||||
|
||||
@Override
|
||||
public JarEntry transform(JarEntry jarEntry) {
|
||||
return jarEntry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2013 the original author or authors.
|
||||
* Copyright 2012-2016 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.
|
||||
@@ -60,7 +60,7 @@ public final class Layouts {
|
||||
/**
|
||||
* Executable JAR layout.
|
||||
*/
|
||||
public static class Jar implements Layout {
|
||||
public static class Jar implements RepackagingLayout {
|
||||
|
||||
@Override
|
||||
public String getLauncherClassName() {
|
||||
@@ -69,7 +69,7 @@ public final class Layouts {
|
||||
|
||||
@Override
|
||||
public String getLibraryDestination(String libraryName, LibraryScope scope) {
|
||||
return "lib/";
|
||||
return "BOOT-INF/lib/";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -77,6 +77,11 @@ public final class Layouts {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRepackagedClassesLocation() {
|
||||
return "BOOT-INF/classes/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExecutable() {
|
||||
return true;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
* Copyright 2012-2016 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.
|
||||
@@ -24,9 +24,12 @@ import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.springframework.boot.loader.tools.JarWriter.EntryTransformer;
|
||||
|
||||
/**
|
||||
* Utility class that can be used to repackage an archive so that it can be executed using
|
||||
* '{@literal java -jar}'.
|
||||
@@ -189,7 +192,14 @@ public class Repackager {
|
||||
writer.writeManifest(buildManifest(sourceJar));
|
||||
Set<String> seen = new HashSet<String>();
|
||||
writeNestedLibraries(unpackLibraries, seen, writer);
|
||||
writer.writeEntries(sourceJar);
|
||||
if (this.layout instanceof RepackagingLayout) {
|
||||
writer.writeEntries(sourceJar,
|
||||
new RenamingEntryTransformer(((RepackagingLayout) this.layout)
|
||||
.getRepackagedClassesLocation()));
|
||||
}
|
||||
else {
|
||||
writer.writeEntries(sourceJar);
|
||||
}
|
||||
writeNestedLibraries(standardLibraries, seen, writer);
|
||||
if (this.layout.isExecutable()) {
|
||||
writer.writeLoaderClasses();
|
||||
@@ -293,4 +303,47 @@ public class Repackager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@code EntryTransformer} that renames entries by applying a prefix.
|
||||
*/
|
||||
private static final class RenamingEntryTransformer implements EntryTransformer {
|
||||
|
||||
private final String namePrefix;
|
||||
|
||||
private RenamingEntryTransformer(String namePrefix) {
|
||||
this.namePrefix = namePrefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JarEntry transform(JarEntry entry) {
|
||||
if (entry.getName().startsWith("META-INF/")
|
||||
|| entry.getName().startsWith("BOOT-INF/")) {
|
||||
return entry;
|
||||
}
|
||||
JarEntry renamedEntry = new JarEntry(this.namePrefix + entry.getName());
|
||||
renamedEntry.setTime(entry.getTime());
|
||||
renamedEntry.setSize(entry.getSize());
|
||||
renamedEntry.setMethod(entry.getMethod());
|
||||
if (entry.getComment() != null) {
|
||||
renamedEntry.setComment(entry.getComment());
|
||||
}
|
||||
renamedEntry.setCompressedSize(entry.getCompressedSize());
|
||||
renamedEntry.setCrc(entry.getCrc());
|
||||
if (entry.getCreationTime() != null) {
|
||||
renamedEntry.setCreationTime(entry.getCreationTime());
|
||||
}
|
||||
if (entry.getExtra() != null) {
|
||||
renamedEntry.setExtra(entry.getExtra());
|
||||
}
|
||||
if (entry.getLastAccessTime() != null) {
|
||||
renamedEntry.setLastAccessTime(entry.getLastAccessTime());
|
||||
}
|
||||
if (entry.getLastModifiedTime() != null) {
|
||||
renamedEntry.setLastModifiedTime(entry.getLastModifiedTime());
|
||||
}
|
||||
return renamedEntry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2012-2016 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
|
||||
*
|
||||
* http://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.loader.tools;
|
||||
|
||||
/**
|
||||
* A specialization of {@link Layout} that repackages an existing archive by moving its
|
||||
* content to a new location.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public interface RepackagingLayout extends Layout {
|
||||
|
||||
/**
|
||||
* Returns the location to which classes should be moved.
|
||||
* @return the repackaged classes location
|
||||
*/
|
||||
String getRepackagedClassesLocation();
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2013 the original author or authors.
|
||||
* Copyright 2012-2016 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.
|
||||
@@ -64,13 +64,13 @@ public class LayoutsTests {
|
||||
public void jarLayout() throws Exception {
|
||||
Layout layout = new Layouts.Jar();
|
||||
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.COMPILE))
|
||||
.isEqualTo("lib/");
|
||||
.isEqualTo("BOOT-INF/lib/");
|
||||
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.CUSTOM))
|
||||
.isEqualTo("lib/");
|
||||
.isEqualTo("BOOT-INF/lib/");
|
||||
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.PROVIDED))
|
||||
.isEqualTo("lib/");
|
||||
.isEqualTo("BOOT-INF/lib/");
|
||||
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.RUNTIME))
|
||||
.isEqualTo("lib/");
|
||||
.isEqualTo("BOOT-INF/lib/");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
* Copyright 2012-2016 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.
|
||||
@@ -291,7 +291,7 @@ public class RepackagerTests {
|
||||
final File libNonJarFile = this.temporaryFolder.newFile();
|
||||
FileCopyUtils.copy(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }, libNonJarFile);
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addFile("lib/" + libJarFileToUnpack.getName(),
|
||||
this.testJarFile.addFile("BOOT-INF/lib/" + libJarFileToUnpack.getName(),
|
||||
libJarFileToUnpack);
|
||||
File file = this.testJarFile.getFile();
|
||||
libJarFile.setLastModified(JAN_1_1980);
|
||||
@@ -305,12 +305,13 @@ public class RepackagerTests {
|
||||
callback.library(new Library(libNonJarFile, LibraryScope.COMPILE));
|
||||
}
|
||||
});
|
||||
assertThat(hasEntry(file, "lib/" + libJarFile.getName())).isTrue();
|
||||
assertThat(hasEntry(file, "lib/" + libJarFileToUnpack.getName())).isTrue();
|
||||
assertThat(hasEntry(file, "lib/" + libNonJarFile.getName())).isFalse();
|
||||
JarEntry entry = getEntry(file, "lib/" + libJarFile.getName());
|
||||
assertThat(hasEntry(file, "BOOT-INF/lib/" + libJarFile.getName())).isTrue();
|
||||
assertThat(hasEntry(file, "BOOT-INF/lib/" + libJarFileToUnpack.getName()))
|
||||
.isTrue();
|
||||
assertThat(hasEntry(file, "BOOT-INF/lib/" + libNonJarFile.getName())).isFalse();
|
||||
JarEntry entry = getEntry(file, "BOOT-INF/lib/" + libJarFile.getName());
|
||||
assertThat(entry.getTime()).isEqualTo(JAN_1_1985);
|
||||
entry = getEntry(file, "lib/" + libJarFileToUnpack.getName());
|
||||
entry = getEntry(file, "BOOT-INF/lib/" + libJarFileToUnpack.getName());
|
||||
assertThat(entry.getComment()).startsWith("UNPACK:");
|
||||
assertThat(entry.getComment().length()).isEqualTo(47);
|
||||
}
|
||||
@@ -395,9 +396,10 @@ public class RepackagerTests {
|
||||
});
|
||||
JarFile jarFile = new JarFile(file);
|
||||
try {
|
||||
assertThat(jarFile.getEntry("lib/" + nestedFile.getName()).getMethod())
|
||||
.isEqualTo(ZipEntry.STORED);
|
||||
assertThat(jarFile.getEntry("test/nested.jar").getMethod())
|
||||
assertThat(
|
||||
jarFile.getEntry("BOOT-INF/lib/" + nestedFile.getName()).getMethod())
|
||||
.isEqualTo(ZipEntry.STORED);
|
||||
assertThat(jarFile.getEntry("BOOT-INF/classes/test/nested.jar").getMethod())
|
||||
.isEqualTo(ZipEntry.STORED);
|
||||
}
|
||||
finally {
|
||||
@@ -432,7 +434,8 @@ public class RepackagerTests {
|
||||
TestJarFile nested = new TestJarFile(this.temporaryFolder);
|
||||
nested.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
final File nestedFile = nested.getFile();
|
||||
this.testJarFile.addFile("lib/" + nestedFile.getName(), nested.getFile());
|
||||
this.testJarFile.addFile("BOOT-INF/lib/" + nestedFile.getName(),
|
||||
nested.getFile());
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
@@ -446,8 +449,9 @@ public class RepackagerTests {
|
||||
});
|
||||
JarFile jarFile = new JarFile(file);
|
||||
try {
|
||||
assertThat(jarFile.getEntry("lib/" + nestedFile.getName()).getComment())
|
||||
.startsWith("UNPACK:");
|
||||
assertThat(
|
||||
jarFile.getEntry("BOOT-INF/lib/" + nestedFile.getName()).getComment())
|
||||
.startsWith("UNPACK:");
|
||||
}
|
||||
finally {
|
||||
jarFile.close();
|
||||
@@ -460,7 +464,8 @@ public class RepackagerTests {
|
||||
TestJarFile nested = new TestJarFile(this.temporaryFolder);
|
||||
nested.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
final File nestedFile = nested.getFile();
|
||||
this.testJarFile.addFile("lib/" + nestedFile.getName(), nested.getFile());
|
||||
this.testJarFile.addFile("BOOT-INF/lib/" + nestedFile.getName(),
|
||||
nested.getFile());
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
@@ -478,7 +483,7 @@ public class RepackagerTests {
|
||||
});
|
||||
JarFile jarFile = new JarFile(file);
|
||||
try {
|
||||
assertThat(jarFile.getEntry("lib/" + nestedFile.getName()).getSize())
|
||||
assertThat(jarFile.getEntry("BOOT-INF/lib/" + nestedFile.getName()).getSize())
|
||||
.isEqualTo(sourceLength);
|
||||
}
|
||||
finally {
|
||||
|
||||
Reference in New Issue
Block a user