Browse Source
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-4868pull/5052/merge
27 changed files with 227 additions and 735 deletions
@ -1,100 +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.lang.management.ManagementFactory; |
|
||||||
import java.net.URL; |
|
||||||
import java.security.AccessController; |
|
||||||
import java.security.PrivilegedAction; |
|
||||||
import java.util.Collections; |
|
||||||
import java.util.HashSet; |
|
||||||
import java.util.List; |
|
||||||
import java.util.Set; |
|
||||||
|
|
||||||
/** |
|
||||||
* A {@link JavaAgentDetector} that detects jars supplied via the {@code -javaagent} JVM |
|
||||||
* input argument. |
|
||||||
* |
|
||||||
* @author Andy Wilkinson |
|
||||||
* @since 1.1.0 |
|
||||||
*/ |
|
||||||
public class InputArgumentsJavaAgentDetector implements JavaAgentDetector { |
|
||||||
|
|
||||||
private static final String JAVA_AGENT_PREFIX = "-javaagent:"; |
|
||||||
|
|
||||||
private final Set<URL> javaAgentJars; |
|
||||||
|
|
||||||
public InputArgumentsJavaAgentDetector() { |
|
||||||
this(getInputArguments()); |
|
||||||
} |
|
||||||
|
|
||||||
InputArgumentsJavaAgentDetector(List<String> inputArguments) { |
|
||||||
this.javaAgentJars = getJavaAgentJars(inputArguments); |
|
||||||
} |
|
||||||
|
|
||||||
private static List<String> getInputArguments() { |
|
||||||
try { |
|
||||||
return AccessController.doPrivileged(new PrivilegedAction<List<String>>() { |
|
||||||
@Override |
|
||||||
public List<String> run() { |
|
||||||
return ManagementFactory.getRuntimeMXBean().getInputArguments(); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
catch (Exception ex) { |
|
||||||
return Collections.emptyList(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private Set<URL> getJavaAgentJars(List<String> inputArguments) { |
|
||||||
Set<URL> javaAgentJars = new HashSet<URL>(); |
|
||||||
for (String argument : inputArguments) { |
|
||||||
String path = getJavaAgentJarPath(argument); |
|
||||||
if (path != null) { |
|
||||||
try { |
|
||||||
javaAgentJars.add(new File(path).getCanonicalFile().toURI().toURL()); |
|
||||||
} |
|
||||||
catch (IOException ex) { |
|
||||||
throw new IllegalStateException( |
|
||||||
"Failed to determine canonical path of Java agent at path '" |
|
||||||
+ path + "'"); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
return javaAgentJars; |
|
||||||
} |
|
||||||
|
|
||||||
private String getJavaAgentJarPath(String arg) { |
|
||||||
if (arg.startsWith(JAVA_AGENT_PREFIX)) { |
|
||||||
String path = arg.substring(JAVA_AGENT_PREFIX.length()); |
|
||||||
int equalsIndex = path.indexOf('='); |
|
||||||
if (equalsIndex > -1) { |
|
||||||
path = path.substring(0, equalsIndex); |
|
||||||
} |
|
||||||
return path; |
|
||||||
} |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public boolean isJavaAgentJar(URL url) { |
|
||||||
return this.javaAgentJars.contains(url); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -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; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -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(); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
Loading…
Reference in new issue