From 5e0ba6ea2e235da76798a9b6dd55833b7951e66e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 Apr 2016 17:39:40 +0100 Subject: [PATCH] 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 --- .../boot/devtools/restart/ChangeableUrls.java | 66 ++++++++++++++++++- .../devtools/restart/ChangeableUrlsTests.java | 32 ++++++++- 2 files changed, 94 insertions(+), 4 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 da8434b9629..9356c4cb4e9 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 @@ -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 { @@ -74,7 +82,59 @@ final class ChangeableUrls implements Iterable { } public static ChangeableUrls fromUrlClassLoader(URLClassLoader classLoader) { - return fromUrls(classLoader.getURLs()); + List urls = new ArrayList(); + for (URL url : classLoader.getURLs()) { + urls.add(url); + urls.addAll(getUrlsFromClassPathOfJarManifestIfPossible(url)); + } + return fromUrls(urls); + } + + private static List 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.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 getUrlsFromClassPathAttribute(Manifest manifest) { + List urls = new ArrayList(); + 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 urls) { 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 efcde694460..723e5588665 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 @@ -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(); + } + }