### What problem does this PR solve? #66211 Problem Summary: The authentication plugin API-version gate rejects every plugin jar that does not contain `Doris-Authentication-Plugin-Api-Version`. Authentication plugins were distributed before this manifest contract existed, so upgrading FE breaks those legacy artifacts even when they implement the current v1 API. This change adds an opt-in legacy mapping to the generic API gate and configures only the authentication family to treat a genuinely absent declaration as API 1.0: - unversioned legacy authentication plugins load with an operator warning; - empty, malformed, and explicitly incompatible declarations remain rejected; - connector, filesystem, lineage, and other strict gates keep their existing fail-closed behavior; - when the FE authentication API moves to another major, legacy v1 plugins are rejected automatically.
fe-extension-loader Developer Integration Guide
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.
1. Module Positioning
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.
2. Applicable Scenarios
2.1 Good Fit For Loader
Use Loader if your module needs:
- External plugin loading from directories.
- Shared loading framework across multiple business modules.
- Standardized failure semantics (
scan,resolve,discover, etc.). - Less duplicated runtime loading code.
2.2 Not A Good Fit For Loader
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.
3. Core Class Guide
3.1 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.
3.2 Core Flow Inside DirectoryPluginRuntimeManager
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.
3.3 PluginLoader
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.
3.4 ChildFirstClassLoader
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.
3.5 ClassLoadingPolicy
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.
3.6 PluginHandle<F>
Represents one successfully loaded plugin, including:
pluginNamepluginDirresolvedJarsclassLoaderfactoryloadedAt
Business modules typically consume pluginName + factory for registration.
3.7 LoadFailure
Represents one failed plugin directory load, including:
pluginDirstagemessagecause
Failure stages:
scanresolvecreateClassLoaderdiscoverinstantiateconflict
3.8 LoadReport<F>
Represents the full result of one loadAll call, including:
- Success list:
successes - Failure list:
failures - Statistics:
rootsScanned,dirsScanned
3.9 Runtime Handle Storage (Built-In)
DirectoryPluginRuntimeManager stores loaded handles in an internal concurrent map.
No separate PluginRuntimeRegistry abstraction is exposed in current implementation.
4. Standard Integration Steps
4.1 Prepare Typed Factory
Your business factory interface should extend PluginFactory.
4.2 Initialize Runtime Manager
DirectoryPluginRuntimeManager<MyPluginFactory> runtime =
new DirectoryPluginRuntimeManager<>();
4.3 Configure Classloading Policy
ClassLoadingPolicy policy = new ClassLoadingPolicy(
Collections.singletonList("org.apache.doris.mybiz."));
4.4 Call loadAll
LoadReport<MyPluginFactory> report = runtime.loadAll(
pluginRoots,
Thread.currentThread().getContextClassLoader(),
MyPluginFactory.class,
policy);
4.5 Process Load Result
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());
}
5. Directory Convention
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.
6. Conflict And Failure Handling
6.1 Duplicate Plugin Name
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.
6.2 Should All Failures Throw
Loader returns LoadReport and does not force exception.
Business modules choose policy by semantics:
- Tolerant mode: log failures and continue startup.
- Strict mode: fail-fast when no successful plugin exists.
6.3 Detailed LoadReport Handling Strategy
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.
6.4 Copy-And-Adapt Template
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
6.5 Authentication Module Mapping
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
7. Current Runtime Scope (V1)
Supported:
loadAllgetlist
Not supported:
reloadunload
Do not depend on runtime hot-reload semantics in V1.
8. Frequently Asked Questions
8.1 Factory Not Found
Check:
- Plugin jar includes
META-INF/services/<factoryType>. - Service file class name is correct.
- Provider class is included in final jar.
8.2 Multiple Factories Found
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.
8.3 Loaded Plugin Not Released
Check:
- Business layer may keep stale handle references.
- Conflict/failure paths may skip classloader close.
- Plugin instance
close()may not release resources.
9. Integration With Authentication Module
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.
Related Docs
- 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