Config Initialization — Eliminate Reflection in Config Loading
Context
YamlConfigLoaderUtils.copyProperties() uses Field.setAccessible(true) +
field.set() to populate ModuleConfig objects from YAML properties. This
reflection pattern is problematic for GraalVM native image — every config field
would require reflect-config.json entries, and setAccessible() is restricted.
Since our module/provider set is fixed (37 modules, see distro-policy.md), we can generate hardcoded field-setting code at build time that eliminates all config-related reflection.
Problem
In ModuleDefine.prepare() and in BanyanDBConfigLoader,
copyProperties() iterates property names, looks up fields by name via
reflection, and sets them:
Field field = getDeclaredField(destClass, propertyName);
field.setAccessible(true); // restricted in native image
field.set(dest, value); // needs reflect-config.json
This requires:
getDeclaredField()with class hierarchy walkfield.setAccessible(true)to bypass private accessfield.set()for every property
Solution: Same-FQCN Replacement of YamlConfigLoaderUtils
Generate a replacement YamlConfigLoaderUtils.java with the same FQCN
(org.apache.skywalking.oap.server.library.util.YamlConfigLoaderUtils) that
dispatches by config object type and sets fields directly — no
Field.setAccessible(), no getDeclaredField() scan.
This is one of 23 same-FQCN replacement classes in the distro (see distro-policy.md for full list).
Field Access Strategy (per field)
| Strategy | Condition | Example |
|---|---|---|
| Lombok setter | All non-final fields (class-level @Setter added via -for-graalvm modules) |
cfg.setRole((String) value) |
| Getter + clear + addAll | Final collection field | cfg.getDownsampling().clear(); cfg.getDownsampling().addAll((List) value) |
| Error | Unknown config type | throw new IllegalArgumentException("Unknown config type: ...") |
All config classes that previously lacked @Setter now have it added via
same-FQCN replacement classes in the -for-graalvm modules. No VarHandle, no
reflection fallback. The generator fails at build time if any non-final field
lacks a setter.
Generated Code Structure
package org.apache.skywalking.oap.server.library.util;
public class YamlConfigLoaderUtils {
// No VarHandle, no reflection. Pure setter-based.
// Type-dispatch: check instanceof, delegate to type-specific method
public static void copyProperties(Object dest, Properties src,
String moduleName, String providerName)
throws IllegalAccessException {
if (dest instanceof CoreModuleConfig) {
copyToCoreModuleConfig((CoreModuleConfig) dest, src, moduleName, providerName);
} else if (dest instanceof ClusterModuleKubernetesConfig) {
copyToClusterModuleKubernetesConfig((ClusterModuleKubernetesConfig) dest, src, moduleName, providerName);
}
// ... all config types ...
else {
throw new IllegalArgumentException("Unknown config type: " + dest.getClass().getName());
}
}
// Per-type: switch on property name, set via Lombok setter
private static void copyToCoreModuleConfig(
CoreModuleConfig cfg, Properties src,
String moduleName, String providerName) {
// iterate properties
switch (key) {
case "role":
cfg.setRole((String) value);
break;
case "persistentPeriod":
cfg.setPersistentPeriod((int) value);
break;
// ... all fields via setters ...
default:
log.warn("{} setting is not supported in {} provider of {} module",
key, providerName, moduleName);
break;
}
}
// ... one method per config type ...
}
Build Tool
Module: build-tools/config-generator
Main class: ConfigInitializerGenerator.java
Input
- Classpath with all SkyWalking module JARs (same dependencies as
oap-graalvm-server) - Provider class list — derived from the fixed module table in
GraalVMOAPServerStartUp - Extra config class list — BanyanDB nested configs used by
BanyanDBConfigLoader
Process
- For each provider, call
newConfigCreator().type()to discover the config class - For each config class, use Java reflection to scan all declared fields
(walking up to
ModuleConfigsuperclass) - For each field, check if a setter method exists (
set+ capitalize(name)) - Fail if any non-final field lacks a setter (all config classes must have
@Setter) - Generate
YamlConfigLoaderUtils.javawith type-dispatch + switch-based field assignment
The generator depends on -for-graalvm modules (not upstream JARs) so it sees
config classes with @Setter added. Original upstream JARs are forced to
provided scope to prevent classpath shadowing.
Output
oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/library/util/YamlConfigLoaderUtils.java(same-FQCN replacement)
Running the generator
JAVA_HOME=/Users/wusheng/.sdkman/candidates/java/25-graal \
mvn -pl build-tools/config-generator exec:java \
-Dexec.args="oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/library/util/YamlConfigLoaderUtils.java"
Config Classes Inventory
37 modules registered in GraalVMOAPServerStartUp. 1 (AlarmModule) has null
ConfigCreator. The rest need coverage.
Provider Config Classes
| Provider | Config Class | Fields | Setter Status |
|---|---|---|---|
| CoreModuleProvider | CoreModuleConfig |
~40 | @Getter class-level, @Setter on ~12 fields, rest need VarHandle |
| BanyanDBStorageProvider | BanyanDBStorageConfig |
~5 | @Getter @Setter |
| ClusterModuleStandaloneProvider | (empty config) | 0 | — |
| ClusterModuleKubernetesProvider | ClusterModuleKubernetesConfig |
3 | @Setter |
| ConfigmapConfigurationProvider | ConfigmapConfigurationSettings |
3 | @Setter |
| PrometheusTelemetryProvider | PrometheusConfig |
5 | @Setter |
| AnalyzerModuleProvider | AnalyzerModuleConfig |
~10 | @Getter @Setter |
| LogAnalyzerModuleProvider | LogAnalyzerModuleConfig |
2 | @Setter |
| SharingServerModuleProvider | SharingServerConfig |
~15 | Needs check |
| EnvoyMetricReceiverProvider | EnvoyMetricReceiverConfig |
~15 | Needs check |
| KafkaFetcherProvider | KafkaFetcherConfig |
~20 | @Data (all setters) |
| ZipkinReceiverProvider | ZipkinReceiverConfig |
~20 | Needs check |
| GraphQLQueryProvider | GraphQLQueryConfig |
4 | @Setter |
| HealthCheckerProvider | HealthCheckerConfig |
1 | @Setter |
| + others | (simple configs) | 0-5 | Various |
BanyanDB Config Loading (BanyanDBConfigLoader)
BanyanDBConfigLoader (in storage-banyandb-plugin) loads config from bydb.yml
and bydb-topn.yml independently of the standard YAML config loading path. It
calls copyProperties() on nested config objects, so the generated
YamlConfigLoaderUtils must handle all BanyanDB inner classes.
loadBaseConfig() — reads bydb.yml, calls copyProperties() 11 times:
| Call target | Type | Handler | Fields |
|---|---|---|---|
config.getGlobal() |
Global |
copyToGlobal() |
18 (targets, maxBulkSize, flushInterval, …) |
config.getRecordsNormal() |
RecordsNormal |
copyToRecordsNormal() |
8 (inherited from GroupResource) |
config.getRecordsLog() |
RecordsLog |
copyToRecordsLog() |
8 |
config.getTrace() |
Trace |
copyToTrace() |
8 |
config.getZipkinTrace() |
ZipkinTrace |
copyToZipkinTrace() |
8 |
config.getRecordsBrowserErrorLog() |
RecordsBrowserErrorLog |
copyToRecordsBrowserErrorLog() |
8 |
config.getMetricsMin() |
MetricsMin |
copyToMetricsMin() |
8 |
config.getMetricsHour() |
MetricsHour |
copyToMetricsHour() |
8 |
config.getMetricsDay() |
MetricsDay |
copyToMetricsDay() |
8 |
config.getMetadata() |
Metadata |
copyToMetadata() |
8 |
config.getProperty() |
Property |
copyToProperty() |
8 |
copyStages() — creates warm/cold Stage objects, calls copyProperties() on each:
| Call target | Type | Handler | Fields |
|---|---|---|---|
| warm Stage | Stage |
copyToStage() |
7 (name, nodeSelector, shardNum, segmentInterval, ttl, replicas, close) |
| cold Stage | Stage |
copyToStage() |
7 |
loadTopNConfig() — reads bydb-topn.yml, populates TopN objects via direct
setter calls (topN.setName(), topN.setGroupByTagNames(), etc.). Does NOT call
copyProperties() — no handler needed.
Class hierarchy note: All group config classes (RecordsNormal, Trace,
MetricsMin, etc.) extend GroupResource. The generator walks the class hierarchy
from each subclass up to GroupResource and generates setter calls for all 8
inherited fields (shardNum, segmentInterval, ttl, replicas,
enableWarmStage, enableColdStage, defaultQueryStages,
additionalLifecycleStages). Trace doesn’t have its own @Getter @Setter but
inherits all setters from GroupResource.
instanceof ordering: Inner classes (Global, RecordsNormal, Trace, etc.)
are static inner classes that do NOT extend BanyanDBStorageConfig, so the
instanceof BanyanDBStorageConfig check matches only the top-level config. Inner
class checks come after and work independently.
Two extra inner classes (RecordsTrace, RecordsZipkinTrace) are included in
EXTRA_CONFIG_CLASSES for completeness, even though BanyanDBConfigLoader doesn’t
currently call copyProperties() on them.
Same-FQCN Replacements (Config Initialization)
| Upstream Class | Upstream Location | Replacement Location | What Changed |
|---|---|---|---|
YamlConfigLoaderUtils |
server-library/library-util/.../util/YamlConfigLoaderUtils.java |
oap-graalvm-server/ (not in library-util-for-graalvm due to 30+ cross-module imports) |
Complete rewrite. Uses type-dispatch with Lombok @Setter methods instead of Field.setAccessible() + field.set(). |
CoreModuleConfig |
server-core/.../core/CoreModuleConfig.java |
oap-libs-for-graalvm/server-core-for-graalvm/ |
Added @Setter at class level. Upstream only has @Getter. |
AnalyzerModuleConfig |
analyzer/agent-analyzer/.../provider/AnalyzerModuleConfig.java |
oap-libs-for-graalvm/agent-analyzer-for-graalvm/ |
Added @Setter at class level. |
LogAnalyzerModuleConfig |
analyzer/log-analyzer/.../provider/LogAnalyzerModuleConfig.java |
oap-libs-for-graalvm/log-analyzer-for-graalvm/ |
Added @Setter at class level. |
EnvoyMetricReceiverConfig |
server-receiver-plugin/envoy-metrics-receiver-plugin/.../EnvoyMetricReceiverConfig.java |
oap-libs-for-graalvm/envoy-metrics-receiver-for-graalvm/ |
Added @Setter at class level. |
OtelMetricReceiverConfig |
server-receiver-plugin/otel-receiver-plugin/.../OtelMetricReceiverConfig.java |
oap-libs-for-graalvm/otel-receiver-for-graalvm/ |
Added @Setter at class level. |
EBPFReceiverModuleConfig |
server-receiver-plugin/skywalking-ebpf-receiver-plugin/.../EBPFReceiverModuleConfig.java |
oap-libs-for-graalvm/ebpf-receiver-for-graalvm/ |
Added @Setter at class level. |
AWSFirehoseReceiverModuleConfig |
server-receiver-plugin/aws-firehose-receiver/.../AWSFirehoseReceiverModuleConfig.java |
oap-libs-for-graalvm/aws-firehose-receiver-for-graalvm/ |
Added @Setter at class level. |
CiliumFetcherConfig |
server-fetcher-plugin/cilium-fetcher-plugin/.../CiliumFetcherConfig.java |
oap-libs-for-graalvm/cilium-fetcher-for-graalvm/ |
Added @Setter at class level. |
StatusQueryConfig |
server-query-plugin/status-query-plugin/.../StatusQueryConfig.java |
oap-libs-for-graalvm/status-query-for-graalvm/ |
Added @Setter at class level. |
HealthCheckerConfig |
server-health-checker/.../HealthCheckerConfig.java |
oap-libs-for-graalvm/health-checker-for-graalvm/ |
Added @Setter at class level. |
Config replacements (except YamlConfigLoaderUtils) are repackaged into their respective -for-graalvm modules via maven-shade-plugin. YamlConfigLoaderUtils lives in oap-graalvm-server because it imports types from 30+ modules; the original .class is excluded from library-util-for-graalvm via shade filter.
Same-FQCN Packaging
The original YamlConfigLoaderUtils.class is excluded from the library-util-for-graalvm shaded JAR. The replacement in oap-graalvm-server.jar is the only copy on the classpath.
Verification
# Run generator
JAVA_HOME=/Users/wusheng/.sdkman/candidates/java/25-graal \
mvn -pl build-tools/config-generator exec:java \
-Dexec.args="oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/library/util/YamlConfigLoaderUtils.java"
# Full build (compile + test + package)
JAVA_HOME=/Users/wusheng/.sdkman/candidates/java/25-graal make build-distro