At present, the JOptCommandLinePropertySource.getPropertyNames get the last element in the result of spec.options(). But the comment says Only the longest name is used for enumerating. It is a contradiction and one of the two(code or comment) has to be changed.
I change the code according the comment because I think the longest name is more useful. And if more than one name have the same longest length, the first one(alphabetically) will be used in my solution.
Fix #22168.
Comment From: jhoeller
I'm not quite sure why the comment says "longest" there to begin with... I'm also not sure that's a good rule to follow: It should rather be the last (or first - if we were to choose this freshly) entry for an intuitive convention that allows for indicating any well-defined name there. From that perspective, I'd rather fix the comment, suggesting that the last name is expected to be the most descriptive (possibly but not necessarily longest).
Comment From: lgxbslgx
As @chludwig-haufe commented on the issue #22168, at present the code of JOptCommandLinePropertySource.getPropertyNames may cause some of the properties were never bound to the corresponding @ConfigurationProperties object.
You can test it using the following code.
@ConfigurationProperties(prefix = "guess")
@Component
public class Guess {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@EnableConfigurationProperties(Guess.class)
@SpringBootApplication
public class IssueTest implements ApplicationRunner {
@Autowired
private Environment springEnvironment;
@Autowired
private Guess guess;
public static void main(String[] args) {
OptionParser optionParser = new OptionParser(false);
optionParser.acceptsAll(Arrays.asList("name", "guess.name")).withRequiredArg();
OptionSet optionSet = optionParser.parse("-name", "apple");
JOptCommandLinePropertySource configurationProperties = new JOptCommandLinePropertySource(optionSet);
StandardEnvironment standardEnvironment = new StandardEnvironment();
standardEnvironment.getPropertySources().addFirst(configurationProperties);
SpringApplication springApplication = new SpringApplication(IssueTest.class);
springApplication.setEnvironment(standardEnvironment);
springApplication.run();
}
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("guess.name = " + guess.getName());
}
}
When I execute the main method using the spring-boot 2.1.4.RELEASE , the name in object guess is null . It is a bug caused by both spring-boot and spring-framework.
-
In
spring-framework, theJOptCommandLinePropertySource.getPropertyNamesonly returns a name of a property but not all the aliases of the property. -
In
spring-boot, theBinder.bindObjectusesfindPropertyto find a property and usecontainsNoDescendantOfto determine whether the property name has a descendant. If property has a descendant, it would continue to find the descendant property to bind thetarget.
For the code I show above , firstly the findProperty in Binder.bindObject will return null using the parameter ConfigurationPropertyName which includes guess . Then the containsNoDescendantOf(context.getSources(), name) will return true. Because JOptCommandLinePropertySource.getPropertyNames return array ["name"] (don't contain guess.name) and the name is not a descendant of guess. As a result, the expression property == null && containsNoDescendantOf(context.getSources(), name) is true and Binder.bindObject will return null so that the binding fail.
To fix the bug, we can change the code in either spring-framework or spring-boot.
-
In
spring-framework, ifJOptCommandLinePropertySource.getPropertyNamesreturn array[ "guess.name"]or["name", "guess.name"], thecontainsNoDescendantOf(context.getSources(), name)will return false. Because theguess.nameis a descendant ofguess. As a result, the expressionproperty == null && containsNoDescendantOf(context.getSources(), name)isfalseandBinder.bindObjectwill continue so that it can bind data successfully. -
In
spring-boot, @snicoll added a commit 430571 which can fix the bug. It changes the expressionproperty == null && containsNoDescendantOf(context.getSources(), name)toproperty == null && containsNoDescendantOf(context.getSources(), name) && context.depth != 0. So if I usespring-boot 2.2.0.BUILD-SNAPSHOTinstead ofspring-boot 2.1.4.RELEASE, the bug doesn't occur.
We could simply consider that spring-boot uses JOptCommandLinePropertySource.getPropertyNames by mistake. But the name called getPropertyNames means the users, such as spring-boot , can use it to get property names without needing to know how it implements. So I want to change code at method getPropertyNames to make it more robust to use.
It may be a problem that we didn't discuss before. What does the getPropertyNames return if the property has aliases? We should make a convention about this situation after discussing.
In my opinion, I don't think it is good to get the first, last, or longest name because it even can't solve the problem above. I would like to return all the aliases .
@jhoeller what is your opinion? Maybe you have a better solution.
Comment From: snicoll
I'm not quite sure why the comment says "longest" there to begin with
Yeah I am puzzled by that as well. It could be that jopt used to put the most descriptive option at the end of the list. It looks like it is semi-arbitrary, see #291.
In my opinion, I don't think it is good to get the first, last, or longest name because it even can't solve the problem above. I would like to return all the aliases .
The thing is, if you have multiple representations of the same option, then you have several name backed by the same "property". I think it's a problem for that contract as it requires one representation. We can't enumerate the same property more than once I am afraid.