org.elasticsearch.jdk.JarHell Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of elasticsearch-core Show documentation
Show all versions of elasticsearch-core Show documentation
Elasticsearch subproject :libs:elasticsearch-core
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.jdk;
import org.elasticsearch.core.PathUtils;
import org.elasticsearch.core.SuppressForbidden;
import java.io.IOException;
import java.lang.Runtime.Version;
import java.lang.module.Configuration;
import java.lang.module.ModuleReference;
import java.lang.module.ResolvedModule;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toUnmodifiableSet;
/**
* Simple check for duplicate class files across the classpath.
*
* This class checks for incompatibilities in the following ways:
*
* - Checks that class files are not duplicated across jars.
* - Checks any {@code X-Compile-Target-JDK} value in the jar
* manifest is compatible with current JRE
* - Checks any {@code X-Compile-Elasticsearch-Version} value in
* the jar manifest is compatible with the current ES
*
*/
public class JarHell {
/** no instantiation */
private JarHell() {}
/** Simple driver class, can be used eg. from builds. Returns non-zero on jar-hell */
@SuppressForbidden(reason = "command line tool")
public static void main(String args[]) throws Exception {
System.out.println("checking for jar hell...");
checkJarHell(System.out::println);
System.out.println("no jar hell found");
}
/**
* Checks the current classpath for duplicate classes
* @param output A {@link String} {@link Consumer} to which debug output will be sent
* @throws IllegalStateException if jar hell was found
*/
public static void checkJarHell(Consumer output) throws IOException {
ClassLoader loader = JarHell.class.getClassLoader();
output.accept("java.class.path: " + System.getProperty("java.class.path"));
output.accept("sun.boot.class.path: " + System.getProperty("sun.boot.class.path"));
if (loader instanceof URLClassLoader urlClassLoader) {
output.accept("classloader urls: " + Arrays.toString(urlClassLoader.getURLs()));
}
checkJarHell(parseClassPath(), output);
}
/**
* Parses the classpath into an array of URLs
* @return array of URLs
* @throws IllegalStateException if the classpath contains empty elements
*/
public static Set parseClassPath() {
return parseClassPath(System.getProperty("java.class.path"));
}
/**
* Parses the classpath into a set of URLs. For testing.
* @param classPath classpath to parse (typically the system property {@code java.class.path})
* @return array of URLs
* @throws IllegalStateException if the classpath contains empty elements
*/
@SuppressForbidden(reason = "resolves against CWD because that is how classpaths work")
static Set parseClassPath(String classPath) {
if (classPath.isEmpty()) {
return Set.of();
}
String pathSeparator = System.getProperty("path.separator");
String fileSeparator = System.getProperty("file.separator");
String elements[] = classPath.split(pathSeparator);
Set urlElements = new LinkedHashSet<>(); // order is already lost, but some filesystems have it
for (String element : elements) {
/*
* Technically empty classpath element behaves like CWD.
* So below is the "correct" code, however in practice with ES, this is usually just a misconfiguration,
* from old shell scripts left behind or something:
*
* if (element.isEmpty()) {
* element = System.getProperty("user.dir");
* }
*
* Instead we just throw an exception, and keep it clean.
*/
if (element.isEmpty()) {
throw new IllegalStateException(
"Classpath should not contain empty elements! (outdated shell script from a previous"
+ " version?) classpath='"
+ classPath
+ "'"
);
}
// we should be able to just Paths.get() each element, but unfortunately this is not the
// whole story on how classpath parsing works: if you want to know, start at sun.misc.Launcher,
// be sure to stop before you tear out your eyes. we just handle the "alternative" filename
// specification which java seems to allow, explicitly, right here...
if (element.startsWith("/") && "\\".equals(fileSeparator)) {
// "correct" the entry to become a normal entry
// change to correct file separators
element = element.replace("/", "\\");
// if there is a drive letter, nuke the leading separator
if (element.length() >= 3 && element.charAt(2) == ':') {
element = element.substring(1);
}
}
// now just parse as ordinary file
try {
if (element.equals("/")) {
// Eclipse adds this to the classpath when running unit tests...
continue;
}
URL url = PathUtils.get(element).toUri().toURL();
// junit4.childvm.count
if (urlElements.add(url) == false && element.endsWith(".jar")) {
throw new IllegalStateException(
"jar hell!" + System.lineSeparator() + "duplicate jar [" + element + "] on classpath: " + classPath
);
}
} catch (MalformedURLException e) {
// should not happen, as we use the filesystem API
throw new RuntimeException(e);
}
}
return Collections.unmodifiableSet(urlElements);
}
/**
* Returns a set of URLs that contain artifacts from both the non-JDK boot
* modules and class path. These URLs constitute the loadable application
* artifacts in the system class loader.
*/
public static Set parseModulesAndClassPath() {
return Stream.concat(parseClassPath().stream(), JarHell.nonJDKBootModuleURLs()).collect(toUnmodifiableSet());
}
/**
* Returns a stream containing the URLs of all non-JDK modules in the boot layer.
* The stream may be empty, if, say, running with ES on the class path.
*/
static Stream nonJDKBootModuleURLs() {
return nonJDKModuleURLs(ModuleLayer.boot().configuration());
}
static Stream nonJDKModuleURLs(Configuration configuration) {
return Stream.concat(Stream.of(configuration), configuration.parents().stream())
.map(Configuration::modules)
.flatMap(Set::stream)
.map(ResolvedModule::reference)
.map(ModuleReference::location)
.flatMap(Optional::stream)
.map(JarHell::toURL)
// assumption, only JDK modules are built into the image
.filter(url -> url.getProtocol().equals("jrt") == false);
}
/**
* Checks the set of URLs for duplicate classes
* @param urls A set of URLs from the system class loader to be checked for conflicting jars
* @param output A {@link String} {@link Consumer} to which debug output will be sent
* @throws IllegalStateException if jar hell was found
*/
@SuppressForbidden(reason = "needs JarFile for speed, just reading entries")
public static void checkJarHell(Set urls, Consumer output) throws IOException {
// we don't try to be sneaky and use deprecated/internal/not portable stuff
// like sun.boot.class.path, and with jigsaw we don't yet have a way to get
// a "list" at all. So just exclude any elements underneath the java home
String javaHome = System.getProperty("java.home");
output.accept("java.home: " + javaHome);
final Map clazzes = new HashMap<>(32768);
Set seenJars = new HashSet<>();
for (final URL url : urls) {
final Path path = toPath(url);
// exclude system resources
if (path.startsWith(javaHome)) {
output.accept("excluding system resource: " + path);
continue;
}
if (path.toString().endsWith(".jar")) {
if (seenJars.add(path) == false) {
throw new IllegalStateException("jar hell!" + System.lineSeparator() + "duplicate jar on classpath: " + path);
}
output.accept("examining jar: " + path);
try (JarFile file = new JarFile(path.toString())) {
Manifest manifest = file.getManifest();
if (manifest != null) {
checkManifest(manifest, path);
}
// inspect entries
Enumeration elements = file.entries();
while (elements.hasMoreElements()) {
String entry = elements.nextElement().getName();
if (entry.endsWith(".class")) {
// for jar format, the separator is defined as /
entry = entry.replace('/', '.').substring(0, entry.length() - 6);
checkClass(clazzes, entry, path);
}
}
}
} else {
output.accept("examining directory: " + path);
// case for tests: where we have class files in the classpath
final Path root = toPath(url);
final String sep = root.getFileSystem().getSeparator();
// don't try and walk class or resource directories that don't exist
// gradle will add these to the classpath even if they never get created
if (Files.exists(root)) {
Files.walkFileTree(root, new SimpleFileVisitor() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String entry = root.relativize(file).toString();
if (entry.endsWith(".class")) {
// normalize with the os separator, remove '.class'
entry = entry.replace(sep, ".").substring(0, entry.length() - ".class".length());
checkClass(clazzes, entry, path);
}
return super.visitFile(file, attrs);
}
});
}
}
}
}
/** inspect manifest for sure incompatibilities */
private static void checkManifest(Manifest manifest, Path jar) {
// give a nice error if jar requires a newer java version
String targetVersion = manifest.getMainAttributes().getValue("X-Compile-Target-JDK");
if (targetVersion != null) {
checkJavaVersion(jar.toString(), targetVersion);
}
}
/**
* Checks that the java specification version {@code targetVersion}
* required by {@code resource} is compatible with the current installation.
*/
public static void checkJavaVersion(String resource, String targetVersion) {
Version version = Version.parse(targetVersion);
if (Runtime.version().feature() < version.feature()) {
throw new IllegalStateException(
String.format(Locale.ROOT, "%s requires Java %s:, your system: %s", resource, targetVersion, Runtime.version().toString())
);
}
}
private static void checkClass(Map clazzes, String clazz, Path jarpath) {
if (clazz.equals("module-info") || clazz.endsWith(".module-info")) {
// Ignore jigsaw module descriptions
return;
}
Path previous = clazzes.put(clazz, jarpath);
if (previous != null) {
if (previous.equals(jarpath)) {
// throw a better exception in this ridiculous case.
// unfortunately the zip file format allows this buggy possibility
// UweSays: It can, but should be considered as bug :-)
throw new IllegalStateException(
"jar hell!"
+ System.lineSeparator()
+ "class: "
+ clazz
+ System.lineSeparator()
+ "exists multiple times in jar: "
+ jarpath
+ " !!!!!!!!!"
);
} else {
throw new IllegalStateException(
"jar hell!"
+ System.lineSeparator()
+ "class: "
+ clazz
+ System.lineSeparator()
+ "jar1: "
+ previous
+ System.lineSeparator()
+ "jar2: "
+ jarpath
);
}
}
}
private static URL toURL(URI uri) {
try {
return uri.toURL();
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
}
private static Path toPath(URL url) {
try {
return PathUtils.get(url.toURI());
} catch (URISyntaxException e) {
throw new AssertionError(e);
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy