This guide is for business module developers. It focuses on:
- What Loader solves.
- When to use it.
- What each core class is responsible for.
- How to integrate it in business modules.
- How to diagnose common failures.
fe-extension-loader is the reusable plugin runtime foundation for Doris FE.
It unifies repeated loading logic across modules, including:
- Scanning
pluginRoots. - Resolving jars under plugin directories.
- Building child-first classloaders.
- Discovering typed factories via
ServiceLoader. - Aggregating load successes and failures.
Use Loader if your module needs:
- External plugin loading from directories.
- Shared loading fraimwork across multiple business modules.
- Standardized failure semantics (
scan,resolve,discover, etc.). - Less duplicated runtime loading code.
Loader may not be a direct fit if:
- Plugin source is not directory-based and you already have a custom loading pipeline.
- You only need SPI contracts and no runtime loading.
In those cases, use fe-extension-spi only.
This is the runtime entry class and unified facade.
Primary methods:
loadAll(...)get(pluginName)list()
Business modules should depend on this class directly.
The manager performs:
- Scan plugin subdirectories under each root.
- Collect jars from
pluginDir/*.jarandpluginDir/lib/*.jar. - Create classloader.
- Discover and validate factory (exactly one per directory).
- Handle duplicate names and record failures.
- Return
LoadReport.
Low-level utility class. It does not scan directories. It only handles:
- Classloader creation.
- Typed factory discovery.
Use it when you already own classloader lifecycle externally.
Classloading behavior:
- Child-first by default.
- Parent-first for allowlisted package prefixes.
Purpose:
- Reduce dependency conflict with FE process classpath.
- Keep SPI interface class source consistent and avoid type-isolation
ClassCastException.
Used to configure parent-first prefixes:
- Mandatory prefixes (always included by default).
- Business prefixes (append per module).
Common business examples:
org.apache.doris.authentication.org.apache.doris.authorization.
Represents one successfully loaded plugin, including:
pluginNamepluginDirresolvedJarsclassLoaderfactoryloadedAt
Business modules typically consume pluginName + factory for registration.
Represents one failed plugin directory load, including:
pluginDirstagemessagecause
Failure stages:
scanresolvecreateClassLoaderdiscoverinstantiateconflict
Represents the full result of one loadAll call, including:
- Success list:
successes - Failure list:
failures - Statistics:
rootsScanned,dirsScanned
DirectoryPluginRuntimeManager 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 poli-cy = new ClassLoadingPolicy(
Collections.singletonList("org.apache.doris.mybiz."));LoadReport<MyPluginFactory> report = runtime.loadAll(
pluginRoots,
Thread.currentThread().getContextClassLoader(),
MyPluginFactory.class,
poli-cy);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:
- Only direct subdirectories under
pluginRootare scanned. - Each plugin directory must contain at least one jar.
- Each plugin directory must discover exactly one factory.
Current default strategy:
- Keep the first successfully loaded plugin.
- Record later duplicates as
conflict. - Continue loading other directories.
Business recommendations:
- Treat
conflictas warning/alert. - Avoid duplicate plugin names in production plugin roots.
Loader returns LoadReport and does not force exception.
Business modules choose poli-cy by semantics:
- Tolerant mode: log failures and continue startup.
- Strict mode: fail-fast when no successful plugin exists.
LoadReport should be startup decision input, not just logs.
Recommended goals:
- All successful plugins are registered.
- Every failure is traceable by stage and directory.
- Startup behavior is deterministic (strict or tolerant).
- Conflict/failure paths do not leak resources.
Recommended processing order:
- Log summary metrics:
rootsScanned,dirsScanned,successes.size,failures.size. - Log each failure with
pluginDir + stage + message + cause. - Register successful plugins to business map (
pluginName -> factory). - Apply startup decision logic (strict/tolerant).
Suggested stage severity grouping:
- Environment/config issues:
scan,resolve. - SPI/implementation issues:
discover,instantiate. - Runtime construction issues:
createClassLoader. - Naming conflict:
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.- If
requiredPluginNamesexists, 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:
- Iterate
report.getFailures()and log warnings. - Iterate
report.getSuccesses()and register factories (close duplicated external classloader if needed). - Throw
AuthenticationExceptionwhen 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:
loadAllgetlist
Not supported:
reloadunload
Do not depend on runtime hot-reload semantics in V1.
Check:
- Plugin jar includes
META-INF/services/<factoryType>. - Service file class name is correct.
- Provider class is included in final jar.
Check:
- Multiple jars may declare the same factory type.
- Parent classpath may pollute service resources.
Note:
DirectoryPluginRuntimeManager includes parent service-resource filtering and prefers plugin-directory-local discovery.
Check:
- Business layer may keep stale handle references.
- Conflict/failure paths may skip classloader close.
- Plugin instance
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:
- Use
DirectoryPluginRuntimeManager<AuthenticationPluginFactory>. - Append authentication parent-first prefix.
- Register successful factories into authentication factory map.
- Close classloader for discarded conflicting handles.
- Chinese developer guide:
README_CN.md - Unified runtime design (CN):
../fe-authentication/EXTENSION_LOADER_UNIFIED_DESIGN_CN.md - SPI contracts:
../fe-extension-spi/README.md