Consider jar's Class-Path attribute when getting changeable URLs

To overcome command length limits on Windows, IntelliJ IDEA may launch
an application with a single jar on the classpath that contains that
application's actual classpath in the Class-Path attribute of its
manifest. This would prevent DevTools restarts from working as it
only considered the single jar's URL when identifying changeable URLs
and ignored the URLs added to the classpath via the jar's manifest.

This commit updates ChangeableUrls when it is created from a
URLClassLoader to consider the Class-Path manifest attribute of any
jars in the class loader's URLs. This allows the full classpath to
be considered when identifying URLs that are changeable and that
need to be monitored for restart triggering.

Closes gh-5127
pull/5663/merge
Andy Wilkinson 9 years ago
parent 7f3d4adfef
commit 5e0ba6ea2e

@ -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,6 +16,9 @@
package org.springframework.boot.devtools.restart;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
@ -23,13 +26,18 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.springframework.boot.devtools.settings.DevToolsSettings;
import org.springframework.util.StringUtils;
/**
* A filtered collections of URLs which can be change after the application has started.
* A filtered collection of URLs which can change after the application has started.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
final class ChangeableUrls implements Iterable<URL> {
@ -74,7 +82,59 @@ final class ChangeableUrls implements Iterable<URL> {
}
public static ChangeableUrls fromUrlClassLoader(URLClassLoader classLoader) {
return fromUrls(classLoader.getURLs());
List<URL> urls = new ArrayList<URL>();
for (URL url : classLoader.getURLs()) {
urls.add(url);
urls.addAll(getUrlsFromClassPathOfJarManifestIfPossible(url));
}
return fromUrls(urls);
}
private static List<URL> getUrlsFromClassPathOfJarManifestIfPossible(URL url) {
JarFile jarFile = getJarFileIfPossible(url);
if (jarFile != null) {
try {
return getUrlsFromClassPathAttribute(jarFile.getManifest());
}
catch (IOException ex) {
throw new IllegalStateException(
"Failed to read Class-Path attribute from manifest of jar "
+ url);
}
}
return Collections.<URL>emptyList();
}
private static JarFile getJarFileIfPossible(URL url) {
try {
File file = new File(url.toURI());
if (file.isFile()) {
return new JarFile(file);
}
}
catch (Exception ex) {
// Assume it's not a jar and continue
}
return null;
}
private static List<URL> getUrlsFromClassPathAttribute(Manifest manifest) {
List<URL> urls = new ArrayList<URL>();
String classPathAttribute = manifest.getMainAttributes()
.getValue(Attributes.Name.CLASS_PATH);
if (StringUtils.hasText(classPathAttribute)) {
for (String entry : StringUtils.delimitedListToStringArray(classPathAttribute,
" ")) {
try {
urls.add(new URL(entry));
}
catch (MalformedURLException ex) {
throw new IllegalStateException(
"Class-Path attribute contains malformed URL", ex);
}
}
}
return urls;
}
public static ChangeableUrls fromUrls(Collection<URL> urls) {

@ -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.
@ -17,13 +17,21 @@
package org.springframework.boot.devtools.restart;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.util.StringUtils;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
@ -31,6 +39,7 @@ import static org.junit.Assert.assertThat;
* Tests for {@link ChangeableUrls}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class ChangeableUrlsTests {
@ -64,6 +73,16 @@ public class ChangeableUrlsTests {
assertThat(urls.size(), equalTo(0));
}
@Test
public void urlsFromJarClassPathAreConsidered() throws Exception {
URL projectCore = makeUrl("project-core");
URL projectWeb = makeUrl("project-web");
ChangeableUrls urls = ChangeableUrls.fromUrlClassLoader(new URLClassLoader(
new URL[] { makeJarFileWithUrlsInManifestClassPath(projectCore,
projectWeb) }));
assertThat(urls.toList(), contains(projectCore, projectWeb));
}
private URL makeUrl(String name) throws IOException {
File file = this.temporaryFolder.newFolder();
file = new File(file, name);
@ -73,4 +92,15 @@ public class ChangeableUrlsTests {
return file.toURI().toURL();
}
private URL makeJarFileWithUrlsInManifestClassPath(URL... urls) throws Exception {
File classpathJar = this.temporaryFolder.newFile("classpath.jar");
Manifest manifest = new Manifest();
manifest.getMainAttributes().putValue(Attributes.Name.MANIFEST_VERSION.toString(),
"1.0");
manifest.getMainAttributes().putValue(Attributes.Name.CLASS_PATH.toString(),
StringUtils.arrayToDelimitedString(urls, " "));
new JarOutputStream(new FileOutputStream(classpathJar), manifest).close();
return classpathJar.toURI().toURL();
}
}

Loading…
Cancel
Save