I hope there are many places we can improve this. Let's start with a simple one.

ConfigDataProperties is a value object, and is constructed reflectively via Binder.bind() which is wasteful. As far as I can tell it is only ever used to look up the value of spring.config.import (the env var upper case version is never used because it would be bound already to the "initial" imports). And "import" doesn't have any kebab case variants, so you don't need a Binder. I think we can simply look in the Environment and if the property is there construct a ConfigDataProperties instance directly by calling the constructor or a factor method.

It also uses an annotation @Name to rename the property bound to its constructor, which also seems unnecessarily indirect (we control both the property name and the constructor source code). We probably can't get rid of the annotation processing if we need to use Binder.bind(), but if we can skip that then even better.

Comment From: dsyer

Here's another idea: Spring Boot can trace back all Binder results to a config property name and value, I believe. Maybe we could use that information to short cut the process - bind once and index the result and re-use it. I'd be happy to look into doing that at build time (fits with the general agenda and constraints of spring-init). We would need to be able to override the existing factories more easily probably - at runtime you would want to consult the record of what is known about at build time so you can avoid doing it again.

Comment From: dsyer

Also FWIW here's an interface in Spring Init for bypassing Binder in @ConfigurationProperties:

public interface PropertiesBinder<T> {

    T bind(T bean, Environment environment);

}

Then there's a global utility PropertiesBinders that you can use to register instances of PropertiesBinder. Something like that would work here - a hook to replace the default Binder based Environment bindings.

Comment From: philwebb

As far as I can tell it is only ever used to look up the value of spring.config.import

It also looks up spring.config.activate.on-cloud-platform and spring.config.activate.on-profile. Both of those are enums and both can be used in the main application.properties in a relaxed form. For example spring.profile.activate.oncloudplatform=Kubernetes is valid.

I think we can simply look in the Environment and if the property is there construct a ConfigDataProperties instance directly by calling the constructor or a factory method.

There are a number of edge cases that cause problems if we try and obtain values directly. We've been bitten with this quite a bit in the past, so when we started working on the new class we intentionally decided to keep the Binder central to the new code. One common problem would be lists where YAML files get expanded to the [N] syntax. We'd need to deal with those which would mean repeating a lot of the logic from IndexedElementsBinder.

It also uses an annotation @Name to rename the property bound to its constructor, which also seems unnecessarily indirect (we control both the property name and the constructor source code).

We'd need a different way to get the parameter name in ValueObjectBinder. Currently PARAMETER_NAME_DISCOVERER is hard coded. I went with the annotation because it meant we could update ValueObjectBinder without needing to increase the surface area of the API.

Comment From: philwebb

Maybe we could use that information to short cut the process - bind once and index the result and re-use it.

Are we assuming that you can't use environment or system variables to override properties?

Comment From: dsyer

Are we assuming that you can't use environment or system variables to override properties?

That's not going to be true in general, but I think for building native images and containers people are willing to accept that kind of compromise. We just have to be explicit that they will only get the souped up performance in a "strict closed world" (no changes to class path and environment at runtime).

Anyway, it's probably not necessary to restrict to literally no changes in the environment, just to only changes in the values of environment properties.

Comment From: dsyer

It also looks up spring.config.activate.on-cloud-platform and spring.config.activate.on-profile. Both of those are enums and both can be used in the main application.properties in a relaxed form. For example spring.profile.activate.oncloudplatform=Kubernetes is valid.

I still don't think that a fully-leaded Binder.bind() is warranted here (and in other places). It's really hard to debug/read/understand apart from anything else. Could we maybe separate the "reflection", "relaxed property names" and "type conversion" concerns a bit more, so that it could be written more imperatively?

E.g.

Collection<String> imports = StringUtils.commaDelimitedListToSet(Something.getRelaxedProperty(environment, "spring.config.import", ""));

The property name conventions could be encapsulated in the Environment even. Then you could do it all without reading any annotation metadata and without calling any methods reflectively:

Collection<String> imports = StringUtils.commaDelimitedListToSet(environment, "spring.config.import", "");
String[] profiles = StringUtils.commaDelimitedListToStringArray(environment, "spring.config.activate.on-profile", "");
CloudPlatform platform = CloudPlatform.valueOf(environment, "spring.config.activate.on-cloud-platform", "NONE").toUpperCase());
ConfigDataProperties data = new ConfigDataProperties(imports, new Activate(platform, profiles));

ConfigDataProperties only has 3 inputs (hence it is a sensible value object) and they are all primitive-ish (easy type conversion from String).

Comment From: philwebb

I totally agree that the Binder could do with some improvements to help with debugging and understanding the code. I don't, however, agree that the answer is to not use it to process ConfigData. One of the reasons that the binder is so complicated is that it attempts to deal with all the subtle binding issues that can occur. We've had issues in the past where people are confused why certain features can't be used at certain times.

For example, if we switch to StringUtils.commaDelimitedListToSet(Something.getRelaxedProperty(environment, "spring.config.import", "")) we're immediately restricting the way that spring.config.import can be used.

This would be a valid application.yml:

spring.config.import: "classpath:foo.yaml,classpath:bar.yaml"

This one would not:

spring.config.import: ["classpath:foo.yaml", "classpath:bar.yaml"]

We'd have to document that restriction and explain why. We'd also be adding the limitation that a comma can never be part of the location string.

On top of that, the new code intentionally doesn't modify the Environment until all processing has completed. So we'd need a new Environment instance just for use during the ConfigData processing.

I did recently make a change to Bindable that allows it to carry attributes. I think we could use this to remove the reflective @Name annotation check. I think we should start with that and looking at what else we can do to improve the Binder code.

Comment From: dsyer

if we switch to StringUtils.commaDelimitedListToSet()

So maybe let's not do that. We'd have to change the implementation of the binding so it doesn't expect a String value. That's maybe included in the "type conversion" part. The goal is just to make it more imperative and less obscure. You can still encapsulate as much as you want potentially.

Not sure I follow the bit about a new Environment instance.

Comment From: philwebb

Looking at all the options I don't think we can remove the use of Binder, certainly not without causing a lot of instability. I've opened #23713 to look at the binder in general so see how we might be able to reduce the use of reflection in 3.x.

Comment From: dsyer

:disappointed: I wasn't expecting to remove it, just make it possible to plug something else in. Can we at least look at doing that in 2.4?

Comment From: philwebb

I guess from all the discussion above I assumed there wasn't a sensible alternative to the Binder that wouldn't cause all sorts of subtle issues. I'm not sure I really follow the desire to replace it only for this bit of code. Even if we can somehow offer a pluggable system for ConfigData, won't the Binder still be used for almost all other @ConfigurationProperties? That's one reason why I thought opening #23713 was a good idea.

Comment From: dsyer

Yes, agree. The thing is I think we need to open up those subtle issues, even if it’s “buyer beware”. If I want to restrict the surface area of binding so that only exact matches of property names works, I should be able to make that choice. I should say, I wouldn’t have that opinion without the need to offer non-reflective options, but nevertheless it seems like it should be generally interesting.

Comment From: snicoll

I don't understand how #23713 isn't a follow-up with a potentially larger scale than this issue. Also, I can see this related to spring native and with all we know, I am not sure I agree this is a must-have frankly. I am going to close this now and we we can revisit once we get to #23713