Use a conventional delegation model in LaunchedURLClassLoader

When an application is run as an executable archive with nested jars,
the application's own classes need to be able to load classes from
within the nested jars. This means that the application's classes need
to be loaded by the same class loader as is used for the nested jars.
When an application is launched with java -jar the contents of the
jar are on the class path of the app class loader, which is the
parent of the LaunchedURLClassLoader that is used to load classes
from within the nested jars. If the root of the jar includes the
application's classes, they would be loaded by the app class loader
and, therefore, would not be able to load classes from within the
nested jars.

Previously, this problem was resolved by LaunchedURLClassLoader being
created with a copy of all of the app class laoder's URLs and by
using an unconventional delegation model that caused it to skip its
parent (the app class loader) and jump straight to its root class
loader. This ensured that the LaunchedURLClassLoader would load both
the application's own classes and those from within any nested jars.
Unfortunately, this unusual delegation model has proved to be
problematic. We have seen and worked around some problems with Java
Agents (see gh-4911 and gh-863), but there are others (see gh-4868)
that cannot be made to work with the current delegation model.

This commit reworks LaunchedURLClassLoader to use a conventional
delegate model with the app class loader as its parent. With this
change in place, the application's own classes need to be hidden
from the app class loader via some other means. This is now achieved
by packaging application classes in BOOT-INF/classes (and, for
symmetry, nested jars are now packaged in BOOT-INF/lib). Both the
JarLauncher and the PropertiesLauncher (which supports the executable
jar layout) have been updated to look for classes and nested jars in
these new locations.

Closes gh-4897
Fixes gh-4868
This commit is contained in:
Andy Wilkinson
2016-01-13 15:52:39 +00:00
parent 9be69f1ac7
commit 87fe0b2ade
27 changed files with 230 additions and 738 deletions

View File

@@ -1,118 +0,0 @@
/*
* Copyright 2012-2014 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.Callable;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.loader.archive.Archive.Entry;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
/**
* Tests for {@link ExecutableArchiveLauncher}
*
* @author Andy Wilkinson
*/
public class ExecutableArchiveLauncherTests {
@Mock
private JavaAgentDetector javaAgentDetector;
private ExecutableArchiveLauncher launcher;
@Before
public void setupMocks() {
MockitoAnnotations.initMocks(this);
this.launcher = new UnitTestExecutableArchiveLauncher(this.javaAgentDetector);
}
@Test
public void createdClassLoaderContainsUrlsFromThreadContextClassLoader()
throws Exception {
final URL[] urls = new URL[] { new URL("file:one"), new URL("file:two") };
doWithTccl(new URLClassLoader(urls), new Callable<Void>() {
@Override
public Void call() throws Exception {
ClassLoader classLoader = ExecutableArchiveLauncherTests.this.launcher
.createClassLoader(new URL[0]);
assertClassLoaderUrls(classLoader, urls);
return null;
}
});
}
@Test
public void javaAgentJarsAreExcludedFromClasspath() throws Exception {
URL javaAgent = new File("my-agent.jar").getCanonicalFile().toURI().toURL();
final URL one = new URL("file:one");
given(this.javaAgentDetector.isJavaAgentJar(javaAgent)).willReturn(true);
doWithTccl(new URLClassLoader(new URL[] { javaAgent, one }),
new Callable<Void>() {
@Override
public Void call() throws Exception {
ClassLoader classLoader = ExecutableArchiveLauncherTests.this.launcher
.createClassLoader(new URL[0]);
assertClassLoaderUrls(classLoader, new URL[] { one });
return null;
}
});
}
private void assertClassLoaderUrls(ClassLoader classLoader, URL[] urls) {
assertThat(classLoader).isInstanceOf(URLClassLoader.class);
assertThat(((URLClassLoader) classLoader).getURLs()).isEqualTo(urls);
}
private void doWithTccl(ClassLoader classLoader, Callable<?> action)
throws Exception {
ClassLoader old = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(classLoader);
action.call();
}
finally {
Thread.currentThread().setContextClassLoader(old);
}
}
private static final class UnitTestExecutableArchiveLauncher
extends ExecutableArchiveLauncher {
UnitTestExecutableArchiveLauncher(JavaAgentDetector javaAgentDetector) {
super(javaAgentDetector);
}
@Override
protected boolean isNestedArchive(Entry entry) {
return false;
}
}
}

View File

@@ -1,73 +0,0 @@
/*
* Copyright 2012-2014 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Arrays;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link InputArgumentsJavaAgentDetector}
*
* @author Andy Wilkinson
*/
public class InputArgumentsJavaAgentDetectorTests {
@Test
public void nonAgentJarsDoNotProduceFalsePositives()
throws MalformedURLException, IOException {
InputArgumentsJavaAgentDetector detector = new InputArgumentsJavaAgentDetector(
Arrays.asList("-javaagent:my-agent.jar"));
assertThat(detector.isJavaAgentJar(
new File("something-else.jar").getCanonicalFile().toURI().toURL()))
.isFalse();
}
@Test
public void singleJavaAgent() throws MalformedURLException, IOException {
InputArgumentsJavaAgentDetector detector = new InputArgumentsJavaAgentDetector(
Arrays.asList("-javaagent:my-agent.jar"));
assertThat(detector.isJavaAgentJar(
new File("my-agent.jar").getCanonicalFile().toURI().toURL())).isTrue();
}
@Test
public void singleJavaAgentWithOptions() throws MalformedURLException, IOException {
InputArgumentsJavaAgentDetector detector = new InputArgumentsJavaAgentDetector(
Arrays.asList("-javaagent:my-agent.jar=a=alpha,b=bravo"));
assertThat(detector.isJavaAgentJar(
new File("my-agent.jar").getCanonicalFile().toURI().toURL())).isTrue();
}
@Test
public void multipleJavaAgents() throws MalformedURLException, IOException {
InputArgumentsJavaAgentDetector detector = new InputArgumentsJavaAgentDetector(
Arrays.asList("-javaagent:my-agent.jar",
"-javaagent:my-other-agent.jar"));
assertThat(detector.isJavaAgentJar(
new File("my-agent.jar").getCanonicalFile().toURI().toURL())).isTrue();
assertThat(detector.isJavaAgentJar(
new File("my-other-agent.jar").getCanonicalFile().toURI().toURL()))
.isTrue();
}
}

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.
@@ -39,19 +39,6 @@ public class LaunchedURLClassLoaderTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void resolveResourceFromWindowsFilesystem() throws Exception {
// This path is invalid - it should return null even on Windows.
// A regular URLClassLoader will deal with it gracefully.
assertThat(getClass().getClassLoader()
.getResource("c:\\Users\\user\\bar.properties")).isNull();
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(
new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") },
getClass().getClassLoader());
// So we should too...
assertThat(loader.getResource("c:\\Users\\user\\bar.properties")).isNull();
}
@Test
public void resolveResourceFromArchive() throws Exception {
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(

View File

@@ -22,21 +22,17 @@ import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link PropertiesLauncher}.
@@ -46,9 +42,6 @@ import static org.mockito.Mockito.verify;
*/
public class PropertiesLauncherTests {
@Mock
private JavaAgentDetector javaAgentDetector;
@Rule
public OutputCapture output = new OutputCapture();
@@ -195,21 +188,6 @@ public class PropertiesLauncherTests {
.isEqualTo("[foo, bar]");
}
@Test
public void testJavaAgentJarsAreExcludedFromClasspath() throws Exception {
List<Archive> allArchives = new PropertiesLauncher().getClassPathArchives();
URL[] parentUrls = ((URLClassLoader) getClass().getClassLoader()).getURLs();
for (URL url : parentUrls) {
given(this.javaAgentDetector.isJavaAgentJar(url)).willReturn(true);
}
List<Archive> nonAgentArchives = new PropertiesLauncher(this.javaAgentDetector)
.getClassPathArchives();
assertThat(nonAgentArchives).hasSize(allArchives.size() - parentUrls.length);
for (URL url : parentUrls) {
verify(this.javaAgentDetector).isJavaAgentJar(url);
}
}
private void waitFor(String value) throws Exception {
int count = 0;
boolean timeout = false;