org.elasticsearch.bootstrap.JarHell Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of elasticsearch Show documentation
Show all versions of elasticsearch Show documentation
Elasticsearch subproject :server
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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.elasticsearch.bootstrap;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.Version;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.io.PathUtils;
import org.elasticsearch.common.logging.Loggers;
import java.io.IOException;
import java.net.MalformedURLException;
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.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
/**
* 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("no jar hell found");
}
/**
* Checks the current classpath for duplicate classes
* @throws IllegalStateException if jar hell was found
*/
public static void checkJarHell() throws IOException, URISyntaxException {
ClassLoader loader = JarHell.class.getClassLoader();
Logger logger = Loggers.getLogger(JarHell.class);
if (logger.isDebugEnabled()) {
logger.debug("java.class.path: {}", System.getProperty("java.class.path"));
logger.debug("sun.boot.class.path: {}", System.getProperty("sun.boot.class.path"));
if (loader instanceof URLClassLoader ) {
logger.debug("classloader urls: {}", Arrays.toString(((URLClassLoader)loader).getURLs()));
}
}
checkJarHell(parseClassPath());
}
/**
* 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) {
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 {
URL url = PathUtils.get(element).toUri().toURL();
if (urlElements.add(url) == false) {
throw new IllegalStateException("jar hell!" + System.lineSeparator() +
"duplicate jar on classpath: " + classPath);
}
} catch (MalformedURLException e) {
// should not happen, as we use the filesystem API
throw new RuntimeException(e);
}
}
return Collections.unmodifiableSet(urlElements);
}
/**
* Checks the set of URLs for duplicate classes
* @throws IllegalStateException if jar hell was found
*/
@SuppressForbidden(reason = "needs JarFile for speed, just reading entries")
public static void checkJarHell(Set urls) throws URISyntaxException, IOException {
Logger logger = Loggers.getLogger(JarHell.class);
// 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");
logger.debug("java.home: {}", javaHome);
final Map clazzes = new HashMap<>(32768);
Set seenJars = new HashSet<>();
for (final URL url : urls) {
final Path path = PathUtils.get(url.toURI());
// exclude system resources
if (path.startsWith(javaHome)) {
logger.debug("excluding system resource: {}", path);
continue;
}
if (path.toString().endsWith(".jar")) {
if (!seenJars.add(path)) {
throw new IllegalStateException("jar hell!" + System.lineSeparator() +
"duplicate jar on classpath: " + path);
}
logger.debug("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 {
logger.debug("examining directory: {}", path);
// case for tests: where we have class files in the classpath
final Path root = PathUtils.get(url.toURI());
final String sep = root.getFileSystem().getSeparator();
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 */
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) {
checkVersionFormat(targetVersion);
checkJavaVersion(jar.toString(), targetVersion);
}
// give a nice error if jar is compiled against different es version
String systemESVersion = Version.CURRENT.toString();
String targetESVersion = manifest.getMainAttributes().getValue("X-Compile-Elasticsearch-Version");
if (targetESVersion != null && targetESVersion.equals(systemESVersion) == false) {
throw new IllegalStateException(jar + " requires Elasticsearch " + targetESVersion
+ ", your system: " + systemESVersion);
}
}
public static void checkVersionFormat(String targetVersion) {
if (!JavaVersion.isValid(targetVersion)) {
throw new IllegalStateException(
String.format(
Locale.ROOT,
"version string must be a sequence of nonnegative decimal integers separated by \".\"'s and may have leading zeros but was %s",
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) {
JavaVersion version = JavaVersion.parse(targetVersion);
if (JavaVersion.current().compareTo(version) < 0) {
throw new IllegalStateException(
String.format(
Locale.ROOT,
"%s requires Java %s:, your system: %s",
resource,
targetVersion,
JavaVersion.current().toString()
)
);
}
}
static void checkClass(Map clazzes, String clazz, Path jarpath) {
Path previous = clazzes.put(clazz, jarpath);
if (previous != null) {
if (previous.equals(jarpath)) {
if (clazz.startsWith("org.apache.xmlbeans")) {
return; // https://issues.apache.org/jira/browse/XMLBEANS-499
}
// 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);
}
}
}
}