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.

  1. In spring-framework, the JOptCommandLinePropertySource.getPropertyNames only returns a name of a property but not all the aliases of the property.

  2. In spring-boot, the Binder.bindObject uses findProperty to find a property and use containsNoDescendantOf to determine whether the property name has a descendant. If property has a descendant, it would continue to find the descendant property to bind the target.

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.

  1. In spring-framework, if JOptCommandLinePropertySource.getPropertyNames return array [ "guess.name"] or ["name", "guess.name"], the containsNoDescendantOf(context.getSources(), name) will return false. Because the guess.name is a descendant of guess. As a result, the expression property == null && containsNoDescendantOf(context.getSources(), name) is false and Binder.bindObject will continue so that it can bind data successfully.

  2. In spring-boot, @snicoll added a commit 430571 which can fix the bug. It changes the expression property == null && containsNoDescendantOf(context.getSources(), name) to property == null && containsNoDescendantOf(context.getSources(), name) && context.depth != 0. So if I use spring-boot 2.2.0.BUILD-SNAPSHOT instead of spring-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.