I have a collection (with a prefix) of data structures and I need to select one data structure from the collection and bind that into a ConfigProps, I've seen similar requests which always seem to end up using weird or very manual hacks.
A very simplified version of my configuration looks like:
teams:
also-ran:
name: We were there
winners:
name: We Are the Winners
losers:
name: We lost
From the command line environment, I receive the property "org.top-team=winners" which tells me I need to bind "teams.winners".
So the key requirement here is that the tail end of the prefix be the result of looking up another property. I need to combine the initial prefix ("teams" in the example) and the value of another property name ("org.top-team").
Here are two possible ConfigProps objects that could use this config. Each example is missing one of the two parts of the indirect binding.
The indirect binding may apply to a top-level ConfigProps:
@ConfigurationProperties(prefix = "teams")
@Data
public class Team {
private String name;
}
A prefix is already given, but it needs a selector added to the end. This will choose WHICH entry under teams to load. Could be as simple as selector="org.topteam"
added to the ConfigProps annotation. Or it could be SpEL in the prefix prefix = "teams.{org.top-team}"
If the indirect binding applies to a field in another ConfigProps, it looks a little different:
@ConfigurationProperties(prefix = "org")
@Data
public class Org {
private Team topTeam;
}
In this case, the selector is already available as a combination of prefix from the parent + field name. What's missing is a property name for the selector lookup. It could be supplied as a new annotation like @BindIndirectly(prefix = "teams")
This would also apply if a collection were involved like department.teams: winners,losers
@ConfigurationProperties(prefix = "department")
@Data
public class Department {
private Set<Team> teams;
}
Comment From: philwebb
Generally we've tried as much as possible to keep logic out of @ConfigurationProperties
binding annotations. There are a number of problems that we can see happening if we try to be too flexible. For example, having prefix = "teams.{org.top-team}"
will make it hard for us to produce a sensible meta-data JSON file for IDEs to consume.
For your example, I wonder if using a Map
might be enough. Something like:
@ConfigurationProperties(prefix = "data")
@Data
public class Data {
private Map<String,Team> teams;
@Data
public static class Team {
private String name;
}
}
With yaml like this:
data:
teams:
also-ran:
name: We were there
winners:
name: We Are the Winners
losers:
name: We lost
You then have a bean a bit like this:
public class MyBean {
MyBean(Date data, @Value("org.top-team") String topTeam) {
Team team = data.getTeams().get(topTeam);
}
}
If that doesn't work, using the org.springframework.boot.context.properties.bind.Binder
directly would be a good option. For example:
Binder binder = Binder.get(environment);
Team team = binder.bind("teams." + topTeam, Team.class).get();
The Binder
can bind to any class, they don't need to have @ConfigurationProperties
annotations. You can also bind to maps or lists, for example by using binder.bind("teams", Bindable.listOf(Team.class)
.
Comment From: NewellRose
My work-around involves using the binder directly and a bindhandler too. It's extremely ugly and a lot more code than it should be.
The syntax I suggested is declarative, not logic. And it's available earlier in the startup process.
Comment From: philwebb
I'm sorry the Binder
isn't working out too well. We tried to design the code so that folks could use it directly if @ConfigurationProperties
didn't have the features that they needed.
Unfortunately, I think that the indirect binding feature that you've described isn't something we'd be comfortable adding directly at this time. We'd rather not add any more complexity in that area or take on the associated maintenance costs.
Thanks anyway for the suggestion.