From ce77f48c3f9c8acdd12d3a629b9c8a393050c271 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 29 Sep 2017 12:07:18 +0100 Subject: [PATCH] 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 --- .../boot/devtools/restart/ChangeableUrls.java | 26 +++++++++++++++++-- .../restart/DefaultRestartInitializer.java | 10 +++---- .../devtools/restart/ChangeableUrlsTests.java | 2 +- .../DefaultRestartInitializerTests.java | 8 +++++- .../spring-boot-devtools-tests/pom.xml | 19 -------------- 5 files changed, 37 insertions(+), 28 deletions(-) diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/ChangeableUrls.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/ChangeableUrls.java index bcb786e82f..2a10f38b3c 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/ChangeableUrls.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/ChangeableUrls.java @@ -18,6 +18,7 @@ package org.springframework.boot.devtools.restart; import java.io.File; import java.io.IOException; +import java.lang.management.ManagementFactory; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; @@ -29,6 +30,7 @@ import java.util.List; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; +import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -89,15 +91,35 @@ final class ChangeableUrls implements Iterable { return this.urls.toString(); } - public static ChangeableUrls fromUrlClassLoader(URLClassLoader classLoader) { + public static ChangeableUrls fromClassLoader(ClassLoader classLoader) { List urls = new ArrayList<>(); - for (URL url : classLoader.getURLs()) { + for (URL url : urlsFromClassLoader(classLoader)) { urls.add(url); urls.addAll(getUrlsFromClassPathOfJarManifestIfPossible(url)); } 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 getUrlsFromClassPathOfJarManifestIfPossible(URL url) { JarFile jarFile = getJarFileIfPossible(url); if (jarFile == null) { diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/DefaultRestartInitializer.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/DefaultRestartInitializer.java index 55b36297cb..61e3268dd3 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/DefaultRestartInitializer.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/DefaultRestartInitializer.java @@ -17,10 +17,10 @@ package org.springframework.boot.devtools.restart; import java.net.URL; -import java.net.URLClassLoader; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; +import java.util.stream.Stream; /** * Default {@link RestartInitializer} that only enable initial restart when running a @@ -53,7 +53,9 @@ public class DefaultRestartInitializer implements RestartInitializer { 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 */ protected URL[] getUrls(Thread thread) { - return ChangeableUrls - .fromUrlClassLoader((URLClassLoader) thread.getContextClassLoader()) - .toArray(); + return ChangeableUrls.fromClassLoader(thread.getContextClassLoader()).toArray(); } } diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/ChangeableUrlsTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/ChangeableUrlsTests.java index 19eca4ebc1..8fe0dce38e 100644 --- a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/ChangeableUrlsTests.java +++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/ChangeableUrlsTests.java @@ -85,7 +85,7 @@ public class ChangeableUrlsTests { .mkdirs(); new File(jarWithClassPath.getParentFile(), "project-web/target/classes").mkdirs(); ChangeableUrls urls = ChangeableUrls - .fromUrlClassLoader(new URLClassLoader(new URL[] { + .fromClassLoader(new URLClassLoader(new URL[] { jarWithClassPath.toURI().toURL(), makeJarFileWithNoManifest() })); assertThat(urls.toList()).containsExactly( new URL(jarWithClassPath.toURI().toURL(), "project-core/target/classes/"), diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/DefaultRestartInitializerTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/DefaultRestartInitializerTests.java index 526b4f60a6..58635a2958 100644 --- a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/DefaultRestartInitializerTests.java +++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/DefaultRestartInitializerTests.java @@ -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"); * 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); } + @Test + public void urlsCanBeRetrieved() { + assertThat(new DefaultRestartInitializer().getUrls(Thread.currentThread())) + .isNotEmpty(); + } + private void testSkipStack(String className, boolean expected) { MockRestartInitializer initializer = new MockRestartInitializer(true); StackTraceElement element = new StackTraceElement(className, "someMethod", diff --git a/spring-boot-integration-tests/spring-boot-devtools-tests/pom.xml b/spring-boot-integration-tests/spring-boot-devtools-tests/pom.xml index b60b1d17be..c536b90a3b 100644 --- a/spring-boot-integration-tests/spring-boot-devtools-tests/pom.xml +++ b/spring-boot-integration-tests/spring-boot-devtools-tests/pom.xml @@ -87,23 +87,4 @@ - - - java9 - - 9 - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - -