DEV Community

Solon Framework
Solon Framework

Posted on

Solon Config Injection: @Inject, @BindProps, and Auto-Refresh — No @Value, No @ConfigurationProperties

I've been using Solon for a while now, and one of the first things I had to unlearn coming from Spring was how configuration gets into my code. There is no @Value scatter, no @ConfigurationProperties class hierarchy — but honestly, after a week I stopped missing both.

Here's how config injection and binding actually work in Solon, with the exact APIs I use daily.

The three expression forms you'll meet

Solon's @Inject uses a ${...} expression syntax. Three forms cover almost everything:

@Inject("${track.name}")              // plain property lookup
@Inject("${track.name:demoApi}")      // with a default value
@Inject("${classpath:user.config.yml}") // inject a whole resource file
Enter fullscreen mode Exit fullscreen mode

The ${xxx:def} default only works for single values — don't try to default a collection or an entity with it. For those, you inject a whole property block instead (more below).

Field injection, the everyday case

@Component
public class DemoService {

    // static field injection, supported since v3.0
    // autoRefreshed = the value updates when config changes (singleton only!)
    @Inject(value = "${track.name:demoApi}", autoRefreshed = true)
    static String trackName;

    // without a match, the field initial value is kept
    @Inject("${track.url}")
    String trackUrl = "http://x.x.x/track";

    // inject a whole config block as Properties
    @Inject("${track.db1}")
    Properties trackDbCfg;

    // or let Solon build a real bean from the config block
    @Inject("${track.db1}")
    HikariDataSource trackDs;
}
Enter fullscreen mode Exit fullscreen mode

The last two lines are the ones that surprised me most. Give @Inject a target type and a property prefix, and Solon constructs the object from the config block — HikariDataSource gets built straight from track.db1.* keys. No manual setJdbcUrl(...) wiring.

One caveat that bit me: autoRefreshed = true is only safe on field injection into singleton beans. If the bean is prototype-scoped, leave auto-refresh off, or you'll get surprising mid-lifecycle value swaps.

Injecting into @bean method parameters and constructors

@Configuration
public class DemoConfig {

    // inject a datasource built from config, into a @Bean method
    @Bean
    public DataSource db1(@Inject("${track.db1}") HikariDataSource ds) {
        return ds;
    }

    // conditional bean: only created when the property exists and is true
    @Bean
    @Condition(onExpression = "${cache.enable:false} == true")
    public CacheService cache(@Inject("${cache.config}") CacheServiceSupplier supplier) {
        return supplier.get();
    }
}
Enter fullscreen mode Exit fullscreen mode

Constructor injection works the same way on @Component classes:

@Component
public class DemoConfig {
    private String demoName;

    public DemoConfig(@Inject("${demo.name}") String name) {
        this.demoName = name;
    }
}
Enter fullscreen mode Exit fullscreen mode

Binding a whole config class: @BindProps

If you prefer a typed config object you can reuse (closer to Spring's @ConfigurationProperties), bind it once and inject it anywhere:

// Option A: bind with @Inject + @Configuration
@Inject("${user.config}")
@Configuration
public class UserProperties {
    public String name;
    public List<String> tags;
}

// Option B: bind with @BindProps (since v3.0.7) — also generates
// config metadata for IDE hints
@BindProps(prefix = "user.config")
@Configuration
public class UserProperties {
    public String name;
    public List<String> tags;
}

// Or bind on a @Bean method
@Configuration
public class DemoConfig {
    @BindProps(prefix = "user.config")
    @Bean
    public UserProperties userProperties() {
        return new UserProperties();
    }
}
Enter fullscreen mode Exit fullscreen mode

Then reuse it anywhere with a plain @Inject:

@Inject
UserProperties userProperties;
Enter fullscreen mode Exit fullscreen mode

A detail I appreciate: @Inject on classes has required detection — if the config is missing, Solon raises an exception instead of silently injecting nulls. That catches typos in app.yml at startup, not at 3am in production.

Manual access when you don't want injection

Sometimes injection is overkill — a util class, a static context, a one-off lookup. Solon.cfg() is your gateway:

String name  = Solon.cfg().get("track.name", "demoApi");   // with default
String url   = Solon.cfg().get("track.url");               // plain

Properties dbCfg = Solon.cfg().getProp("track.db1");       // whole block as Properties
HikariDataSource ds = Solon.cfg().getBean("track.db1", HikariDataSource.class); // as bean
Enter fullscreen mode Exit fullscreen mode

Prefer method-call form for frequently-read values so you always get the freshest state:

public static String trackName() {
    return Solon.cfg().get("track.name", "demoApi");
}
Enter fullscreen mode Exit fullscreen mode

Change subscription: two ways

Config can change at runtime (config service push, code updates). Two mechanisms exist:

1. Auto-refresh on injected fields (singletons only):

@Inject(value = "${track.name:demoApi}", autoRefreshed = true)
String trackName;
Enter fullscreen mode Exit fullscreen mode

2. Manual subscription — you get every changed key and filter what you need:

Solon.cfg().onChange((key, val) -> {
    if (key.startsWith("track.name")) {
        // refresh whatever depends on it
    }
});
Enter fullscreen mode Exit fullscreen mode

The mental model

You want to... Spring way Solon way
Inject a scalar @Value("${x}") @Inject("${x}") or @Inject("${x:def}")
Inject a config block @ConfigurationProperties + class @Inject("${prefix}") into Properties or a typed target
Reusable typed config @ConfigurationProperties + registration @Inject/@BindProps + @Configuration class, plain @Inject to reuse
Build a bean from config @Bean + manual mapping @Inject("${prefix}") HikariDataSource or Solon.cfg().getBean(...)
React to config change @RefreshScope (cloud) autoRefreshed=true or Solon.cfg().onChange(...)
Missing key handling silently null / error varies @Inject throws on missing (required detection)

Two things worth calling out:

  • @Inject doubles as the bean-injection annotation. In Spring, @Autowired and @Value are separate concerns. In Solon, one annotation handles bean references (@Inject DataSource) and config values (@Inject("${...}")) — and when both are present (@Inject("${track.db1}") HikariDataSource), it builds a bean from config. Fewer annotations, one consistent rule.
  • The config block is a first-class citizen. getProp() / getBean() on any prefix means config-to-object conversion isn't locked inside a DI annotation — it's available anywhere Solon.cfg() is.

That's the whole surface. No profile classes to register, no @ConfigurationProperties hierarchy to maintain — a prefix, a target type, and Solon does the rest.

Top comments (0)