From 270530c4fd85067cb5d4926b1c43ec48c352059d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 11 Aug 2016 16:01:18 +0100 Subject: [PATCH] Fix class loading problems when CLI extensions are installed Previously, CLI extensions where installed into the CLI's lib directory which meant that they were on the class path of the app class loader. Following the change to an executable jar's packaging, this meant that they could not see classes in the CLI and a ClassNotFoundException would result. This commit updates the CLI to install extensions into lib/ext and load commands using a new ClassLoader that has all of the jars in lib/ext on its class path and that uses the launch class loader as its parent. Closes gh-6615 --- .../springframework/boot/cli/SpringCli.java | 35 +++++++++++++++++-- .../cli/command/install/InstallCommand.java | 4 +-- .../boot/cli/command/install/Installer.java | 29 +++++++++------ .../cli/command/install/UninstallCommand.java | 6 ++-- .../cli/command/install/InstallerTests.java | 20 +++++------ 5 files changed, 66 insertions(+), 28 deletions(-) diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java index dbbb7fb758..81c5c198bb 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 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,12 @@ package org.springframework.boot.cli; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; import java.util.ServiceLoader; import org.springframework.boot.cli.command.CommandFactory; @@ -25,6 +31,7 @@ import org.springframework.boot.cli.command.core.HintCommand; import org.springframework.boot.cli.command.core.VersionCommand; import org.springframework.boot.cli.command.shell.ShellCommand; import org.springframework.boot.loader.tools.LogbackInitializer; +import org.springframework.util.SystemPropertyUtils; /** * Spring Command Line Interface. This is the main entry-point for the Spring command line @@ -60,10 +67,34 @@ public final class SpringCli { private static void addServiceLoaderCommands(CommandRunner runner) { ServiceLoader factories = ServiceLoader.load(CommandFactory.class, - runner.getClass().getClassLoader()); + createCommandClassLoader(runner)); for (CommandFactory factory : factories) { runner.addCommands(factory.getCommands()); } } + private static URLClassLoader createCommandClassLoader(CommandRunner runner) { + return new URLClassLoader(getExtensionURLs(), runner.getClass().getClassLoader()); + } + + private static URL[] getExtensionURLs() { + List urls = new ArrayList(); + String home = SystemPropertyUtils + .resolvePlaceholders("${spring.home:${SPRING_HOME:.}}"); + File extDirectory = new File(new File(home, "lib"), "ext"); + if (extDirectory.isDirectory()) { + for (File file : extDirectory.listFiles()) { + if (file.getName().endsWith(".jar")) { + try { + urls.add(file.toURI().toURL()); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + } + } + return urls.toArray(new URL[urls.size()]); + } + } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/install/InstallCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/install/InstallCommand.java index 55b65a200b..0092214140 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/install/InstallCommand.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/install/InstallCommand.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-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. @@ -37,7 +37,7 @@ import org.springframework.util.Assert; public class InstallCommand extends OptionParsingCommand { public InstallCommand() { - super("install", "Install dependencies to the lib directory", + super("install", "Install dependencies to the lib/ext directory", new InstallOptionHandler()); } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/install/Installer.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/install/Installer.java index 290aad7822..e6021a3059 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/install/Installer.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/install/Installer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 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. @@ -92,7 +92,7 @@ class Installer { } public void install(List artifactIdentifiers) throws Exception { - File libDirectory = getDefaultLibDirectory(); + File libDirectory = getDefaultExtDirectory(); libDirectory.mkdirs(); Log.info("Installing into: " + libDirectory); List artifactFiles = this.dependencyResolver.resolve(artifactIdentifiers); @@ -125,13 +125,13 @@ class Installer { } public void uninstall(List artifactIdentifiers) throws Exception { - File libDirectory = getDefaultLibDirectory(); - Log.info("Uninstalling from: " + libDirectory); + File extDirectory = getDefaultExtDirectory(); + Log.info("Uninstalling from: " + extDirectory); List artifactFiles = this.dependencyResolver.resolve(artifactIdentifiers); for (File artifactFile : artifactFiles) { int installCount = getInstallCount(artifactFile); if (installCount <= 1) { - new File(libDirectory, artifactFile.getName()).delete(); + new File(extDirectory, artifactFile.getName()).delete(); } setInstallCount(artifactFile, installCount - 1); } @@ -139,23 +139,30 @@ class Installer { } public void uninstallAll() throws Exception { - File libDirectory = getDefaultLibDirectory(); - Log.info("Uninstalling from: " + libDirectory); + File extDirectory = getDefaultExtDirectory(); + Log.info("Uninstalling from: " + extDirectory); for (String name : this.installCounts.stringPropertyNames()) { - new File(libDirectory, name).delete(); + new File(extDirectory, name).delete(); } this.installCounts.clear(); saveInstallCounts(); } - private File getDefaultLibDirectory() { + private File getDefaultExtDirectory() { String home = SystemPropertyUtils .resolvePlaceholders("${spring.home:${SPRING_HOME:.}}"); - return new File(home, "lib"); + File extDirectory = new File(new File(home, "lib"), "ext"); + if (!extDirectory.isDirectory()) { + if (!extDirectory.mkdirs()) { + throw new IllegalStateException( + "Failed to create ext directory " + extDirectory); + } + } + return extDirectory; } private File getInstalled() { - return new File(getDefaultLibDirectory(), ".installed"); + return new File(getDefaultExtDirectory(), ".installed"); } } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/install/UninstallCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/install/UninstallCommand.java index 92d2338bf0..f7ccd54f9b 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/install/UninstallCommand.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/install/UninstallCommand.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 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. @@ -28,7 +28,7 @@ import org.springframework.boot.cli.command.status.ExitStatus; import org.springframework.boot.cli.util.Log; /** - * {@link Command} to uninstall dependencies from the CLI's lib directory. + * {@link Command} to uninstall dependencies from the CLI's lib/ext directory. * * @author Dave Syer * @author Andy Wilkinson @@ -37,7 +37,7 @@ import org.springframework.boot.cli.util.Log; public class UninstallCommand extends OptionParsingCommand { public UninstallCommand() { - super("uninstall", "Uninstall dependencies from the lib directory", + super("uninstall", "Uninstall dependencies from the lib/ext directory", new UninstallOptionHandler()); } diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/install/InstallerTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/install/InstallerTests.java index 3b41f12797..2003c10658 100644 --- a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/install/InstallerTests.java +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/install/InstallerTests.java @@ -66,7 +66,7 @@ public class InstallerTests { File foo = createTemporaryFile("foo.jar"); given(this.resolver.resolve(Arrays.asList("foo"))).willReturn(Arrays.asList(foo)); this.installer.install(Arrays.asList("foo")); - assertThat(getNamesOfFilesInLib()).containsOnly("foo.jar", ".installed"); + assertThat(getNamesOfFilesInLibExt()).containsOnly("foo.jar", ".installed"); } @Test @@ -75,7 +75,7 @@ public class InstallerTests { given(this.resolver.resolve(Arrays.asList("foo"))).willReturn(Arrays.asList(foo)); this.installer.install(Arrays.asList("foo")); this.installer.uninstall(Arrays.asList("foo")); - assertThat(getNamesOfFilesInLib()).contains(".installed"); + assertThat(getNamesOfFilesInLibExt()).contains(".installed"); } @Test @@ -88,16 +88,16 @@ public class InstallerTests { given(this.resolver.resolve(Arrays.asList("charlie"))) .willReturn(Arrays.asList(charlie, alpha)); this.installer.install(Arrays.asList("bravo")); - assertThat(getNamesOfFilesInLib()).containsOnly("alpha.jar", "bravo.jar", + assertThat(getNamesOfFilesInLibExt()).containsOnly("alpha.jar", "bravo.jar", ".installed"); this.installer.install(Arrays.asList("charlie")); - assertThat(getNamesOfFilesInLib()).containsOnly("alpha.jar", "bravo.jar", + assertThat(getNamesOfFilesInLibExt()).containsOnly("alpha.jar", "bravo.jar", "charlie.jar", ".installed"); this.installer.uninstall(Arrays.asList("bravo")); - assertThat(getNamesOfFilesInLib()).containsOnly("alpha.jar", "charlie.jar", + assertThat(getNamesOfFilesInLibExt()).containsOnly("alpha.jar", "charlie.jar", ".installed"); this.installer.uninstall(Arrays.asList("charlie")); - assertThat(getNamesOfFilesInLib()).containsOnly(".installed"); + assertThat(getNamesOfFilesInLibExt()).containsOnly(".installed"); } @Test @@ -111,15 +111,15 @@ public class InstallerTests { .willReturn(Arrays.asList(charlie, alpha)); this.installer.install(Arrays.asList("bravo")); this.installer.install(Arrays.asList("charlie")); - assertThat(getNamesOfFilesInLib()).containsOnly("alpha.jar", "bravo.jar", + assertThat(getNamesOfFilesInLibExt()).containsOnly("alpha.jar", "bravo.jar", "charlie.jar", ".installed"); this.installer.uninstallAll(); - assertThat(getNamesOfFilesInLib()).containsOnly(".installed"); + assertThat(getNamesOfFilesInLibExt()).containsOnly(".installed"); } - private Set getNamesOfFilesInLib() { + private Set getNamesOfFilesInLibExt() { Set names = new HashSet(); - for (File file : new File("target/lib").listFiles()) { + for (File file : new File("target/lib/ext").listFiles()) { names.add(file.getName()); } return names;