spring-boot icon indicating copy to clipboard operation
spring-boot copied to clipboard

Dev tools causes misconfigured command-line application to exit with 0

Open msmerc opened this issue 2 years ago • 13 comments

I have a really simple spring application that's misconfigured:

@SpringBootApplication()
public class SimpleApplication implements CommandLineRunner {
    public static void main(String[] args) {
        var ctx = SpringApplication.run(SimpleApplication.class, args);
        System.exit(SpringApplication.exit(ctx));
    }

    public static class MyObject {}
    
    //Oops - No bean provided!!!
    @Autowired MyObject myObject;

    @Override public void run(String... args) { }
}

I get the following error (in intellij):

...
Consider defining a bean of type 'test.app.SimpleApplication$MyObject' in your configuration.


Process finished with exit code 1

Which is what I'd expect.

If I add the spring-boot-devtools dependency in maven:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

Then I get the following:

...
Consider defining a bean of type 'test.app.SimpleApplication$MyObject' in your configuration.


Process finished with exit code 0

It seems that adding this package causes spring boot to have the wrong return codes.

msmerc avatar Nov 05 '21 15:11 msmerc

Here's my full maven config:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>springboot-test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>13</maven.compiler.source>
        <maven.compiler.target>13</maven.compiler.target>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.5.5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <!-- This dependency causes the issue -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
    </dependencies>
</project>

msmerc avatar Nov 05 '21 15:11 msmerc

  1. The app runs within a RestartLauncher due to devtools https://github.com/spring-projects/spring-boot/blob/v2.5.6/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/RestartLauncher.java#L46-L53

  2. When SpringApplication handles the failure it invokes SpringBootExceptionHandler.registerLoggedException for UnsatisfiedDependencyException

  3. The last thing SpringApplication does is ReflectionUtils.rethrowRuntimeException(exception); SpringApplication.java

  4. This causes the exception seen in RestartLauncher to be java.lang.reflect.InvocationTargetException containing target UnsatisfiedDependencyException

  5. Finally, SpringBootExceptionHandler has a registered Exception UnsatisfiedDependencyException .. does not have InvocationTargetException as a registed exception and thus the exit code is 0

RegisteredExceptionVsActual

Ideally, in this case it should investigate the target of the exception and match it

cdmatta avatar Nov 05 '21 17:11 cdmatta

#26894 is in a similar area.

wilkinsona avatar Nov 15 '21 12:11 wilkinsona

@cdmatta @wilkinsona When relying on developmentOnly'org.springframework.boot:spring-boot-devtools', the SpringBoot project will schedule the org.springframework.boot.devtools.restart.RestartLauncher#run() function in devtools when it starts, and the abnormal status code 0 is returned by the abnormal status of devtools, I guess this problem should be related to the operating principle of spring-boot-devtools.

CangYa2021 avatar Nov 27 '21 02:11 CangYa2021

@msmerc @cdmatta You can debug at the following methods. image image

@wilkinsona I think this situation is not a bug, isn't it ?

CangYa2021 avatar Nov 27 '21 02:11 CangYa2021

If this is the intended behavior then I think spring-boot-devtools needs to inform the user that it's changed the exit code! Otherwise this is very non-obvious. Took me ages to track down the culprit.

msmerc avatar Nov 29 '21 09:11 msmerc

It looks like a bug to me. preventNonZeroExitCode() should only come into play if the JVM is exiting due to Dev Tools' own SilentExitException. For any other failure the intent is that the exit code should be unchanged. From what @cdmatta has described above, it's an UnsatisfiedDependencyException so the code that's specific to SilentExitException shouldn't be involved.

wilkinsona avatar Nov 29 '21 10:11 wilkinsona

@wilkinsona @msmerc For the above problem, I tracked again and got the following results:

1、The first is to set SilentExitExceptionHandler in https://github.com/spring-projects/spring-boot/blob/3478e1912cbd545b83788187bf92a06a7453167f/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java#L139

2、Suspend the main thread at https://github.com/spring-projects/spring-boot/blob/3478e1912cbd545b83788187bf92a06a7453167f/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java#L296, and then get the error of the child thread

3、Handle the error obtained in https://github.com/spring-projects/spring-boot/blob/3478e1912cbd545b83788187bf92a06a7453167f/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java#L262-L273

4、When we track https://github.com/spring-projects/spring-boot/blob/3478e1912cbd545b83788187bf92a06a7453167f/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java#L180, it will return SilentExitException

5、At this time, the SilentExitExceptionHandler of the main thread handles SilentExitException and silently terminates the JVM https://github.com/spring-projects/spring-boot/blob/16f54d2c5c27813be63bffd7703285b499ee3892/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/SilentExitExceptionHandler.java#L37-L49

I think devtool does not classify and actively handle the exceptions generated by the child threads, but chooses to return the exception information through the log listener, and chooses to silently end the program. When developmentOnly'org.springframework.boot:spring-boot-devtools' is not introduced, the exit code should be 1 returned by JVM.

CangYa2021 avatar Dec 06 '21 06:12 CangYa2021

@wilkinsona If this logic is unreasonable and I want to solve it, please give some guidance.

CangYa2021 avatar Dec 08 '21 02:12 CangYa2021

@msmerc What's your reason for using DevTools in an application that would normally run and then exit? There's some background about the current behaviour for applications that behave like this in https://github.com/spring-projects/spring-boot/issues/5968.

wilkinsona avatar Dec 08 '21 11:12 wilkinsona

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

spring-projects-issues avatar Dec 15 '21 11:12 spring-projects-issues

@wilkinsona I'm not quite sure I understand. Obviously there's no intention for the app to be misconfigured!

Our app has multiple modules. Basically one webapp and a bunch of supporting CommandLineRunner utilities. These modules all had common dependencies (including DevTools) - arguably this is incorrect since the devtools only make sense for the webapp, but it's hardly something non-spring-experts would be aware of.

We have a test that validates the spring configuration by running one of the spring command line utils on the code during a continuous integration stage. Although the app was misconfigured, the test was passing because the exit was 0, so our test wasn't picking up configuration issues.

The other problem is that it's very non-obvious that the devtools are the issue. There's nothing in the output telling you that devtools is changing the exit code.

msmerc avatar Dec 15 '21 13:12 msmerc

Thanks, @msmerc. Knowing if the usage is accidental or if there was a usecase that we have overlooked is valuable in helping us to prioritise a fix. If there was a usecase that we have overlooked, recommending that you avoid using dev tools in a command-line app would not have been very helpful.

wilkinsona avatar Jan 07 '22 10:01 wilkinsona