fe/fe-extension-loader/README.md
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