All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.neo4j.procedure.impl.ProcedureClassLoader Maven / Gradle / Ivy

/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [https://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */
package org.neo4j.procedure.impl;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.jar.JarFile;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import org.neo4j.internal.kernel.api.exceptions.ProcedureException;
import org.neo4j.kernel.api.exceptions.Status;
import org.neo4j.kernel.extension.ExtensionFactory;
import org.neo4j.logging.InternalLog;
import org.neo4j.util.VisibleForTesting;

/**
 * This class provide child-first delegated classloading for JARs that do not inject state into the DBMS.
 * 

* The classloader uses a simple heuristic to determine if the JARs inject state or not. It checks the archive for presence of the file * META-INF/services/org.neo4j.kernel.extension.ExtensionFactory. This file is required for the DBMS to be able to service load the extension. * If present, the JAR is assumed to store complex state that will not be reloadable. Otherwise, the JAR is assumed to be "stateless", and we * thus take care to resolve it in the local classloader, rather than resolving it in the parent classloader as would happen if the archive is * on the classpath. */ class ProcedureClassLoader extends URLClassLoader { /** * Return a new ProcedureClassLoader from a collection of JARs * * @param jars Paths to JAR archives * @return the created classloader and a list of classes it has resolved. */ public static Result setup(Collection jars, InternalLog log, boolean procedureReloadEnabled) throws ZipException, ProcedureException { return setup(ProcedureClassLoader.class.getClassLoader(), jars, log, procedureReloadEnabled); } @VisibleForTesting static Result setup(ClassLoader parent, Collection jars, InternalLog log, boolean procedureReloadEnabled) throws ZipException, ProcedureException { var loader = new ProcedureClassLoader( jars.stream().map(ProcedureClassLoader::toURL).toArray(URL[]::new), parent); // Resolve classes from JARs with extensions using the normal classloader hierarchy. ClassResolver extensionResolver = loader::loadClass; // Optionally resolve classes from JARs that do not contain extensions directly in the // ProcedureClassLoader to make it possible to reload these. ClassResolver extensionlessResolver = (procedureReloadEnabled ? loader::preload : loader::loadClass); // Enumerate all classes present in the provided JARs. var classes = enumerateClasses(jars, log); var entries = resolveAll(classes, log, extensionResolver, extensionlessResolver); return new Result(loader, entries); } public record Result(ProcedureClassLoader loader, List loadedClasses) {} private ProcedureClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } // Register the classloader as parallel capable to avoid deadlocks. We want to mimic the // behaviour of the URLClassLoader and implement the same behaviour. static { ClassLoader.registerAsParallelCapable(); } private Class preload(String name) throws ClassNotFoundException { // Validate that the class was not already loaded in the parent class loader. Class klass = super.findLoadedClass(name); if (klass != null) { return klass; } // Find the class directly among the JARs of this classloader. // If we can't find it for some reason, fail with a ClassNotFoundException. klass = findClass(name); // Resolve it in this classloader so that we do not attempt to // load it in the parent in the future. resolveClass(klass); return klass; } private interface ClassResolver { Class resolve(String name) throws ClassNotFoundException; } public record Entry(Path jar, Class cls) {} private static List resolveAll( ClassEnumeration enumeration, InternalLog log, ClassResolver extensionResolver, ClassResolver extensionlessResolver) throws ProcedureException { var entries = new ArrayList(); // Ordering here is important. Stateless classes should be loaded before stateful to // ensure that the classes can be classload:ed again. entries.addAll(resolve(enumeration.withoutExtensions, log, extensionlessResolver)); entries.addAll(resolve(enumeration.withExtensions, log, extensionResolver)); return entries; } private static List resolve(Map classes, InternalLog log, ClassResolver method) throws ProcedureException { List exceptions = new ArrayList<>(); List entries = new ArrayList<>(); for (var entry : classes.entrySet()) { var className = entry.getKey(); var jar = entry.getValue().toAbsolutePath(); Class klass; try { klass = method.resolve(entry.getKey()); } catch (ClassNotFoundException exc) { // Should not happen... exceptions.add(exc); continue; } catch (IllegalAccessError exc) { // Will likely happen when a different versions of library already has been loaded by the DBMS logWarning(log, className, jar, exc); exceptions.add(exc); continue; } catch (NoClassDefFoundError exc) { // This error is considered non-fatal in the current implementation, // and is triggered e.g. by attempting to classload "META-INF.versions.9.my.package". logWarning(log, className, jar, exc); continue; } catch (LinkageError exc) { // Not quite sure what errors trigger this case. logWarning(log, className, jar, exc); continue; } // Evaluate the declared attributes of the class, to attempt to produce a LinkageError // if the dependency is unmet. try { //noinspection ResultOfMethodCallIgnored klass.getDeclaredClasses(); //noinspection ResultOfMethodCallIgnored klass.getDeclaredMethods(); //noinspection ResultOfMethodCallIgnored klass.getDeclaredFields(); entries.add(new Entry(jar, klass)); } catch (LinkageError | Exception exc) { logWarning(log, className, jar, exc); } } // Collect exceptions and throw them as a unit if (!exceptions.isEmpty()) { var exc = new ProcedureException( Status.Procedure.ProcedureRegistrationFailed, "Failed to register procedures for the following reasons:"); for (var exception : exceptions) { exc.addSuppressed(exception); } throw exc; } return entries; } private static void logWarning(InternalLog log, String className, Path jar, Throwable exc) { log.warn( "Failed to load `%s` from plugin jar `%s`: %s: %s", className, jar, exc.getClass().getName(), exc.getMessage()); } private static URL toURL(Path f) { try { return f.toUri().toURL(); } catch (MalformedURLException e) { throw new RuntimeException(e); } } private record ClassEnumeration(Map withExtensions, Map withoutExtensions) {} private static ClassEnumeration enumerateClasses(Collection jars, InternalLog log) throws ZipException { var out = new ClassEnumeration(new HashMap<>(), new HashMap<>()); var invalidFiles = new ArrayList(); for (Path pth : jars) { try (var jf = open(pth)) { var content = listClasses(jf); var destination = (content.hasExtension ? out.withExtensions : out.withoutExtensions); for (var klass : content.classes()) { destination.put(klass, pth); } } catch (IOException exc) { log.error(String.format("Plugin jar file: %s corrupted.", pth)); invalidFiles.add(pth.getFileName().toString()); } } if (!invalidFiles.isEmpty()) { throw new ZipException(String.format( "Some jar procedure files (%s) are invalid, see log for details.", String.join(", ", invalidFiles))); } return out; } private record Content(List classes, boolean hasExtension) {} private static Content listClasses(JarFile jf) { var classes = new ArrayList(); var it = jf.versionedStream().iterator(); boolean hasExtension = jf.getEntry("META-INF/services/" + ExtensionFactory.class.getCanonicalName()) != null; while (it.hasNext()) { var entry = it.next(); var name = entry.getName(); if (name.endsWith(".class")) { classes.add(qualifiedName(name)); } } return new Content(classes, hasExtension); } private static String qualifiedName(String name) { return name.substring(0, name.length() - ".class".length()).replace('/', '.'); } private static JarFile open(Path pth) throws IOException { Objects.requireNonNull(pth); return new JarFile(pth.toFile(), true, ZipFile.OPEN_READ, JarFile.runtimeVersion()); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy