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:
Andy Wilkinson 2016-01-13 15:52:39 +00:00
parent 9be69f1ac7
commit 87fe0b2ade
27 changed files with 230 additions and 738 deletions

View File

@ -220,7 +220,7 @@
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/assembly/lib</outputDirectory>
<outputDirectory>${project.build.directory}/assembly/BOOT-INF/lib</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>

View File

@ -15,6 +15,7 @@
<include>${project.groupId}:${project.artifactId}</include>
</includes>
<unpack>true</unpack>
<outputDirectory>BOOT-INF/classes/</outputDirectory>
</dependencySet>
</dependencySets>
<fileSets>

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 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.
@ -52,7 +52,7 @@ public final class PackagedSpringApplicationLauncher {
}
private Object[] getSources(URLClassLoader classLoader) throws Exception {
Enumeration<URL> urls = classLoader.findResources("META-INF/MANIFEST.MF");
Enumeration<URL> urls = classLoader.getResources("META-INF/MANIFEST.MF");
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
Manifest manifest = new Manifest(url.openStream());

View File

@ -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.
@ -42,8 +42,9 @@ public class MultiProjectRepackagingTests {
File buildLibs = new File(
"target/multi-project-transitive-file-dependency/main/build/libs");
JarFile jarFile = new JarFile(new File(buildLibs, "main.jar"));
assertThat(jarFile.getEntry("lib/commons-logging-1.1.3.jar")).isNotNull();
assertThat(jarFile.getEntry("lib/foo.jar")).isNotNull();
assertThat(jarFile.getEntry("BOOT-INF/lib/commons-logging-1.1.3.jar"))
.isNotNull();
assertThat(jarFile.getEntry("BOOT-INF/lib/foo.jar")).isNotNull();
jarFile.close();
}
@ -57,7 +58,7 @@ public class MultiProjectRepackagingTests {
"target/multi-project-common-file-dependency/build/libs");
JarFile jarFile = new JarFile(
new File(buildLibs, "multi-project-common-file-dependency.jar"));
assertThat(jarFile.getEntry("lib/foo.jar")).isNotNull();
assertThat(jarFile.getEntry("BOOT-INF/lib/foo.jar")).isNotNull();
jarFile.close();
}
@ -70,7 +71,7 @@ public class MultiProjectRepackagingTests {
File buildLibs = new File(
"target/multi-project-runtime-project-dependency/projectA/build/libs");
JarFile jarFile = new JarFile(new File(buildLibs, "projectA.jar"));
assertThat(jarFile.getEntry("lib/projectB.jar")).isNotNull();
assertThat(jarFile.getEntry("BOOT-INF/lib/projectB.jar")).isNotNull();
jarFile.close();
}

View File

@ -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.
@ -144,7 +144,7 @@ public class RepackagingTests {
.run();
File buildLibs = new File("target/repackage/build/libs");
JarFile jarFile = new JarFile(new File(buildLibs, "repackage.jar"));
assertThat(jarFile.getEntry("lib/foo.jar")).isNotNull();
assertThat(jarFile.getEntry("BOOT-INF/lib/foo.jar")).isNotNull();
jarFile.close();
}
@ -166,7 +166,7 @@ public class RepackagingTests {
private boolean isDevToolsJarIncluded(File repackageFile) throws IOException {
JarFile jarFile = new JarFile(repackageFile);
try {
String name = "lib/spring-boot-devtools-" + BOOT_VERSION + ".jar";
String name = "BOOT-INF/lib/spring-boot-devtools-" + BOOT_VERSION + ".jar";
return jarFile.getEntry(name) != null;
}
finally {

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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.
@ -14,24 +14,21 @@
* limitations under the License.
*/
package org.springframework.boot.loader;
import java.net.URL;
package org.springframework.boot.loader.tools;
/**
* A strategy for detecting Java agents.
* A specialization of {@link Layout} that repackages an existing archive by moving its
* content to a new location.
*
* @author Andy Wilkinson
* @since 1.1.0
* @since 1.4.0
*/
public interface JavaAgentDetector {
public interface RepackagingLayout extends Layout {
/**
* Returns {@code true} if {@code url} points to a Java agent jar file, otherwise
* {@code false} is returned.
* @param url The url to examine
* @return if the URL points to a Java agent
* Returns the location to which classes should be moved.
* @return the repackaged classes location
*/
boolean isJavaAgentJar(URL url);
String getRepackagedClassesLocation();
}

View File

@ -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

View File

@ -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 {

View File

@ -49,7 +49,7 @@
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/assembly/lib</outputDirectory>
<outputDirectory>${project.build.directory}/assembly/BOOT-INF/lib</outputDirectory>
</configuration>
</execution>
</executions>

View File

@ -14,6 +14,7 @@
<includes>
<include>${project.groupId}:${project.artifactId}</include>
</includes>
<outputDirectory>BOOT-INF/classes</outputDirectory>
<unpack>true</unpack>
</dependencySet>
</dependencySets>

View File

@ -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.
@ -16,13 +16,8 @@
package org.springframework.boot.loader;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.Manifest;
@ -40,24 +35,16 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
private final Archive archive;
private final JavaAgentDetector javaAgentDetector;
public ExecutableArchiveLauncher() {
this(new InputArgumentsJavaAgentDetector());
}
public ExecutableArchiveLauncher(JavaAgentDetector javaAgentDetector) {
try {
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
this.javaAgentDetector = javaAgentDetector;
}
protected ExecutableArchiveLauncher(Archive archive) {
this.javaAgentDetector = new InputArgumentsJavaAgentDetector();
this.archive = archive;
}
@ -92,31 +79,6 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
return archives;
}
@Override
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
Set<URL> copy = new LinkedHashSet<URL>(urls.length);
ClassLoader loader = getDefaultClassLoader();
if (loader instanceof URLClassLoader) {
for (URL url : ((URLClassLoader) loader).getURLs()) {
if (addDefaultClassloaderUrl(urls, url)) {
copy.add(url);
}
}
}
Collections.addAll(copy, urls);
return super.createClassLoader(copy.toArray(new URL[copy.size()]));
}
private boolean addDefaultClassloaderUrl(URL[] urls, URL url) {
String jarUrl = "jar:" + url + "!/";
for (URL nestedUrl : urls) {
if (nestedUrl.equals(url) || nestedUrl.toString().equals(jarUrl)) {
return false;
}
}
return !this.javaAgentDetector.isJavaAgentJar(url);
}
/**
* Determine if the specified {@link JarEntry} is a nested item that should be added
* to the classpath. The method is called once for each entry.
@ -134,20 +96,4 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
}
private static ClassLoader getDefaultClassLoader() {
ClassLoader classloader = null;
try {
classloader = Thread.currentThread().getContextClassLoader();
}
catch (Throwable ex) {
// Cannot access thread context ClassLoader - falling back to system class
// loader...
}
if (classloader == null) {
// No thread context class loader -> use class loader of this class.
classloader = ExecutableArchiveLauncher.class.getClassLoader();
}
return classloader;
}
}

View File

@ -1,100 +0,0 @@
/*
* Copyright 2012-2014 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;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* A {@link JavaAgentDetector} that detects jars supplied via the {@code -javaagent} JVM
* input argument.
*
* @author Andy Wilkinson
* @since 1.1.0
*/
public class InputArgumentsJavaAgentDetector implements JavaAgentDetector {
private static final String JAVA_AGENT_PREFIX = "-javaagent:";
private final Set<URL> javaAgentJars;
public InputArgumentsJavaAgentDetector() {
this(getInputArguments());
}
InputArgumentsJavaAgentDetector(List<String> inputArguments) {
this.javaAgentJars = getJavaAgentJars(inputArguments);
}
private static List<String> getInputArguments() {
try {
return AccessController.doPrivileged(new PrivilegedAction<List<String>>() {
@Override
public List<String> run() {
return ManagementFactory.getRuntimeMXBean().getInputArguments();
}
});
}
catch (Exception ex) {
return Collections.emptyList();
}
}
private Set<URL> getJavaAgentJars(List<String> inputArguments) {
Set<URL> javaAgentJars = new HashSet<URL>();
for (String argument : inputArguments) {
String path = getJavaAgentJarPath(argument);
if (path != null) {
try {
javaAgentJars.add(new File(path).getCanonicalFile().toURI().toURL());
}
catch (IOException ex) {
throw new IllegalStateException(
"Failed to determine canonical path of Java agent at path '"
+ path + "'");
}
}
}
return javaAgentJars;
}
private String getJavaAgentJarPath(String arg) {
if (arg.startsWith(JAVA_AGENT_PREFIX)) {
String path = arg.substring(JAVA_AGENT_PREFIX.length());
int equalsIndex = path.indexOf('=');
if (equalsIndex > -1) {
path = path.substring(0, equalsIndex);
}
return path;
}
return null;
}
@Override
public boolean isJavaAgentJar(URL url) {
return this.javaAgentJars.contains(url);
}
}

View File

@ -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.
@ -22,13 +22,17 @@ import org.springframework.boot.loader.archive.Archive;
/**
* {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
* included inside a {@code /lib} directory.
* included inside a {@code /BOOT-INF/lib} and that application classes are included
* inside a {@code /BOOT-INF/classes} directory.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class JarLauncher extends ExecutableArchiveLauncher {
private static final String LIB = "lib/";
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
public JarLauncher() {
}
@ -39,7 +43,10 @@ public class JarLauncher extends ExecutableArchiveLauncher {
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
return !entry.isDirectory() && entry.getName().startsWith(LIB);
if (entry.isDirectory()) {
return entry.getName().startsWith(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
@Override

View File

@ -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.
@ -23,13 +23,10 @@ import java.net.URLClassLoader;
import java.net.URLConnection;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import org.springframework.boot.loader.jar.Handler;
import org.springframework.boot.loader.jar.JarFile;
import org.springframework.lang.UsesJava7;
/**
* {@link ClassLoader} used by the {@link Launcher}.
@ -40,10 +37,6 @@ import org.springframework.lang.UsesJava7;
*/
public class LaunchedURLClassLoader extends URLClassLoader {
private static LockProvider LOCK_PROVIDER = setupLockProvider();
private final ClassLoader rootClassLoader;
/**
* Create a new {@link LaunchedURLClassLoader} instance.
* @param urls the URLs from which to load classes and resources
@ -51,57 +44,21 @@ public class LaunchedURLClassLoader extends URLClassLoader {
*/
public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
this.rootClassLoader = findRootClassLoader(parent);
}
private ClassLoader findRootClassLoader(ClassLoader classLoader) {
while (classLoader != null) {
if (classLoader.getParent() == null) {
return classLoader;
}
classLoader = classLoader.getParent();
}
return null;
}
/**
* Gets the resource with the given {@code name}. Unlike a standard
* {@link ClassLoader}, this method will first search the root class loader. If the
* resource is not found, this method will call {@link #findResource(String)}.
*/
@Override
public URL getResource(String name) {
URL url = null;
if (this.rootClassLoader != null) {
url = this.rootClassLoader.getResource(name);
}
return (url == null ? findResource(name) : url);
}
@Override
public URL findResource(String name) {
Handler.setUseFastConnectionExceptions(true);
try {
if (name.equals("") && hasURLs()) {
return getURLs()[0];
}
Handler.setUseFastConnectionExceptions(true);
try {
return super.findResource(name);
}
finally {
Handler.setUseFastConnectionExceptions(false);
}
return super.findResource(name);
}
catch (IllegalArgumentException ex) {
return null;
finally {
Handler.setUseFastConnectionExceptions(false);
}
}
@Override
public Enumeration<URL> findResources(String name) throws IOException {
if (name.equals("") && hasURLs()) {
return Collections.enumeration(Arrays.asList(getURLs()));
}
Handler.setUseFastConnectionExceptions(true);
try {
return super.findResources(name);
@ -111,106 +68,47 @@ public class LaunchedURLClassLoader extends URLClassLoader {
}
}
private boolean hasURLs() {
return getURLs().length > 0;
}
/**
* Gets the resources with the given {@code name}. Returns a combination of the
* resources found by {@link #findResources(String)} and from
* {@link ClassLoader#getResources(String) getResources(String)} on the root class
* loader, if any.
*/
@Override
public Enumeration<URL> getResources(String name) throws IOException {
if (this.rootClassLoader == null) {
return findResources(name);
}
return new ResourceEnumeration(this.rootClassLoader.getResources(name),
findResources(name));
}
/**
* Attempt to load classes from the URLs before delegating to the parent loader.
*/
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (LaunchedURLClassLoader.LOCK_PROVIDER.getLock(this, name)) {
Class<?> loadedClass = findLoadedClass(name);
if (loadedClass == null) {
Handler.setUseFastConnectionExceptions(true);
try {
loadedClass = doLoadClass(name);
}
finally {
Handler.setUseFastConnectionExceptions(false);
}
}
if (resolve) {
resolveClass(loadedClass);
}
return loadedClass;
}
}
private Class<?> doLoadClass(String name) throws ClassNotFoundException {
// 1) Try the root class loader
Handler.setUseFastConnectionExceptions(true);
try {
if (this.rootClassLoader != null) {
return this.rootClassLoader.loadClass(name);
}
definePackageIfNecessary(name);
return super.loadClass(name, resolve);
}
catch (Exception ex) {
// Ignore and continue
}
// 2) Try to find locally
try {
findPackage(name);
Class<?> cls = findClass(name);
return cls;
}
catch (Exception ex) {
// Ignore and continue
}
// 3) Use standard loading
return super.loadClass(name, false);
}
private void findPackage(final String name) throws ClassNotFoundException {
int lastDot = name.lastIndexOf('.');
if (lastDot != -1) {
String packageName = name.substring(0, lastDot);
if (getPackage(packageName) == null) {
try {
definePackageForFindClass(name, packageName);
}
catch (Exception ex) {
// Swallow and continue
}
}
finally {
Handler.setUseFastConnectionExceptions(false);
}
}
/**
* Define a package before a {@code findClass} call is made. This is necessary to
* ensure that the appropriate manifest for nested JARs associated with the package.
* @param name the class name being found
* @param packageName the package
* ensure that the appropriate manifest for nested JARs is associated with the
* package.
* @param className the class name being found
*/
private void definePackageForFindClass(final String name, final String packageName) {
private void definePackageIfNecessary(String className) {
int lastDot = className.lastIndexOf('.');
if (lastDot >= 0) {
String packageName = className.substring(0, lastDot);
if (getPackage(packageName) == null) {
definePackage(packageName);
}
}
}
private void definePackage(final String packageName) {
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws ClassNotFoundException {
String packageEntryName = packageName.replace(".", "/") + "/";
for (URL url : getURLs()) {
try {
if (url.getContent() instanceof JarFile) {
JarFile jarFile = (JarFile) url.getContent();
if (jarFile.getManifest() != null) {
if (jarFile.getEntry(packageEntryName) != null
&& jarFile.getManifest() != null) {
definePackage(packageName, jarFile.getManifest(),
url);
return null;
@ -242,6 +140,7 @@ public class LaunchedURLClassLoader extends URLClassLoader {
}
}
catch (IOException ex) {
// Ignore
}
}
@ -254,76 +153,4 @@ public class LaunchedURLClassLoader extends URLClassLoader {
}
}
@UsesJava7
private static LockProvider setupLockProvider() {
try {
ClassLoader.registerAsParallelCapable();
return new Java7LockProvider();
}
catch (NoSuchMethodError ex) {
return new LockProvider();
}
}
/**
* Strategy used to provide the synchronize lock object to use when loading classes.
*/
private static class LockProvider {
public Object getLock(LaunchedURLClassLoader classLoader, String className) {
return classLoader;
}
}
/**
* Java 7 specific {@link LockProvider}.
*/
@UsesJava7
private static class Java7LockProvider extends LockProvider {
@Override
public Object getLock(LaunchedURLClassLoader classLoader, String className) {
return classLoader.getClassLoadingLock(className);
}
}
/**
* {@link Enumeration} implementation used for {@code getResources()}.
*/
private static class ResourceEnumeration implements Enumeration<URL> {
private final Enumeration<URL> rootResources;
private final Enumeration<URL> localResources;
ResourceEnumeration(Enumeration<URL> rootResources,
Enumeration<URL> localResources) {
this.rootResources = rootResources;
this.localResources = localResources;
}
@Override
public boolean hasMoreElements() {
try {
Handler.setUseFastConnectionExceptions(true);
return this.rootResources.hasMoreElements()
|| this.localResources.hasMoreElements();
}
finally {
Handler.setUseFastConnectionExceptions(false);
}
}
@Override
public URL nextElement() {
if (this.rootResources.hasMoreElements()) {
return this.rootResources.nextElement();
}
return this.localResources.nextElement();
}
}
}

View File

@ -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.
@ -22,11 +22,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@ -60,7 +57,7 @@ import org.springframework.boot.loader.util.SystemPropertyUtils;
* <ul>
* <li>{@code loader.path}: a comma-separated list of directories to append to the
* classpath (containing file resources and/or nested archives in *.jar or *.zip).
* Defaults to {@code lib} in your application archive</li>
* Defaults to {@code BOOT-INF/classes,BOOT-INF/lib} in your application archive</li>
* <li>{@code loader.main}: the main method to delegate execution to once the class loader
* is set up. No default, but will fall back to looking for a {@code Start-Class} in a
* {@code MANIFEST.MF}, if there is one in <code>${loader.home}/META-INF</code>.</li>
@ -123,12 +120,8 @@ public class PropertiesLauncher extends Launcher {
private static final Pattern WORD_SEPARATOR = Pattern.compile("\\W+");
private static final URL[] EMPTY_URLS = {};
private final File home;
private final JavaAgentDetector javaAgentDetector;
private List<String> paths = new ArrayList<String>();
private final Properties properties = new Properties();
@ -136,13 +129,8 @@ public class PropertiesLauncher extends Launcher {
private Archive parent;
public PropertiesLauncher() {
this(new InputArgumentsJavaAgentDetector());
}
PropertiesLauncher(JavaAgentDetector javaAgentDetector) {
try {
this.home = getHomeDirectory();
this.javaAgentDetector = javaAgentDetector;
initializeProperties(this.home);
initializePaths();
this.parent = createArchive();
@ -158,7 +146,7 @@ public class PropertiesLauncher extends Launcher {
}
private void initializeProperties(File home) throws Exception, IOException {
String config = "classpath:"
String config = "classpath:BOOT-INF/classes/"
+ SystemPropertyUtils.resolvePlaceholders(
SystemPropertyUtils.getProperty(CONFIG_NAME, "application"))
+ ".properties";
@ -425,7 +413,7 @@ public class PropertiesLauncher extends Launcher {
lib.addAll(nested);
}
}
addParentClassLoaderEntries(lib);
addNestedEntries(lib);
return lib;
}
@ -482,82 +470,26 @@ public class PropertiesLauncher extends Launcher {
return new FilteredArchive(this.parent, filter);
}
private void addParentClassLoaderEntries(List<Archive> lib)
throws IOException, URISyntaxException {
ClassLoader parentClassLoader = getClass().getClassLoader();
List<Archive> urls = new ArrayList<Archive>();
for (URL url : getURLs(parentClassLoader)) {
if (!this.javaAgentDetector.isJavaAgentJar(url)) {
Archive archive = createArchiveIfPossible(url);
if (archive != null) {
urls.add(archive);
private void addNestedEntries(List<Archive> lib) {
// The parent archive might have "BOOT-INF/lib/" and "BOOT-INF/classes/"
// directories, meaning we are running from an executable JAR. We add nested
// entries from there with low priority (i.e. at end).
try {
lib.addAll(this.parent.getNestedArchives(new EntryFilter() {
@Override
public boolean matches(Entry entry) {
if (entry.isDirectory()) {
return entry.getName().startsWith(JarLauncher.BOOT_INF_CLASSES);
}
return entry.getName().startsWith(JarLauncher.BOOT_INF_LIB);
}
}
}
// The parent archive might have a "lib/" directory, meaning we are running from
// an executable JAR. We add nested entries from there with low priority (i.e. at
// end).
addNestedArchivesFromParent(urls);
for (Archive archive : urls) {
// But only add them if they are not already included
if (findArchive(lib, archive) < 0) {
lib.add(archive);
}
}
}
private Archive createArchiveIfPossible(URL url)
throws IOException, URISyntaxException {
if (url.toString().endsWith(".jar") || url.toString().endsWith(".zip")) {
return new JarFileArchive(new File(url.toURI()));
}));
}
if (url.toString().endsWith("/*")) {
String name = url.getFile();
File dir = new File(name.substring(0, name.length() - 1));
return (dir.exists() ? new ExplodedArchive(dir, false) : null);
catch (IOException ex) {
// Ignore
}
String filename = URLDecoder.decode(url.getFile(), "UTF-8");
return new ExplodedArchive(new File(filename));
}
private void addNestedArchivesFromParent(List<Archive> urls) {
int index = findArchive(urls, this.parent);
if (index >= 0) {
try {
Archive nested = getNestedArchive("lib/");
if (nested != null) {
List<Archive> extra = new ArrayList<Archive>(
nested.getNestedArchives(new ArchiveEntryFilter()));
urls.addAll(index + 1, extra);
}
}
catch (Exception ex) {
// ignore
}
}
}
private int findArchive(List<Archive> urls, Archive archive) {
// Do not rely on Archive to have an equals() method. Look for the archive by
// matching strings.
if (archive == null) {
return -1;
}
int i = 0;
for (Archive url : urls) {
if (url.toString().equals(archive.toString())) {
return i;
}
i++;
}
return -1;
}
private URL[] getURLs(ClassLoader classLoader) {
if (classLoader instanceof URLClassLoader) {
return ((URLClassLoader) classLoader).getURLs();
}
return EMPTY_URLS;
}
private String cleanupPath(String path) {

View File

@ -1,118 +0,0 @@
/*
* Copyright 2012-2014 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;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.Callable;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.loader.archive.Archive.Entry;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
/**
* Tests for {@link ExecutableArchiveLauncher}
*
* @author Andy Wilkinson
*/
public class ExecutableArchiveLauncherTests {
@Mock
private JavaAgentDetector javaAgentDetector;
private ExecutableArchiveLauncher launcher;
@Before
public void setupMocks() {
MockitoAnnotations.initMocks(this);
this.launcher = new UnitTestExecutableArchiveLauncher(this.javaAgentDetector);
}
@Test
public void createdClassLoaderContainsUrlsFromThreadContextClassLoader()
throws Exception {
final URL[] urls = new URL[] { new URL("file:one"), new URL("file:two") };
doWithTccl(new URLClassLoader(urls), new Callable<Void>() {
@Override
public Void call() throws Exception {
ClassLoader classLoader = ExecutableArchiveLauncherTests.this.launcher
.createClassLoader(new URL[0]);
assertClassLoaderUrls(classLoader, urls);
return null;
}
});
}
@Test
public void javaAgentJarsAreExcludedFromClasspath() throws Exception {
URL javaAgent = new File("my-agent.jar").getCanonicalFile().toURI().toURL();
final URL one = new URL("file:one");
given(this.javaAgentDetector.isJavaAgentJar(javaAgent)).willReturn(true);
doWithTccl(new URLClassLoader(new URL[] { javaAgent, one }),
new Callable<Void>() {
@Override
public Void call() throws Exception {
ClassLoader classLoader = ExecutableArchiveLauncherTests.this.launcher
.createClassLoader(new URL[0]);
assertClassLoaderUrls(classLoader, new URL[] { one });
return null;
}
});
}
private void assertClassLoaderUrls(ClassLoader classLoader, URL[] urls) {
assertThat(classLoader).isInstanceOf(URLClassLoader.class);
assertThat(((URLClassLoader) classLoader).getURLs()).isEqualTo(urls);
}
private void doWithTccl(ClassLoader classLoader, Callable<?> action)
throws Exception {
ClassLoader old = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(classLoader);
action.call();
}
finally {
Thread.currentThread().setContextClassLoader(old);
}
}
private static final class UnitTestExecutableArchiveLauncher
extends ExecutableArchiveLauncher {
UnitTestExecutableArchiveLauncher(JavaAgentDetector javaAgentDetector) {
super(javaAgentDetector);
}
@Override
protected boolean isNestedArchive(Entry entry) {
return false;
}
}
}

View File

@ -1,73 +0,0 @@
/*
* Copyright 2012-2014 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;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Arrays;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link InputArgumentsJavaAgentDetector}
*
* @author Andy Wilkinson
*/
public class InputArgumentsJavaAgentDetectorTests {
@Test
public void nonAgentJarsDoNotProduceFalsePositives()
throws MalformedURLException, IOException {
InputArgumentsJavaAgentDetector detector = new InputArgumentsJavaAgentDetector(
Arrays.asList("-javaagent:my-agent.jar"));
assertThat(detector.isJavaAgentJar(
new File("something-else.jar").getCanonicalFile().toURI().toURL()))
.isFalse();
}
@Test
public void singleJavaAgent() throws MalformedURLException, IOException {
InputArgumentsJavaAgentDetector detector = new InputArgumentsJavaAgentDetector(
Arrays.asList("-javaagent:my-agent.jar"));
assertThat(detector.isJavaAgentJar(
new File("my-agent.jar").getCanonicalFile().toURI().toURL())).isTrue();
}
@Test
public void singleJavaAgentWithOptions() throws MalformedURLException, IOException {
InputArgumentsJavaAgentDetector detector = new InputArgumentsJavaAgentDetector(
Arrays.asList("-javaagent:my-agent.jar=a=alpha,b=bravo"));
assertThat(detector.isJavaAgentJar(
new File("my-agent.jar").getCanonicalFile().toURI().toURL())).isTrue();
}
@Test
public void multipleJavaAgents() throws MalformedURLException, IOException {
InputArgumentsJavaAgentDetector detector = new InputArgumentsJavaAgentDetector(
Arrays.asList("-javaagent:my-agent.jar",
"-javaagent:my-other-agent.jar"));
assertThat(detector.isJavaAgentJar(
new File("my-agent.jar").getCanonicalFile().toURI().toURL())).isTrue();
assertThat(detector.isJavaAgentJar(
new File("my-other-agent.jar").getCanonicalFile().toURI().toURL()))
.isTrue();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 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.
@ -39,19 +39,6 @@ public class LaunchedURLClassLoaderTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void resolveResourceFromWindowsFilesystem() throws Exception {
// This path is invalid - it should return null even on Windows.
// A regular URLClassLoader will deal with it gracefully.
assertThat(getClass().getClassLoader()
.getResource("c:\\Users\\user\\bar.properties")).isNull();
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(
new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") },
getClass().getClassLoader());
// So we should too...
assertThat(loader.getResource("c:\\Users\\user\\bar.properties")).isNull();
}
@Test
public void resolveResourceFromArchive() throws Exception {
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(

View File

@ -22,21 +22,17 @@ import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link PropertiesLauncher}.
@ -46,9 +42,6 @@ import static org.mockito.Mockito.verify;
*/
public class PropertiesLauncherTests {
@Mock
private JavaAgentDetector javaAgentDetector;
@Rule
public OutputCapture output = new OutputCapture();
@ -195,21 +188,6 @@ public class PropertiesLauncherTests {
.isEqualTo("[foo, bar]");
}
@Test
public void testJavaAgentJarsAreExcludedFromClasspath() throws Exception {
List<Archive> allArchives = new PropertiesLauncher().getClassPathArchives();
URL[] parentUrls = ((URLClassLoader) getClass().getClassLoader()).getURLs();
for (URL url : parentUrls) {
given(this.javaAgentDetector.isJavaAgentJar(url)).willReturn(true);
}
List<Archive> nonAgentArchives = new PropertiesLauncher(this.javaAgentDetector)
.getClassPathArchives();
assertThat(nonAgentArchives).hasSize(allArchives.size() - parentUrls.length);
for (URL url : parentUrls) {
verify(this.javaAgentDetector).isJavaAgentJar(url);
}
}
private void waitFor(String value) throws Exception {
int count = 0;
boolean timeout = false;

View File

@ -1,3 +1,19 @@
/*
* 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.
*/
import java.io.*;
import org.springframework.boot.maven.*;
@ -6,8 +22,8 @@ new Verify.JarArchiveVerification(f, Verify.SAMPLE_APP) {
@Override
protected void verifyZipEntries(Verify.ArchiveVerifier verifier) throws Exception {
super.verifyZipEntries(verifier)
verifier.assertHasEntryNameStartingWith("lib/org.springframework.boot.maven.it-acme-lib-0.0.1.BUILD-SNAPSHOT.jar")
verifier.assertHasEntryNameStartingWith("lib/org.springframework.boot.maven.it.another-acme-lib-0.0.1.BUILD-SNAPSHOT.jar")
verifier.assertHasEntryNameStartingWith("BOOT-INF/lib/org.springframework.boot.maven.it-acme-lib-0.0.1.BUILD-SNAPSHOT.jar")
verifier.assertHasEntryNameStartingWith("BOOT-INF/lib/org.springframework.boot.maven.it.another-acme-lib-0.0.1.BUILD-SNAPSHOT.jar")
}
}.verify();

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 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.
@ -22,7 +22,7 @@ new Verify.JarArchiveVerification(f, Verify.SAMPLE_APP) {
@Override
protected void verifyZipEntries(Verify.ArchiveVerifier verifier) throws Exception {
super.verifyZipEntries(verifier)
verifier.assertHasUnpackEntry("lib/spring-core-")
verifier.assertHasNonUnpackEntry("lib/spring-context-")
verifier.assertHasUnpackEntry("BOOT-INF/lib/spring-core-")
verifier.assertHasNonUnpackEntry("BOOT-INF/lib/spring-context-")
}
}.verify();

View File

@ -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.
@ -218,14 +218,15 @@ public final class Verify {
@Override
protected void verifyZipEntries(ArchiveVerifier verifier) throws Exception {
super.verifyZipEntries(verifier);
verifier.assertHasEntryNameStartingWith("lib/spring-context");
verifier.assertHasEntryNameStartingWith("lib/spring-core");
verifier.assertHasEntryNameStartingWith("lib/javax.servlet-api-3");
verifier.assertHasEntryNameStartingWith("BOOT-INF/lib/spring-context");
verifier.assertHasEntryNameStartingWith("BOOT-INF/lib/spring-core");
verifier.assertHasEntryNameStartingWith("BOOT-INF/lib/javax.servlet-api-3");
assertThat(verifier
.hasEntry("org/" + "springframework/boot/loader/JarLauncher.class"))
.hasEntry("org/springframework/boot/loader/JarLauncher.class"))
.as("Unpacked launcher classes").isTrue();
assertThat(verifier.hasEntry("org/" + "test/SampleApplication.class"))
.as("Own classes").isTrue();
assertThat(verifier
.hasEntry("BOOT-INF/classes/org/test/SampleApplication.class"))
.as("Own classes").isTrue();
}
@Override