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 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: [email protected]
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 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.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.