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();
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
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);
Three variants cover different shapes:
-
getProp(keyStarts)→ a singleProps(one section) -
getGroupedProp(keyStarts)→Map<String, Props>(multi-instance sections likedemo.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);
Or bind the whole property set to an existing bean instance:
MailConfig cfg = new MailConfig();
props.bindTo(cfg); // matches keys against bean fields
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
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
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
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
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+ */ }
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");
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 injectingEnvironment -
@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/getListedPropsplit config by prefix at runtime -
toBean/bindToconvert config sections into POJOs without annotations -
loadEnv(prefix)bridges env vars;loadAdd/loadAddIfAbsentlayer extra files -
getByExpr/getByTmplhandle expression and template lookup with:defdefaults - 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)