Skip to content

refactor(config): replace manual parsing with ConfigBeanFactory binding#6615

Open
vividctrlalt wants to merge 5 commits intotronprotocol:developfrom
vividctrlalt:refactor/config-bean-binding
Open

refactor(config): replace manual parsing with ConfigBeanFactory binding#6615
vividctrlalt wants to merge 5 commits intotronprotocol:developfrom
vividctrlalt:refactor/config-bean-binding

Conversation

@vividctrlalt
Copy link
Copy Markdown
Contributor

What

Replace ~850 lines of manual if (config.hasPath(KEY)) / getXxx blocks in
Args.applyConfigParams() with ConfigBeanFactory.create() automatic binding.
Each config.conf domain now maps to a typed Java bean class. Delete ConfigKey.java
(~100 string constants) — config.conf becomes the sole source of truth for key names.

Why

Adding a new config parameter previously required editing 3 files (config.conf,
ConfigKey.java, Args.java). The manual parsing was error-prone, hard to review,
and duplicated every key name as a Java string constant. With bean binding, adding
a parameter only requires adding a field to the bean class and a line in reference.conf.

Introducing reference.conf

Previously, default values were scattered across three places: bean field initializers,
ternary expressions in Args.java, and comments in ConfigKey.java. Defaults could
be inconsistent, and there was no single place to see all config parameters and their
default values at a glance.

reference.conf is the official recommended practice of the Typesafe Config library
(see official docs):
libraries and applications declare all config parameters and their defaults in
src/main/resources/reference.conf, and users only need to override the values they
want to change — Typesafe Config merges them automatically.
Akka, Play Framework, and Spark all follow this convention.

Benefits:

  • Single source of defaults: all default values centralized in one file, eliminating
    scatter and inconsistency
  • Self-documenting: opening reference.conf shows the complete list of config
    parameters, defaults, and comments — no need to dig through Java code
  • ConfigBeanFactory safety net: bean binding requires every field to have a
    corresponding config key. reference.conf ensures binding never fails due to
    missing keys, even when users don't configure a value
  • Community convention: projects using Typesafe Config naturally expect
    reference.conf to exist, lowering the learning curve for new contributors

Changes

Commit 1: Core refactor — ConfigBeanFactory binding

  • Create typed bean classes for all config domains: VmConfig, BlockConfig,
    CommitteeConfig, MetricsConfig, NodeConfig, EventConfig, StorageConfig,
    GenesisConfig, RateLimiterConfig, MiscConfig, LocalWitnessConfig
  • Simplify Args.applyConfigParams() from ~850 lines to ~50 lines of domain binding calls
  • Delete ConfigKey.java (~100 string constants)
  • Migrate Storage.java static getters to read from StorageConfig bean
  • Add unit tests for all config bean classes

Commit 2: reference.conf as single source of defaults

  • Add common/src/main/resources/reference.conf with defaults for all config domains
  • Move scattered default values from bean field initializers into reference.conf
  • Expose config beans as static singletons (NodeConfig.getInstance() etc.)
  • Auto-bind discovery, PBFT, and list fields in NodeConfig

Commit 3: Storage cleanup

  • Move default/defaultM/defaultL LevelDB option reading from Storage into
    StorageConfig, so Storage no longer touches Config directly
  • Add DbOptionOverride with nullable boxed types for partial override semantics
  • Fix cacheSize type from int to long to match LevelDB Options API

Scope

  • Only changes the reading side: how config.conf values are parsed into Java objects
  • Does NOT move fields out of CommonParameter (future Phase 2)
  • Does NOT change config.conf format or CLI parameter handling
  • Does NOT modify the 847 CommonParameter.getInstance() call sites

Future Plan

This refactor is Phase 1 — it only replaces the config reading layer. After bean
binding, values are still copied one by one to CommonParameter's flat fields, because
847 call sites across the codebase depend on CommonParameter.getInstance().

The goal of Phase 2 is to remove the CommonParameter intermediary:

  1. Untangle CLI parameter interference: some config parameters are currently written
    by both config.conf and CLI arguments (JCommander), converging on CommonParameter.
    The CLI override logic needs to be unified into Typesafe Config's override mechanism
    (ConfigFactory.systemProperties() or custom overrides), eliminating CLI's direct
    write dependency on CommonParameter
  2. Gradually migrate call sites: replace 847 CommonParameter.getInstance().getXxx()
    calls with direct domain bean access — NodeConfig.getInstance().getXxx(),
    VmConfig.getInstance().getXxx(), etc.
  3. Delete CommonParameter: once all call sites are migrated and CLI arguments no
    longer write directly, remove CommonParameter and the bridge-copy code in Args

End state: config.confreference.conf fallback → ConfigBeanFactory → domain
bean singletons. Fully type-safe, no intermediary layer, no string constants.

Test

  • All existing tests pass (framework full test suite)
  • Added 12 new test classes covering all config bean domains
  • Checkstyle passes

vividctrlalt added a commit to vividctrlalt/system-test that referenced this pull request Apr 1, 2026
HOCON null values cannot be bound by ConfigBeanFactory to String fields.
Use empty string instead, which has the same effect (triggers the
automatic IP detection fallback in java-tron).

Related: tronprotocol/java-tron#6615
Replace ~850 lines of manual if/hasPath/getXxx blocks in Args.applyConfigParams()
with ConfigBeanFactory.create() automatic binding. Each config.conf domain now maps
to a typed Java bean class (VmConfig, BlockConfig, CommitteeConfig, MetricsConfig,
NodeConfig, EventConfig, StorageConfig, etc.).

- Delete ConfigKey.java (~100 string constants), config.conf is sole source of truth
- Migrate Storage.java static getters to read from StorageConfig bean
- Add unit tests for all config bean classes
- Migrate DynamicArgs to use bean binding
Move all default values from scattered bean field initializers into
reference.conf, making it the single source of truth for config defaults.
Expose config beans as static singletons for convenient access.

- Add comprehensive reference.conf with defaults for all config domains
- Auto-bind discovery, PBFT, and list fields in NodeConfig
- Expose config beans as static singletons (NodeConfig.getInstance() etc.)
- Move postProcess logic into bean classes
- Fix test configs (external.ip=null -> empty string)
- Document manual-read keys with reasons in reference.conf
…Config

Move default/defaultM/defaultL LevelDB option reading into StorageConfig,
so Storage no longer touches Config directly.

- Add DbOptionOverride with nullable boxed types for partial overrides
- Fix cacheSize type from int to long to match LevelDB Options API
- Remove dead externalIp(Config) bridge method
- Remove setIfNeeded and Config field from Storage
- Replace null values (discovery.external.ip, trustNode) with empty
  string before ConfigBeanFactory binding for external config compat
  (system-test uses "external.ip = null" which ConfigBeanFactory cannot
  bind to String fields; recommend updating system-test to use "" instead)
- Fix floating point comparison with Double.compare (java:S1244)
- Extract duplicated string literals into constants/variables (java:S1192)
@@ -79,7 +80,11 @@ private void updateActiveNodes(Config config) {
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DynamicArgs: NodeConfig constructed twice per reload — wrong abstraction boundary

private void updateActiveNodes(Config config) {
    NodeConfig nodeConfig = NodeConfig.fromConfig(config);  // parse #1
    ...
}

private void updateTrustNodes(Config config) {
    NodeConfig nodeConfig = NodeConfig.fromConfig(config);  // parse #2 — same Config object
    ...
}

Beyond the redundant ConfigBeanFactory reflection cost, there is a deeper engineering issue:

Broken single source of truth. Both methods are steps of one atomic reload, yet they produce two separate NodeConfig instances from the same input. The code gives no guarantee they reflect the same state — any future side effect or environment-sensitive substitution in fromConfig() could cause them to silently diverge. The root cause: Config is the wrong parameter type here. Both methods''' actual contract is NodeConfig.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: DynamicArgs NodeConfig parsed twice — Fixed. NodeConfig is now parsed once in reload() and passed to both methods.

@317787106
Copy link
Copy Markdown
Collaborator

Unused files these related to configuration common/src/main/java/org/tron/core/config/README.md and common/src/main/java/org/tron/core/config/args/Overlay.java can be deleted.

* All getXxxFromConfig methods now read from StorageConfig bean instead of
* manual string constants. Signatures preserved for backward compatibility.
*/

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including getDbEngineFromConfig, a total of 12 methods are no longer used and can be removed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: Storage 12 unused methods — Fixed. All removed, confirmed zero callers.

private static final String PBFT_EXPIRE_NUM_KEY = "pBFTExpireNum";
private static final String ALLOW_PBFT_KEY = "allowPBFT";

public static CommitteeConfig fromConfig(Config config) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest to replace class-level @Getter/@Setter with per-field annotations in CommitteeConfig.

The previous approach used class-level annotations but silently suppressed them on allowPBFT and pBFTExpireNum via AccessLevel.NONE. This is misleading — a reader assumes all fields are covered until they scan the entire file to find the exceptions. The code has poor readability.

Per-field annotations make each field's contract explicit. The two non-standard fields carry no Lombok annotations at all, so their manual accessors immediately signal that special handling is required.

There is no need to modify the configuration parameter name solely due to case differences.

Lombok best practice: use class-level annotations only when the policy is uniform across all fields; switch to per-field when exceptions exist.

Suggest to optimize like this concisely:

//no Getter Setter
@Slf4j
public class CommitteeConfig {

  @Getter @Setter private long allowCreationOfContracts = 0;
  ...
  @Getter @Setter private long changedDelegation = 0;

  // These two fields which violates JavaBean naming convention are excluded from auto-binding and
  // handled manually in fromConfig().
  @Getter private long allowPBFT = 0;
  @Getter private long pBFTExpireNum = 20;

  @Getter @Setter private long allowTvmFreeze = 0;
   ...
  @Getter @Setter private long dynamicEnergyMaxFactor = 0;

  // proposalExpireTime is NOT a committee field — it's in block.* and handled by BlockConfig

  // Defaults come from reference.conf (loaded globally via Configuration.java)

  private static final String PBFT_EXPIRE_NUM_KEY = "pBFTExpireNum";
  private static final String ALLOW_PBFT_KEY = "allowPBFT";

  public static CommitteeConfig fromConfig(Config config) {
    Config section = config.getConfig("committee");

    CommitteeConfig cc = ConfigBeanFactory.create(section, CommitteeConfig.class);
    // Ensure the manually-named fields get the right values from the original keys
    cc.allowPBFT = section.hasPath(ALLOW_PBFT_KEY) ? section.getLong(ALLOW_PBFT_KEY) : 0;
    cc.pBFTExpireNum = section.hasPath(PBFT_EXPIRE_NUM_KEY)
        ? section.getLong(PBFT_EXPIRE_NUM_KEY) : 20;

    cc.postProcess();
    return cc;
  }

  private void postProcess() {
  ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: CommitteeConfig per-field @Getter/https://github.com/Setter — Good suggestion for readability. However, the root cause is that allowPBFT and pBFTExpireNum violate JavaBean naming convention (consecutive uppercase "PBFT"). Switching annotation style is a cosmetic fix.

Since PBFT is not yet active, I'd suggest addressing this in a future version by renaming the config keys to standard camelCase (allowPbft, pbftExpireNum). That eliminates the need for manual binding entirely, and the annotation style issue goes away with it. The same approach should apply to other beans (NodeConfig, StorageConfig, EventConfig) that have similar AccessLevel.NONE workarounds.

+ "committee.allowNewRewardAlgorithm = 1"
+ " or committee.allowNewReward = 1"
+ " or committee.allowTvmVote = 1.");
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should throws TronError instead of IllegalArgumentException here, as the IllegalArgumentException here appears to be an oversight and may be silently swallowed by an upstream catch block, causing the node to start with an invalid committee configuration instead of exiting immediately.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that TronError is the better choice semantically. However, this PR is a pure refactor — the original code in Args.java used IllegalArgumentException here, so I kept it unchanged to avoid behavioral changes.

Also, in the current call stack (FullNode.main → Args.setParam → applyConfigParams → CommitteeConfig.fromConfig → postProcess), there is no catch(Exception) that would swallow it. The IllegalArgumentException propagates to the UncaughtExceptionHandler and exits the node correctly.

That said, unifying all config validation errors to TronError would be a good follow-up.

influxdb {
ip = ""
port = 8086
database = ""
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It missing default value is "metrics" here, it should be same as that in code,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Changed to database = "metrics" to match MetricsConfig.InfluxDbConfig default.

@vividctrlalt
Copy link
Copy Markdown
Contributor Author

Good catch. Both files are indeed unused:

  • Overlay.java — created in 2018, never assigned in Args.java. CommonParameter.overlay is always null. However, deleting it requires also removing the field from CommonParameter, OverlayTest.java, and the assertNull in ParameterTest.java.
  • README.md — an outdated (2018) config doc placed inside the Java source tree. Content is stale and the location is wrong (gets packaged into the jar).

Both are out of scope for this PR (config binding refactor). I'd suggest a separate cleanup PR to remove them along with any other dead config artifacts.

@kuny0707 kuny0707 requested a review from halibobo1205 April 6, 2026 08:46
- DynamicArgs: parse NodeConfig once in reload(), pass to both methods
- Storage: remove 12 unused getXxxFromConfig static methods
- reference.conf: fix influxdb database default to "metrics"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants