This guide is for business module developers. It focuses on:
fe-extension-loader is the reusable plugin runtime foundation for Doris FE.
It unifies repeated loading logic across modules, including:
pluginRoots.ServiceLoader.Use Loader if your module needs:
scan, resolve, discover, etc.).Loader may not be a direct fit if:
In those cases, use fe-extension-spi only.
DirectoryPluginRuntimeManager<F>This is the runtime entry class and unified facade.
Primary methods:
loadAll(...)get(pluginName)list()Business modules should depend on this class directly.
DirectoryPluginRuntimeManagerThe manager performs:
pluginDir/*.jar and pluginDir/lib/*.jar.LoadReport.PluginLoaderLow-level utility class. It does not scan directories. It only handles:
Use it when you already own classloader lifecycle externally.
ChildFirstClassLoaderClassloading behavior:
Purpose:
ClassCastException.ClassLoadingPolicyUsed to configure parent-first prefixes:
Common business examples:
org.apache.doris.authentication.org.apache.doris.authorization.PluginHandle<F>Represents one successfully loaded plugin, including:
pluginNamepluginDirresolvedJarsclassLoaderfactoryloadedAtBusiness modules typically consume pluginName + factory for registration.
LoadFailureRepresents one failed plugin directory load, including:
pluginDirstagemessagecauseFailure stages:
scanresolvecreateClassLoaderdiscoverinstantiateconflictLoadReport<F>Represents the full result of one loadAll call, including:
successesfailuresrootsScanned, dirsScannedDirectoryPluginRuntimeManager stores loaded handles in an internal concurrent map.
No separate PluginRuntimeRegistry abstraction is exposed in current implementation.
Your business factory interface should extend PluginFactory.
DirectoryPluginRuntimeManager<MyPluginFactory> runtime = new DirectoryPluginRuntimeManager<>();
ClassLoadingPolicy policy = new ClassLoadingPolicy( Collections.singletonList("org.apache.doris.mybiz."));
loadAllLoadReport<MyPluginFactory> report = runtime.loadAll( pluginRoots, Thread.currentThread().getContextClassLoader(), MyPluginFactory.class, policy);
for (LoadFailure failure : report.getFailures()) { LOG.warn("plugin load failure: dir={}, stage={}, message={}", failure.getPluginDir(), failure.getStage(), failure.getMessage(), failure.getCause()); } for (PluginHandle<MyPluginFactory> handle : report.getSuccesses()) { factoryMap.putIfAbsent(handle.getPluginName(), handle.getFactory()); }
Recommended layout:
<pluginRoot>/ <pluginA>/ pluginA.jar lib/ dep1.jar dep2.jar <pluginB>/ pluginB.jar
Rules:
pluginRoot are scanned.Current default strategy:
conflict.Business recommendations:
conflict as warning/alert.Loader returns LoadReport and does not force exception.
Business modules choose policy by semantics:
LoadReport Handling StrategyLoadReport should be startup decision input, not just logs.
Recommended goals:
Recommended processing order:
rootsScanned, dirsScanned, successes.size, failures.size.pluginDir + stage + message + cause.pluginName -> factory).Suggested stage severity grouping:
scan, resolve.discover, instantiate.createClassLoader.conflict (usually warning, not immediate stop).Recommended decision rules:
dirsScanned == 0: often means empty roots or no external setup.dirsScanned > 0 && successes.isEmpty() in strict mode: fail-fast.dirsScanned > 0 && successes.isEmpty() in tolerant mode: warn and continue only if business allows no external plugin.requiredPluginNames exists, enforce presence even when partial loads succeeded.public static <F extends PluginFactory> void processLoadReport( LoadReport<F> report, Map<String, F> factoryMap, boolean strictMode, Set<String> requiredPluginNames) { Objects.requireNonNull(report, "report"); Objects.requireNonNull(factoryMap, "factoryMap"); Objects.requireNonNull(requiredPluginNames, "requiredPluginNames"); // Step 1: summary metrics LOG.info("plugin load summary: rootsScanned={}, dirsScanned={}, successCount={}, failureCount={}", report.getRootsScanned(), report.getDirsScanned(), report.getSuccesses().size(), report.getFailures().size()); // Step 2: failure details LoadFailure firstNonConflictFailure = null; for (LoadFailure failure : report.getFailures()) { LOG.warn("plugin load failure: dir={}, stage={}, message={}", failure.getPluginDir(), failure.getStage(), failure.getMessage(), failure.getCause()); if (!LoadFailure.STAGE_CONFLICT.equals(failure.getStage()) && firstNonConflictFailure == null) { firstNonConflictFailure = failure; } } // Step 3: register successful plugins int registered = 0; for (PluginHandle<F> handle : report.getSuccesses()) { F existing = factoryMap.putIfAbsent(handle.getPluginName(), handle.getFactory()); if (existing != null) { // If business map already contains the name, close discarded external classloader. closeClassLoaderQuietly(handle.getClassLoader()); LOG.warn("skip duplicated plugin name in business map: {}", handle.getPluginName()); continue; } registered++; } // Step 4: startup decision (strict/tolerant) if (strictMode && report.getDirsScanned() > 0 && registered == 0 && firstNonConflictFailure != null) { throw new IllegalStateException( "No plugin loaded in strict mode: stage=" + firstNonConflictFailure.getStage() + ", dir=" + firstNonConflictFailure.getPluginDir() + ", message=" + firstNonConflictFailure.getMessage(), firstNonConflictFailure.getCause()); } // Step 5: required plugin checks for (String required : requiredPluginNames) { if (!factoryMap.containsKey(required)) { throw new IllegalStateException("Required plugin is missing: " + required); } } }
The closeClassLoaderQuietly implementation pattern can be referenced from:
../fe-authentication/fe-authentication-handler/src/main/java/org/apache/doris/authentication/handler/AuthenticationPluginManager.java
Current authentication module handling is:
report.getFailures() and log warnings.report.getSuccesses() and register factories (close duplicated external classloader if needed).AuthenticationException when directories were scanned but no external plugin was loaded.Reference implementation:
../fe-authentication/fe-authentication-handler/src/main/java/org/apache/doris/authentication/handler/AuthenticationPluginManager.java
Supported:
loadAllgetlistNot supported:
reloadunloadDo not depend on runtime hot-reload semantics in V1.
Check:
META-INF/services/<factoryType>.Check:
Note:
DirectoryPluginRuntimeManager includes parent service-resource filtering and prefers plugin-directory-local discovery.
Check:
close() may not release resources.Authentication integration sample:
../fe-authentication/fe-authentication-handler/src/main/java/org/apache/doris/authentication/handler/AuthenticationPluginManager.java
Key integration points:
DirectoryPluginRuntimeManager<AuthenticationPluginFactory>.README_CN.md../fe-authentication/EXTENSION_LOADER_UNIFIED_DESIGN_CN.md../fe-extension-spi/README.md