Fall back to JVM's class path when TCCL is not a URLClassLoader

Previously, DevTools assumed that the TCCL was a URLClassLoader
when trying to determine the URLs that it should examine to determine
the locations that should be watched for triggering a restart. This
fails on Java 9 as the TCCL is not a URLClassLoader.

This commit updates the logic that determines the changeable URLs to
fall back to examining the JVM's class path when the TCCL is not a
URLClassLoader, typically because the application is running on Java
9. This fall back isn't a direct equivalent of the behaviour on Java 8
as the class path of the TCCL and the class path with which the JVM
was launched may not be the same. However, I consider the fix to be
reasonable for two reasons:

1. In reality, the class path of the TCCL and the class path with
   which the JVM was launched are the same.
2. There appears to be now to get the URLs on the class path of the
   TCCL on Java 9. There is a URLClassPath field, however Java 9's
   access restrictions prevent us from using it even if we resort to
   reflection.

Closes gh-10454
pull/10447/merge
Andy Wilkinson 7 years ago
parent b152b98f84
commit ce77f48c3f

@ -18,6 +18,7 @@ package org.springframework.boot.devtools.restart;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader; import java.net.URLClassLoader;
@ -29,6 +30,7 @@ import java.util.List;
import java.util.jar.Attributes; import java.util.jar.Attributes;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import java.util.stream.Stream;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
@ -89,15 +91,35 @@ final class ChangeableUrls implements Iterable<URL> {
return this.urls.toString(); return this.urls.toString();
} }
public static ChangeableUrls fromUrlClassLoader(URLClassLoader classLoader) { public static ChangeableUrls fromClassLoader(ClassLoader classLoader) {
List<URL> urls = new ArrayList<>(); List<URL> urls = new ArrayList<>();
for (URL url : classLoader.getURLs()) { for (URL url : urlsFromClassLoader(classLoader)) {
urls.add(url); urls.add(url);
urls.addAll(getUrlsFromClassPathOfJarManifestIfPossible(url)); urls.addAll(getUrlsFromClassPathOfJarManifestIfPossible(url));
} }
return fromUrls(urls); return fromUrls(urls);
} }
private static URL[] urlsFromClassLoader(ClassLoader classLoader) {
if (classLoader instanceof URLClassLoader) {
return ((URLClassLoader) classLoader).getURLs();
}
return Stream
.of(ManagementFactory.getRuntimeMXBean().getClassPath()
.split(File.pathSeparator))
.map(ChangeableUrls::toURL).toArray(URL[]::new);
}
private static URL toURL(String classPathEntry) {
try {
return new File(classPathEntry).toURI().toURL();
}
catch (MalformedURLException ex) {
throw new IllegalArgumentException(
"URL could not be created from '" + classPathEntry + "'", ex);
}
}
private static List<URL> getUrlsFromClassPathOfJarManifestIfPossible(URL url) { private static List<URL> getUrlsFromClassPathOfJarManifestIfPossible(URL url) {
JarFile jarFile = getJarFileIfPossible(url); JarFile jarFile = getJarFileIfPossible(url);
if (jarFile == null) { if (jarFile == null) {

@ -17,10 +17,10 @@
package org.springframework.boot.devtools.restart; package org.springframework.boot.devtools.restart;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
import java.util.stream.Stream;
/** /**
* Default {@link RestartInitializer} that only enable initial restart when running a * Default {@link RestartInitializer} that only enable initial restart when running a
@ -53,7 +53,9 @@ public class DefaultRestartInitializer implements RestartInitializer {
return null; return null;
} }
} }
return getUrls(thread); URL[] urls = getUrls(thread);
Stream.of(urls).forEach(System.out::println);
return urls;
} }
/** /**
@ -89,9 +91,7 @@ public class DefaultRestartInitializer implements RestartInitializer {
* @return the URLs * @return the URLs
*/ */
protected URL[] getUrls(Thread thread) { protected URL[] getUrls(Thread thread) {
return ChangeableUrls return ChangeableUrls.fromClassLoader(thread.getContextClassLoader()).toArray();
.fromUrlClassLoader((URLClassLoader) thread.getContextClassLoader())
.toArray();
} }
} }

@ -85,7 +85,7 @@ public class ChangeableUrlsTests {
.mkdirs(); .mkdirs();
new File(jarWithClassPath.getParentFile(), "project-web/target/classes").mkdirs(); new File(jarWithClassPath.getParentFile(), "project-web/target/classes").mkdirs();
ChangeableUrls urls = ChangeableUrls ChangeableUrls urls = ChangeableUrls
.fromUrlClassLoader(new URLClassLoader(new URL[] { .fromClassLoader(new URLClassLoader(new URL[] {
jarWithClassPath.toURI().toURL(), makeJarFileWithNoManifest() })); jarWithClassPath.toURI().toURL(), makeJarFileWithNoManifest() }));
assertThat(urls.toList()).containsExactly( assertThat(urls.toList()).containsExactly(
new URL(jarWithClassPath.toURI().toURL(), "project-core/target/classes/"), new URL(jarWithClassPath.toURI().toURL(), "project-core/target/classes/"),

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2016 the original author or authors. * Copyright 2012-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -86,6 +86,12 @@ public class DefaultRestartInitializerTests {
testSkipStack("cucumber.runtime.Runtime.run", true); testSkipStack("cucumber.runtime.Runtime.run", true);
} }
@Test
public void urlsCanBeRetrieved() {
assertThat(new DefaultRestartInitializer().getUrls(Thread.currentThread()))
.isNotEmpty();
}
private void testSkipStack(String className, boolean expected) { private void testSkipStack(String className, boolean expected) {
MockRestartInitializer initializer = new MockRestartInitializer(true); MockRestartInitializer initializer = new MockRestartInitializer(true);
StackTraceElement element = new StackTraceElement(className, "someMethod", StackTraceElement element = new StackTraceElement(className, "someMethod",

@ -87,23 +87,4 @@
</plugins> </plugins>
</pluginManagement> </pluginManagement>
</build> </build>
<profiles>
<profile>
<id>java9</id>
<activation>
<jdk>9</jdk>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project> </project>

Loading…
Cancel
Save