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.getPropertyNames
only returns a name of a property but not all the aliases of the property. -
In
spring-boot
, theBinder.bindObject
usesfindProperty
to find a property and usecontainsNoDescendantOf
to 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.getPropertyNames
return array[ "guess.name"]
or["name", "guess.name"]
, thecontainsNoDescendantOf(context.getSources(), name)
will return false. Because theguess.name
is a descendant ofguess
. As a result, the expressionproperty == null && containsNoDescendantOf(context.getSources(), name)
isfalse
andBinder.bindObject
will 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-SNAPSHOT
instead 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.