From ff3c01cfaa9dc40465f8c7cd892b4b19ffd87310 Mon Sep 17 00:00:00 2001 From: Andrea Di Cesare Date: Tue, 13 Feb 2024 11:02:26 +0100 Subject: [PATCH] :bug: Refactor plugin class loading to use custom PluginsClassloader. This change ensures that plugin classes are consistently loaded by a single classloader, preventing conflicts arising from classes being loaded by multiple classloaders. --- .../restheart/plugins/PluginsClassloader.java | 79 +++++++++++++++++++ .../org/restheart/plugins/PluginsFactory.java | 22 +----- .../org/restheart/plugins/PluginsScanner.java | 38 +-------- .../org/restheart/plugins/PluginsTest.java | 5 +- 4 files changed, 87 insertions(+), 57 deletions(-) create mode 100644 core/src/main/java/org/restheart/plugins/PluginsClassloader.java diff --git a/core/src/main/java/org/restheart/plugins/PluginsClassloader.java b/core/src/main/java/org/restheart/plugins/PluginsClassloader.java new file mode 100644 index 000000000..746a3508b --- /dev/null +++ b/core/src/main/java/org/restheart/plugins/PluginsClassloader.java @@ -0,0 +1,79 @@ +/*- + * ========================LICENSE_START================================= + * restheart-core + * %% + * Copyright (C) 2014 - 2024 SoftInstigate + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License + * along with this program. If not, see . + * =========================LICENSE_END================================== + */ +package org.restheart.plugins; + +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; + +/** + * Loads a class, including searching within all plugin JAR files. + *

+ * This method is essential for the {@code collectFieldInjections()} method, which processes field injections + * annotated with {@code @Inject}. Specifically, it addresses cases where the {@code @Inject} annotation + * references a {@link org.restheart.plugins.Provider} that returns an object. The class of this object may reside + * in a plugin JAR file, necessitating a comprehensive search to locate and load the class correctly. + *

+ */ +public class PluginsClassloader extends ClassLoader { + private static PluginsClassloader SINGLETON = null; + + /** + * call after PluginsScanner.jars array is populated + * @param jars + */ + public static void init(URL[] jars) { + if (SINGLETON != null) { + throw new IllegalStateException("already initialized"); + } else { + try { + SINGLETON = new PluginsClassloader(jars); + } catch(IOException ioe) { + throw new RuntimeException("error initializing", ioe); + } + } + } + + private final URLClassLoader pluginsClassLoader; + + private PluginsClassloader(URL[] jars) throws IOException { + this.pluginsClassLoader = new URLClassLoader(jars); + } + + public static PluginsClassloader getInstance() { + if (SINGLETON == null) { + throw new IllegalStateException("not initialized"); + } else { + return SINGLETON; + } + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + try { + // use the current classloader + return this.getClass().getClassLoader().loadClass(name); + } catch (ClassNotFoundException cnfe) { + // look in the plugins jars + return this.pluginsClassLoader.loadClass(name); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/org/restheart/plugins/PluginsFactory.java b/core/src/main/java/org/restheart/plugins/PluginsFactory.java index 5e74f10b6..e0a04d1db 100644 --- a/core/src/main/java/org/restheart/plugins/PluginsFactory.java +++ b/core/src/main/java/org/restheart/plugins/PluginsFactory.java @@ -23,7 +23,6 @@ import static org.restheart.configuration.Utils.getOrDefault; import java.lang.reflect.InvocationTargetException; -import java.net.URLClassLoader; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashSet; @@ -56,15 +55,7 @@ public static PluginsFactory getInstance() { return SINGLETON; } - private final ArrayList classLoaders = new ArrayList<>(); - private PluginsFactory() { - classLoaders.add(this.getClass().getClassLoader()); - - // take classloaders from PluginsScanner into account - if (PluginsScanner.jars != null) { - classLoaders.add(new URLClassLoader(PluginsScanner.jars)); - } } private Set> authMechanismsCache = null; @@ -283,18 +274,7 @@ private Class loadPluginClass(PluginDescriptor plugin) throws ClassNotFo return PC_CACHE.get(plugin.clazz()); } - for (var classLoader : this.classLoaders) { - try { - var pluginc = (Class) classLoader.loadClass(plugin.clazz()); - - PC_CACHE.put(plugin.clazz(), pluginc); - return pluginc; - } catch (ClassNotFoundException cnfe) { - // nothing to do - } - } - - throw new ClassNotFoundException("plugin class not found " + plugin.clazz()); + return (Class) PluginsClassloader.getInstance().loadClass(plugin.clazz()); } private Plugin instantiatePlugin(Class pc, String pluginType, String pluginName, Configuration conf) throws InstantiationException, IllegalAccessException, InvocationTargetException, IllegalArgumentException, SecurityException, ClassNotFoundException { diff --git a/core/src/main/java/org/restheart/plugins/PluginsScanner.java b/core/src/main/java/org/restheart/plugins/PluginsScanner.java index cf0b05a2c..20c3d985b 100644 --- a/core/src/main/java/org/restheart/plugins/PluginsScanner.java +++ b/core/src/main/java/org/restheart/plugins/PluginsScanner.java @@ -45,7 +45,6 @@ import java.util.stream.Collectors; import java.util.AbstractMap; -import org.checkerframework.common.returnsreceiver.qual.This; import org.restheart.Bootstrapper; import org.restheart.graal.NativeImageBuildTimeChecker; import org.restheart.plugins.security.AuthMechanism; @@ -300,7 +299,7 @@ private static ArrayList collectFieldInjections(ClassInfo p } try { - var fieldClass = loadClass(fi.getTypeDescriptor().toString()); + var fieldClass = PluginsClassloader.getInstance().loadClass(fi.getTypeDescriptor().toString()); ret.add(new FieldInjectionDescriptor(fi.getName(), fieldClass, annotationParams, fi.hashCode())); } catch(ClassNotFoundException cnfe) { // should not happen @@ -312,39 +311,6 @@ private static ArrayList collectFieldInjections(ClassInfo p return ret; } - private static ArrayList _classLoaders = null; - - /** - * Loads a class, including searching within all plugin JAR files. - *

- * This method is essential for the {@code collectFieldInjections()} method, which processes field injections - * annotated with {@code @Inject}. Specifically, it addresses cases where the {@code @Inject} annotation - * references a {@link org.restheart.plugins.Provider} that returns an object. The class of this object may reside - * in a plugin JAR file, necessitating a comprehensive search to locate and load the class correctly. - *

- */ - private static Class loadClass(String clazz) throws ClassNotFoundException { - if (_classLoaders == null || _classLoaders.isEmpty()) { - _classLoaders = new ArrayList<>(); - _classLoaders.add(PluginsScanner.class.getClassLoader()); - - // take all classloaders into account to search also within all plugin JAR files - if (PluginsScanner.jars != null) { - _classLoaders.add(new URLClassLoader(PluginsScanner.jars)); - } - } - - for (var classLoader: _classLoaders) { - try { - return Class.forName(clazz, false, classLoader); - } catch (ClassNotFoundException cnfe) { - // nothing to do - } - } - - throw new ClassNotFoundException("plugin class not found " + clazz); - } - /** * this removes the reference to scanResult in the annotation info * otherwise the huge object won't be garbage collected @@ -373,6 +339,8 @@ public RuntimeClassGraph() { this.jars = findPluginsJars(pdir); if (jars != null && jars.length != 0) { + PluginsClassloader.init(jars); + this.classGraph = new ClassGraph().disableModuleScanning().disableDirScanning() .disableNestedJarScanning().disableRuntimeInvisibleAnnotations() .addClassLoader(new URLClassLoader(jars)).addClassLoader(ClassLoader.getSystemClassLoader()) diff --git a/core/src/test/java/org/restheart/plugins/PluginsTest.java b/core/src/test/java/org/restheart/plugins/PluginsTest.java index 8a68b582f..80e2a8cda 100644 --- a/core/src/test/java/org/restheart/plugins/PluginsTest.java +++ b/core/src/test/java/org/restheart/plugins/PluginsTest.java @@ -21,6 +21,8 @@ package org.restheart.plugins; +import java.net.URL; + import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mockStatic; @@ -121,6 +123,7 @@ public void validProviders() { } private static MockedStatic mockPluginsScanner(List providerDescriptors) { + PluginsClassloader.init(new URL[0]); var scanner = mockStatic(PluginsScanner.class); scanner.when(PluginsScanner::providers).thenReturn(providerDescriptors); return scanner; @@ -204,7 +207,7 @@ private static List providerDescriptors() { var iC2 = new ArrayList(); var apC2 = new ArrayList>(); - apC2.add(new AbstractMap.SimpleEntry("value", "c1")); + apC2.add(new AbstractMap.SimpleEntry<>("value", "c1")); iC2.add(new FieldInjectionDescriptor("s", String.class, apC2, 8)); var providerDescriptors = new ArrayList();