Adam Burley opened SPR-13749 and commented
I got directed here by the Spring Boot team. If you have an accessor (getter) in a
@ConfigurationProperties
-annotated class to retrieve a collection, and if you call the copy constructor in that accessor, the returned collection appears to be empty.
Example YAML file ("example.yml"):
one:
two:
- three
- four
- five
Example Spring Controller:
@RestController
public class ExampleController {
@Autowired
private ExampleYamlStore exampleYamlStore;
@RequestMapping({"/*"})
public String index() {
List<String> two = exampleYamlStore.getTwo();
String str;
if (two.size() == 0) {
str = "Size was zero";
} else {
str = two.get(0) + ","
+ two.get(1) + ","
+ two.get(2);
}
return "RESULT: " + str;
}
}
Example YAML store (without copy constructor in accessor):
@Component
@ConfigurationProperties(locations = "classpath:/example.yml", prefix = "one")
public class ExampleYamlStore {
private List<String> two = new ArrayList<>();
public List<String> getTwo() {
return two; // ***MARKED LINE***
}
public void setTwo(List<String> two) {
this.two = new ArrayList<>(two);
}
}
Running this code and hitting http://localhost:8080/foo will give the response "RESULT: three,four,five" as expected.
However if you change the marked line above to:
return new ArrayList<>(two);
Then hit http://localhost:8080/foo - you will get the response "RESULT: Size was zero".
Spring should allow consumers to return a copy of a collection, to prevent any caller from modifying the internal collection. Are you retrieving the list somewhere internally and then modifying it but not setting it back onto the @ConfigurationProperties
-annotated class?
I am using the latest version of Spring Boot (1.3.0) and Java 1.7. I got told by the team at Spring Boot that "Binding is actually handled by the core Spring Framework."
Affects: 4.2.3
Comment From: spring-projects-issues
Adam Burley commented
"Accessor" is another word for "getter". Therefore I don't quite understand the revised title. I'll rephrase.
I presume your intention was to highlight that the current behaviour is not considered (by your team) to be a bug but merely a current restriction of the Spring Framework that all YAML stores must provide accessors which expose the internal object.
Comment From: spring-projects-issues
Juergen Hoeller commented
Thanks for the rephrasing, Adam Burley, that's indeed what I had in mind. The current YamlProcessor
implementation operates on a flattened data structure which expects the raw collections in it, as far as I can see. We may rearchitect this, of course, but only really for 4.3.
That said, if you have an idea for a simple enough patch to the current YamlProcessor
, we're certainly open to rolling it into 4.2.x as well.
Juergen
Comment From: spring-projects-issues
Juergen Hoeller commented
Thanks for the rephrasing, Adam Burley, that's indeed what I had in mind. The current YamlProcessor
implementation operates on a flattened data structure which expects the raw collections in it, as far as I can see. We may rearchitect this, of course, but only really for 4.3.
That said, if you have an idea for a simple enough patch to the current YamlProcessor
, we're certainly open to rolling it into 4.2.x as well.
Juergen
Comment From: spring-projects-issues
Adam Burley commented
Thanks...to be honest I don't know much about the internal implementation of binding. From looking at the code, it seems that the real work happens inside AbstractNestablePropertyAccessor
, in particular the method setPropertyValue(PropertyTokenHolder,PropertyValue)
. In very very simplified terms, what it does is:
- First it gets the property value to be set via
getPropertyValue(PropertyTokenHolder)
. This is ultimately resolved viaBeanWrapperImpl.BeanPropertyHandler.getValue()
(via the abstract signaturePropertyHandler.getValue()
) which looks up a "read method" and then invokes it - Second, it adds the value to the list (
list.add(convertedValue)
) (and similar handling exists for other collections)
The third obvious step would seem to be to do the opposite of the first step. In other words, invoke a method like setRealPropertyValue(PropertyTokenHolder,Object)
(you can call it whatever you like) which mirrors (at least the first few lines of) getPropertyValue(PropertyTokenHolder)
. After getting the PropertyHandler
, you can call the abstract method PropertyHandler.setValue(Object,Object)
(implementation in BeanWrapperImpl.BeanPropertyHandler
again, mirroring the first step) to set the list back onto the store object, passing this.object
as the first argument. (The setValue
method looks up a "write method" and then invokes it, again mirroring the first step.)
So I think adding this third step would be a start. Of course, I have probably vastly oversimplified the problem and not considered lots of edge cases, etc.
Comment From: spring-projects-issues
Stéphane Nicoll commented
Another example of such problem with our Flyway integration
Comment From: spring-projects-issues
Stéphane Nicoll commented
Of course, I have probably vastly oversimplified the problem and not considered lots of edge cases, etc.
You already did a pretty deep analysis. Just to insist on what you wrote already, this has nothing to do with YAML and can be easily reproduced with simple keys (I just did). The binding mechanism in the framework expects the object returned by the getter to be mutable and a whole lot of features are built based on that. Having worked quite deeply in that area recently, I am really not keen to support that use case I am afraid: I think we should be able to add the feature but I am convinced we'll bring additional regressions.
Juergen Hoeller thoughts?
Comment From: spring-projects-issues
Juergen Hoeller commented
From my perspective, it is sensible to require access to the original collections for such bindings. After all, the binding paths explicitly reference specific collection elements there which does suggest taking the collection and modifying it, instead of completely replacing it. If the corresponding getter method happens to return a copy or unmodifiable decorator, you could switch to field access instead, or provide a non-public getter/setter pair (specifically named) just for binding purposes.
Note that Spring does not generally require access to the underlying collections. It's rather just about binding paths that go into a specific collection to replace a specific element.
Juergen
Comment From: spring-projects-issues
Adam Burley commented
you could switch to field access instead, or provide a non-public getter/setter pair (specifically named) just for binding purposes
Sorry, I don't know how to do that... I would certainly be open to providing other methods for Spring to call, but I don't know how I would let Spring know to call those rather than the real getters and setters?
For example:
public List<String> getTwo() {
return new ArrayList<>(two);
}
public void setTwo(List<String> two) {
this.two = new ArrayList<>(two);
}
private List<String> someOtherMethod() { // This method is only called by Spring
return two;
}
Is there some way to specify (e.g. by using a method-level annotation) that Spring should call someOtherMethod
rather than getTwo
as the "getter"?
Comment From: spring-projects-issues
Juergen Hoeller commented
At this point, it's all driven by the property name: Your external property reference would have to be named e.g. "twoInternal" for a "getTwoInternal" method to be called... The more common solution is to use direct field access in that case, referring to the field names, e.g. on a DataBinder
using the initDirectFieldAccess
method. Boot's configuration properties might not support that mode though; I'll check with Stéphane.
Juergen
Comment From: spring-projects-issues
Stéphane Nicoll commented
It does not (see #1254). configuration properties add an extra layer with the meta-data generation: I am not keen (to say the least) to open the possibility of mixed use cases at runtime. That would make property discovery quite complicated with lots of edge case. That's why I personally like the Javabean option: this is a (public) configuration option after all so requiring a regular Javabean property makes sense to me.
Comment From: spring-projects-issues
Adam Burley commented
So am I basically stuck with doing this kind of thing:
private List<String> two = new ArrayList<>();
private List<String> getTwo() {
return two;
}
public List<String> publicGetTwo() {
return new ArrayList<>(two);
}
Comment From: spring-projects-issues
Stéphane Nicoll commented
I don't know what you mean by "stuck". Given that you bound "an object" to the environment, I think it's reasonable to ask for a certain structure for that object. If you want your public API not to conform to that structure, you can bound a package protected object or bind your configuration using a different mechanism.
I agree that not being able to use direct field binding with Spring Boot is problematic (because you could use reflection to hide said requested structure). Having said that, you're far from being stuck IMO.
Comment From: spring-projects-issues
Adam Burley commented
you can bound a package protected object
Okay, so you mean something like this?
@Component
@ConfigurationProperties(locations = "classpath:/example.yml", prefix = "one")
/*default*/ class ExampleYamlStorePP {
private List<String> two = new ArrayList<>();
public List<String> getTwo() {
return two;
}
public void setTwo(List<String> two) {
this.two = new ArrayList<>(two);
}
}
@Component
public class ExampleYamlStore {
@Autowired
private ExampleYamlStorePP exampleYamlStorePP;
public List<String> getTwo() {
return new ArrayList<>(exampleYamlStorePP.getTwo());
}
public void setTwo(List<String> two) {
exampleYamlStorePP.setTwo(two);
}
}
Comment From: spring-projects-issues
Stéphane Nicoll commented
More or less. You don't necessarily need to keep them consistent once the app has started. You can do the other way around and initialize your object on @PostConstruct
based on what was bound from the environment. As I said, many options.
Comment From: spring-projects-issues
Stéphane Nicoll commented
After a throughout consideration, we decided to reject this improvement. Binding is operating on the Environment
and external property source (configuration) so it should have access to the original collections.
Comment From: mwisnicki
If you ever decide to reopen and implement interface binding, please also support default property values via default interface method :)