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
This commit is contained in:
Andy Wilkinson 2016-08-11 16:01:18 +01:00
parent b2420be886
commit 270530c4fd
5 changed files with 66 additions and 28 deletions

View File

@ -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<CommandFactory> 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<URL> urls = new ArrayList<URL>();
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()]);
}
}

View File

@ -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());
}

View File

@ -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<String> artifactIdentifiers) throws Exception {
File libDirectory = getDefaultLibDirectory();
File libDirectory = getDefaultExtDirectory();
libDirectory.mkdirs();
Log.info("Installing into: " + libDirectory);
List<File> artifactFiles = this.dependencyResolver.resolve(artifactIdentifiers);
@ -125,13 +125,13 @@ class Installer {
}
public void uninstall(List<String> artifactIdentifiers) throws Exception {
File libDirectory = getDefaultLibDirectory();
Log.info("Uninstalling from: " + libDirectory);
File extDirectory = getDefaultExtDirectory();
Log.info("Uninstalling from: " + extDirectory);
List<File> 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");
}
}

View File

@ -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());
}

View File

@ -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<String> getNamesOfFilesInLib() {
private Set<String> getNamesOfFilesInLibExt() {
Set<String> names = new HashSet<String>();
for (File file : new File("target/lib").listFiles()) {
for (File file : new File("target/lib/ext").listFiles()) {
names.add(file.getName());
}
return names;