Skip to content

Tweak PropertiesLauncher to locate classes as well as nested jars #8486

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ the `Main-Class` attribute and leave out `Start-Class`.
* `loader.path` can contain directories (scanned recursively for jar and zip files),
archive paths, a directory within an archive that is scanned for jar files (for
example, `dependencies.jar!/lib`), or wildcard patterns (for the default JVM behavior).
Archive paths can be relative to `loader.home`, or anywhere in the file system with a `jar:file:` prefix.
* `loader.path` (if empty) defaults to `BOOT-INF/lib` (meaning a local directory or a
nested one if running from an archive). Because of this `PropertiesLauncher` behaves the
same as `JarLauncher` when no additional configuration is provided.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -299,11 +301,11 @@ private List<String> parsePathsProperty(String commaSeparatedPaths) {
List<String> paths = new ArrayList<String>();
for (String path : commaSeparatedPaths.split(",")) {
path = cleanupPath(path);
// Empty path (i.e. the archive itself if running from a JAR) is always added
// to the classpath so no need for it to be explicitly listed
if (!path.equals("")) {
paths.add(path);
if ("".equals(path)) {
// This means: user wants root of archive but not current directory
path = "/";
}
paths.add(path);
}
if (paths.isEmpty()) {
paths.add("lib");
Expand Down Expand Up @@ -336,7 +338,13 @@ protected String getMainClass() throws Exception {

@Override
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
ClassLoader loader = super.createClassLoader(archives);
Set<URL> urls = new LinkedHashSet<URL>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
ClassLoader loader = new LaunchedURLClassLoader(urls.toArray(new URL[0]),
getClass().getClassLoader());
debug("Classpath: " + urls);
String customLoaderClassName = getProperty("loader.classLoader");
if (customLoaderClassName != null) {
loader = wrapWithCustomClassLoader(loader, customLoaderClassName);
Expand Down Expand Up @@ -454,13 +462,15 @@ private List<Archive> getClassPathArchives(String path) throws Exception {
String root = cleanupPath(stripFileUrlPrefix(path));
List<Archive> lib = new ArrayList<Archive>();
File file = new File(root);
if (!isAbsolutePath(root)) {
file = new File(this.home, root);
}
if (file.isDirectory()) {
debug("Adding classpath entries from " + file);
Archive archive = new ExplodedArchive(file, false);
lib.add(archive);
if (!"/".equals(root)) {
if (!isAbsolutePath(root)) {
file = new File(this.home, root);
}
if (file.isDirectory()) {
debug("Adding classpath entries from " + file);
Archive archive = new ExplodedArchive(file, false);
lib.add(archive);
}
}
Archive archive = getArchive(file);
if (archive != null) {
Expand Down Expand Up @@ -488,24 +498,46 @@ private Archive getArchive(File file) throws IOException {
return null;
}

private List<Archive> getNestedArchives(String root) throws Exception {
if (root.startsWith("/")
private List<Archive> getNestedArchives(String path) throws Exception {
String root = path;
if (!root.equals("/") && root.startsWith("/")
|| this.parent.getUrl().equals(this.home.toURI().toURL())) {
// If home dir is same as parent archive, no need to add it twice.
return null;
}
Archive parent = this.parent;
if (root.startsWith("jar:file:") && root.contains("!")) {
if (root.contains("!")) {
int index = root.indexOf("!");
String file = root.substring("jar:file:".length(), index);
parent = new JarFileArchive(new File(file));
File file = new File(this.home, root.substring(0, index));
if (root.startsWith("jar:file:")) {
file = new File(root.substring("jar:file:".length(), index));
}
parent = new JarFileArchive(file);
root = root.substring(index + 1, root.length());
while (root.startsWith("/")) {
root = root.substring(1);
}
}
if (root.endsWith(".jar")) {
File file = new File(this.home, root);
if (file.exists()) {
parent = new JarFileArchive(file);
root = "";
}
}
if (root.equals("/") || root.equals("./") || root.equals(".")) {
// The prefix for nested jars is actually empty if it's at the root
root = "";
}
EntryFilter filter = new PrefixMatchingArchiveFilter(root);
return parent.getNestedArchives(filter);
List<Archive> archives = new ArrayList<Archive>(parent.getNestedArchives(filter));
if (("".equals(root) || ".".equals(root)) && !path.endsWith(".jar")
&& parent != this.parent) {
// You can't find the root with an entry filter so it has to be added
// explicitly. But don't add the root of the parent archive.
archives.add(parent);
}
return archives;
}

private void addNestedEntries(List<Archive> lib) {
Expand All @@ -518,7 +550,7 @@ private void addNestedEntries(List<Archive> lib) {
@Override
public boolean matches(Entry entry) {
if (entry.isDirectory()) {
return entry.getName().startsWith(JarLauncher.BOOT_INF_CLASSES);
return entry.getName().equals(JarLauncher.BOOT_INF_CLASSES);
}
return entry.getName().startsWith(JarLauncher.BOOT_INF_LIB);
}
Expand Down Expand Up @@ -607,6 +639,9 @@ private PrefixMatchingArchiveFilter(String prefix) {

@Override
public boolean matches(Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(this.prefix);
}
return entry.getName().startsWith(this.prefix) && this.filter.matches(entry);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import org.assertj.core.api.Condition;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
Expand All @@ -36,6 +37,9 @@
import org.mockito.MockitoAnnotations;

import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.ExplodedArchive;
import org.springframework.boot.loader.archive.JarFileArchive;
import org.springframework.core.io.FileSystemResource;
import org.springframework.test.util.ReflectionTestUtils;

import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -72,6 +76,7 @@ public void close() {
System.clearProperty("loader.config.name");
System.clearProperty("loader.config.location");
System.clearProperty("loader.system");
System.clearProperty("loader.classLoader");
}

@Test
Expand Down Expand Up @@ -131,6 +136,21 @@ public void testUserSpecifiedDotPath() throws Exception {
.isEqualTo("[.]");
}

@Test
public void testUserSpecifiedSlashPath() throws Exception {
System.setProperty("loader.path", "jars/");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(ReflectionTestUtils.getField(launcher, "paths").toString())
.isEqualTo("[jars/]");
List<Archive> archives = launcher.getClassPathArchives();
assertThat(archives).areExactly(1, new Condition<Archive>() {
@Override
public boolean matches(Archive value) {
return value.toString().endsWith("app.jar!/");
}
});
}

@Test
public void testUserSpecifiedWildcardPath() throws Exception {
System.setProperty("loader.path", "jars/*");
Expand All @@ -153,13 +173,79 @@ public void testUserSpecifiedJarPath() throws Exception {
waitFor("Hello World");
}

@Test
public void testUserSpecifiedRootOfJarPath() throws Exception {
System.setProperty("loader.path",
"jar:file:./src/test/resources/nested-jars/app.jar!/");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(ReflectionTestUtils.getField(launcher, "paths").toString())
.isEqualTo("[jar:file:./src/test/resources/nested-jars/app.jar!/]");
List<Archive> archives = launcher.getClassPathArchives();
assertThat(archives).areExactly(1, new Condition<Archive>() {
@Override
public boolean matches(Archive value) {
return value.toString().endsWith("foo.jar!/");
}
});
assertThat(archives).areExactly(1, new Condition<Archive>() {
@Override
public boolean matches(Archive value) {
return value.toString().endsWith("app.jar!/");
}
});
}

@Test
public void testUserSpecifiedRootOfJarPathWithDot() throws Exception {
System.setProperty("loader.path", "nested-jars/app.jar!/./");
PropertiesLauncher launcher = new PropertiesLauncher();
List<Archive> archives = launcher.getClassPathArchives();
assertThat(archives).areExactly(1, new Condition<Archive>() {
@Override
public boolean matches(Archive value) {
return value.toString().endsWith("foo.jar!/");
}
});
assertThat(archives).areExactly(1, new Condition<Archive>() {
@Override
public boolean matches(Archive value) {
return value.toString().endsWith("app.jar!/");
}
});
}

@Test
public void testUserSpecifiedRootOfJarPathWithDotAndJarPrefix() throws Exception {
System.setProperty("loader.path",
"jar:file:./src/test/resources/nested-jars/app.jar!/./");
PropertiesLauncher launcher = new PropertiesLauncher();
List<Archive> archives = launcher.getClassPathArchives();
assertThat(archives).areExactly(1, new Condition<Archive>() {
@Override
public boolean matches(Archive value) {
return value.toString().endsWith("foo.jar!/");
}
});
}

@Test
public void testUserSpecifiedJarFileWithNestedArchives() throws Exception {
System.setProperty("loader.path", "nested-jars/app.jar");
System.setProperty("loader.main", "demo.Application");
PropertiesLauncher launcher = new PropertiesLauncher();
launcher.launch(new String[0]);
waitFor("Hello World");
List<Archive> archives = launcher.getClassPathArchives();
assertThat(archives).areExactly(1, new Condition<Archive>() {
@Override
public boolean matches(Archive value) {
return value.toString().endsWith("foo.jar!/");
}
});
assertThat(archives).areExactly(1, new Condition<Archive>() {
@Override
public boolean matches(Archive value) {
return value.toString().endsWith("app.jar!/");
}
});
}

@Test
Expand Down Expand Up @@ -209,11 +295,25 @@ public void testUserSpecifiedClassPathOrder() throws Exception {
public void testCustomClassLoaderCreation() throws Exception {
System.setProperty("loader.classLoader", TestLoader.class.getName());
PropertiesLauncher launcher = new PropertiesLauncher();
ClassLoader loader = launcher.createClassLoader(Collections.<Archive>emptyList());
ClassLoader loader = launcher.createClassLoader(archives());
assertThat(loader).isNotNull();
assertThat(loader.getClass().getName()).isEqualTo(TestLoader.class.getName());
}

private List<Archive> archives() throws Exception {
List<Archive> list = new ArrayList<Archive>();
String path = System.getProperty("java.class.path");
for (String url : path.split(File.pathSeparator)) {
if (url.endsWith(".jar")) {
list.add(new JarFileArchive(new FileSystemResource(url).getFile()));
}
else {
list.add(new ExplodedArchive(new FileSystemResource(url).getFile()));
}
}
return list;
}

@Test
public void testUserSpecifiedConfigPathWins() throws Exception {

Expand Down
Binary file not shown.