This is an automated email from the ASF dual-hosted git repository.
bamaer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 819d67ef5f Share one scan class loader per plugin jar, fixes #9148
(#9152)
819d67ef5f is described below
commit 819d67ef5f53faae3b873196ab23923f3dd03945
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Fri Oct 30 06:34:32 2026 +0100
Share one scan class loader per plugin jar, fixes #9148 (#9152)
---
.../apache/hop/core/plugins/BasePluginType.java | 20 +++++++++++++++-
.../java/org/apache/hop/core/plugins/JarCache.java | 28 ++++++++++++++++++++++
2 files changed, 47 insertions(+), 1 deletion(-)
diff --git a/core/src/main/java/org/apache/hop/core/plugins/BasePluginType.java
b/core/src/main/java/org/apache/hop/core/plugins/BasePluginType.java
index 4be3a06f31..82c22ea443 100644
--- a/core/src/main/java/org/apache/hop/core/plugins/BasePluginType.java
+++ b/core/src/main/java/org/apache/hop/core/plugins/BasePluginType.java
@@ -729,13 +729,31 @@ public abstract class BasePluginType<T extends
Annotation> implements IPluginTyp
return new String[0];
}
+ /** The class in this plugin type's hierarchy that declares
createUrlClassLoader(). */
+ private Class<?> scanClassLoaderBuilder() {
+ for (Class<?> c = getClass(); c != null; c = c.getSuperclass()) {
+ try {
+ c.getDeclaredMethod("createUrlClassLoader", URL.class,
ClassLoader.class);
+ return c;
+ } catch (NoSuchMethodException e) {
+ // look in the super class
+ }
+ }
+ return BasePluginType.class;
+ }
+
protected void registerPluginJars() throws HopPluginException {
List<PluginClassFile> pluginClassFiles =
findAnnotatedClassFiles(pluginClass.getName());
+ Class<?> loaderBuilder = scanClassLoaderBuilder();
+ ClassLoader parent = getClass().getClassLoader();
for (PluginClassFile pluginClassFile : pluginClassFiles) {
+ URL jarFile = pluginClassFile.getJarFile();
URLClassLoader urlClassLoader =
- createUrlClassLoader(pluginClassFile.getJarFile(),
getClass().getClassLoader());
+ JarCache.getInstance()
+ .getScanClassLoader(
+ jarFile, loaderBuilder, parent, () ->
createUrlClassLoader(jarFile, parent));
try {
Class<?> clazz =
urlClassLoader.loadClass(pluginClassFile.getClassName());
diff --git a/core/src/main/java/org/apache/hop/core/plugins/JarCache.java
b/core/src/main/java/org/apache/hop/core/plugins/JarCache.java
index 26fcff6f93..c451a833a1 100644
--- a/core/src/main/java/org/apache/hop/core/plugins/JarCache.java
+++ b/core/src/main/java/org/apache/hop/core/plugins/JarCache.java
@@ -21,6 +21,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
+import java.net.URLClassLoader;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Enumeration;
@@ -29,6 +30,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.function.Supplier;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import org.apache.commons.lang3.StringUtils;
@@ -49,12 +51,14 @@ public class JarCache {
private final Map<File, Set<File>> jarFiles;
private final Set<File> nativeFiles;
private final Set<File> pluginFiles;
+ private final Map<ScanClassLoaderKey, URLClassLoader> scanClassLoaders;
private JarCache() {
nativeFiles = new HashSet<>();
pluginFiles = new HashSet<>();
jarFiles = new HashMap<>();
indexCache = new HashMap<>();
+ scanClassLoaders = new HashMap<>();
}
public static JarCache getInstance() {
@@ -180,11 +184,35 @@ public class JarCache {
return index;
}
+ /**
+ * The class loader the plugin scan uses to load the annotated classes of
one jar. Plugin types
+ * that build their scan class loaders the same way share one per jar and
parent, so a jar's lib/
+ * folder and dependencies.xml are read once and not for every annotated
class in it.
+ *
+ * @param jarFile the jar with the annotated classes
+ * @param builder the class whose createUrlClassLoader() builds the loader
+ * @param parent the parent class loader
+ * @param factory creates the class loader the first time this combination
is asked for
+ * @return the shared scan class loader
+ */
+ public URLClassLoader getScanClassLoader(
+ final URL jarFile,
+ final Class<?> builder,
+ final ClassLoader parent,
+ final Supplier<URLClassLoader> factory) {
+ return scanClassLoaders.computeIfAbsent(
+ new ScanClassLoaderKey(jarFile.toExternalForm(), builder, parent), k
-> factory.get());
+ }
+
+ private record ScanClassLoaderKey(String jarFile, Class<?> builder,
ClassLoader parent) {}
+
public void clear() {
nativeFiles.clear();
pluginFiles.clear();
indexCache.clear();
jarFiles.clear();
+ // The loaders stay open: classes loaded during the scan can still be
referenced.
+ scanClassLoaders.clear();
}
public Set<File> findJarFiles(final File folder) throws HopFileException {