DEV Community

Solon Framework
Solon Framework

Posted on

Solon.cfg() Everything: The Programmatic Config Center Behind SolonProps — Typed Getters, Prefix Groups, and Bean Binding

Most frameworks make you read config the same way: sprinkle @Value annotations everywhere, or grab a string from Environment and parse it yourself every single time. It works, but the code ends up with a hundred tiny, untestable couplings to a file somewhere.

Solon's answer is different. Config is not scattered across annotations — it lives in one place, and you can reach it from anywhere in your app. That place is SolonProps, the application's property collection (a.k.a. the config center). You grab it with a single static call:

SolonProps props = Solon.cfg();
Enter fullscreen mode Exit fullscreen mode

That's it. No injection, no context lookup. Solon.cfg() is available anywhere, including in static contexts and utility classes. This post walks through the API surface you'll actually use — typed getters, prefix grouping, bean conversion, expression/template lookup, and dynamic loading.

1. Typed getters — no more manual parsing

The most common thing you do with config is read a value. SolonProps gives you typed shortcuts so you never hand-parse a string:

String name  = props.get("demo.name");              // String, or null
String port  = props.get("demo.port", "8080");      // with default
int    max   = props.getInt("demo.max", 100);       // int
long   ttl   = props.getLong("demo.ttl", 3000L);    // long
double ratio = props.getDouble("demo.ratio", 0.5);  // double
boolean on   = props.getBool("demo.on", false);     // boolean
Enter fullscreen mode Exit fullscreen mode

There's also getByKeys("demo.url", "demo.url2") which returns the first non-null value across several keys — handy when you're migrating config keys and want a graceful fallback.

2. Prefix grouping — get a whole section at once

This is where it gets interesting. Spring has @ConfigurationProperties(prefix=...), but Solon does it at runtime without any annotation. Pass a key prefix and you get back a fresh Props collection with just that section:

// app.yml
// demo.db:
//   url: jdbc:mysql://localhost:3306/demo
//   user: root
//   maxPool: 8

Props db = props.getProp("demo.db");
String url = db.get("url");
int pool   = db.getInt("maxPool", 4);
Enter fullscreen mode Exit fullscreen mode

Three variants cover different shapes:

  • getProp(keyStarts) → a single Props (one section)
  • getGroupedProp(keyStarts)Map<String, Props> (multi-instance sections like demo.db1, demo.db2)
  • getListedProp(keyStarts)Map<String, Props> (list-style indexed sections)

So multi-datasource or multi-client configs don't need custom parsing code. The framework splits them for you.

3. Bean conversion — config straight into objects

Instead of reading keys one by one, bind an entire prefix to a POJO:

// demo.mail:
//   host: smtp.example.com
//   port: 587
//   from: no-reply@example.com

public class MailConfig {
    public String host;
    public int    port;
    public String from;
}

MailConfig cfg = props.toBean("demo.mail", MailConfig.class);
Enter fullscreen mode Exit fullscreen mode

Or bind the whole property set to an existing bean instance:

MailConfig cfg = new MailConfig();
props.bindTo(cfg);   // matches keys against bean fields
Enter fullscreen mode Exit fullscreen mode

toBean(Class<T>) converts the entire collection; toBean(prefix, Class<T>) scopes it to a prefix. bindTo(bean) mutates an existing instance. No reflection boilerplate on your side.

4. Expression & template lookup

Config values can reference each other (Solon resolves ${...} inside values at load time), but you can also do it on demand:

// expr:  ${key} | key | ${key:def} | key:def
String v1 = props.getByExpr("demo.db.url");          // plain key
String v2 = props.getByExpr("${demo.db.url}");       // explicit expr
String v3 = props.getByExpr("${demo.db.port:3306}"); // with default

// tmpl:  ${key} | aaa${key}bbb | ${key:def}/ccc
String t1 = props.getByTmpl("jdbc:${demo.db.url}/db"); // template
Enter fullscreen mode Exit fullscreen mode

getByExpr is for single expressions, getByTmpl for embedding values inside larger strings. Both respect :def defaults.

5. Loading & extending at runtime

Solon apps load app.yml automatically, but you can pull in more files at any time:

props.loadAdd("classpath:demo-ext.yml");        // from classpath
props.loadAdd(new URL("file:/etc/app/extra.yml")); // from URL
props.loadAdd(System.getProperties());          // merge JVM props
Enter fullscreen mode Exit fullscreen mode

There's also loadAddIfAbsent(...) for incremental loading that won't clobber existing keys — useful for layering environment overrides. And if you build a plugin that needs to contribute config, loadAdd(Properties) plus plugs() (the plugin config collection) are your integration points.

6. Environment variables — bridge 12-factor style

Kubernetes, Docker, and CI all prefer env vars over files. Pull them into your props with a prefix filter:

props.loadEnv("DEMO_");                          // all vars starting with DEMO_
props.loadEnv(k -> k.contains("SOLON"));         // custom predicate
Enter fullscreen mode Exit fullscreen mode

Loaded vars become normal props, readable through all the typed getters above. One line, no third-party dotenv library.

7. App & server metadata

The framework exposes its own identity as typed getters — useful in logs, health checks, and admin pages:

props.appName();        // solon.app.name
props.appGroup();       // solon.app.group
props.appNamespace();   // solon.app.namespace
props.appTitle();       // solon.app.title
props.appEnabled();     // solon.app.enabled (health)
props.locale();         // solon.locale

props.serverPort();     // server.port
props.serverHost();     // server.host
props.serverContextPath(); // server.contextPath
Enter fullscreen mode Exit fullscreen mode

8. Runtime mode checks

Instead of passing flags around, ask the config center directly:

if (props.isDebugMode())      { /* dev-time behavior */ }
if (props.isSetupMode())      { /* first-run setup     */ }
if (props.isFilesMode())      { /* running from files  */ }
if (props.isDriftMode())      { /* solon cloud drift   */ }
if (props.isWhiteMode())      { /* security whitelist  */ }
if (props.testing())          { /* under unit test     */ }
if (props.isEnabledVirtualThreads()) { /* JDK 21+        */ }
Enter fullscreen mode Exit fullscreen mode

9. One subtle gotcha: removing a key

Because props are synced to System.getProperties() at load time, removing a key requires a double delete:

props.remove("redis.onOff");
System.getProperties().remove("redis.onOff");
Enter fullscreen mode Exit fullscreen mode

Forget the second line and the key can still surface through System.getProperties() lookups. It's the one place where the two-way sync shows its face.

Where it fits compared to Spring

  • @Value("${x}")props.get("x") or the typed getters — no annotation needed, works in any context including static methods
  • Environment.getProperty("x")Solon.cfg().get("x") — available without injecting Environment
  • @ConfigurationProperties(prefix="x")props.getProp("x") / props.toBean("x", X.class) — runtime, no compile-time binding

Spring's approach is declarative and compile-checked; Solon's is a single runtime object with the same power, minus the annotation ceremony. Pick whichever fits your style — but know that in Solon the config center is always one call away.

TL;DR

  • Solon.cfg() gives you the whole config center, from anywhere, no injection
  • Typed getters (getInt/getLong/getBool/getDouble) kill manual parsing
  • getProp/getGroupedProp/getListedProp split config by prefix at runtime
  • toBean/bindTo convert config sections into POJOs without annotations
  • loadEnv(prefix) bridges env vars; loadAdd/loadAddIfAbsent layer extra files
  • getByExpr/getByTmpl handle expression and template lookup with :def defaults
  • Deleting a key needs a double remove (props + System.getProperties())

Full API reference: SolonProps Interface Reference — part of the official Solon config guide.

Top comments (0)