Scott Balmos
Scott Balmos

Reputation: 331

Spring Boot how to cleanly shut down during startup

I'm working on including a license key validator in my Spring Boot 2.3.4 app, and am using an @EventListener on a ContextRefreshedEvent, along with SpringApplication.exit() to force the app to shut down at startup if the key is invalid. All's well, and it seems like the app shuts down. But there is a ton of needless stack traces related to the task scheduler still trying to start up after the application context has closed. I have two beans in my app that used @Scheduled, for reference.

Is there any way to more cleanly force a shutdown during startup, in this case? I have also tried listening to ApplicationStartedEvent and ApplicationReadyEvent, still with varying levels of stack traces spewed.

Obvious test case class, forcing the license to be invalid:

@Component
public class LicenseValidator {
    private static final Logger LOGGER = LoggerFactory.getLogger(LicenseValidator.class);

    private final String licenseKey;

    public LicenseValidator(@Value("${app.license:}") String licenseKey) {
        this.licenseKey = licenseKey;
    }

    @EventListener
    public void onStartup(ContextRefreshedEvent event) {
        if (StringUtils.isEmpty(licenseKey)) {
            LOGGER.error("*** CRITICAL: LICENSE INVALID");
            SpringApplication.exit(event.getApplicationContext(), () -> 0);
        }
    }
}

Logs during shutdown (already in debug mode):

2020-10-20 09:48:55.042  INFO 15272 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-10-20 09:48:55.042  INFO 15272 --- [           main] DeferredRepositoryInitializationListener : Triggering deferred initialization of Spring Data repositories…
2020-10-20 09:48:55.323  INFO 15272 --- [           main] DeferredRepositoryInitializationListener : Spring Data repositories initialized!
2020-10-20 09:48:55.323 ERROR 15272 --- [           main] c.n.myapp.LicenseValidator               : *** CRITICAL: LICENSE INVALID
2020-10-20 09:48:55.573  INFO 15272 --- [           main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2020-10-20 09:48:55.573  INFO 15272 --- [           main] o.s.s.c.ThreadPoolTaskScheduler          : Shutting down ExecutorService 'taskScheduler'
2020-10-20 09:48:55.573  INFO 15272 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
2020-10-20 09:48:55.573  INFO 15272 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2020-10-20 09:48:55.589  INFO 15272 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
2020-10-20 09:48:55.605  INFO 15272 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2020-10-20 09:48:55.605  WARN 15272 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.boot.context.properties.ConfigurationPropertiesBindException: Error creating bean with name 'spring.task.scheduling-org.springframework.boot.autoconfigure.task.TaskSchedulingProperties': Could not bind properties to 'TaskSchedulingProperties' : prefix=spring.task.scheduling, ignoreInvalidFields=false, ignoreUnknownFields=true; nested exception is java.lang.IllegalStateException: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@2d36e77e has been closed already
2020-10-20 09:48:55.605  INFO 15272 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2020-10-20 09:48:55.620 ERROR 15272 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.boot.context.properties.ConfigurationPropertiesBindException: Error creating bean with name 'spring.task.scheduling-org.springframework.boot.autoconfigure.task.TaskSchedulingProperties': Could not bind properties to 'TaskSchedulingProperties' : prefix=spring.task.scheduling, ignoreInvalidFields=false, ignoreUnknownFields=true; nested exception is java.lang.IllegalStateException: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@2d36e77e has been closed already
    at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.bind(ConfigurationPropertiesBindingPostProcessor.java:92) ~[spring-boot-2.3.4.RELEASE.jar:2.3.4.RELEASE]
...

Upvotes: 1

Views: 3100

Answers (2)

Mark Bramnik
Mark Bramnik

Reputation: 42441

As an idea and a question that might hopefully lead to the answer ;) Why do you even want to start the other spring beans (like scheduled stuff, etc)? Depending on you actual application there might be a lot of stuff happening during the application startup, some actually update the state of your environment:

To name a few and give you an idea what be done during the startup:

  • Flyway might run a migration on your database if you have one
  • Hibernate may even create schema for you if its instructed to do so
  • Maybe you have ElasticSearch and create index during the startup, who knows

So, bottom line, The code that checks the license, in my understanding should run before all this stuff and in general as early as possible

So I can think about two solutions:

  1. Run the code even before the Spring boot starts its bootstraping:
@SpringBootApplication
public class Main {

   public static void main(...) {
      LicenceChecker.checkLicence(); 
      SpringApplication.run(Main.class);
   }
}
  1. The first method might have a drawback that you can't rely on Spring's property definitions to specify the licence key. In this case you will want to run the licence checker after the "environmen" has been loaded, but way before spring starts creating the beans.

Spring/spring boot indeed has such an abstraction called EnvironmentPostProcessor:

Step 1: Create the post processor:

package foo.bar;

import org.springframework.boot.env.EnvironmentPostProcessor;

public class LicenceCheckingEnvironmentPostProcessor implements EnvironmentPostProcessor {

   public void postProcessEnvironment(ConfigurableEnvironment configurableEnvironment, SpringApplication springApplication)
       ... check the licence here ...
       // access the properties, profiles, whatever via the configurableEnvironment object
}

Step 2: Register the post processor:

  • Create META-INF/spring.factories file and put in there:
org.springframework.boot.env.EnvironmentPostProcessor=\
  foo.bar.LicenceCheckingEnvironmentPostProcessor

That should work

Upvotes: 2

Scott Balmos
Scott Balmos

Reputation: 331

For sake of posterity, I'm continuing to flesh this out, adding additional hooks, logic, etc. But taking the basic idea from Actuator's ShutdownEndpoint, I have changed my LicenseValidator to be a @Scheduled which fires off a separate thread to gracefully shut things down if the license is invalid. In the final usage, the license key would not be a @Value property, but read from the DB, periodically validated against a central license server, etc.

@Component
public class LicenseValidator {
    private static final Logger LOGGER = LoggerFactory.getLogger(LicenseValidator.class);

    private final String licenseKey;

    private final ConfigurableApplicationContext ctx;

    public LicenseValidator(@Value("${app.license:}") String licenseKey,
                            ConfigurableApplicationContext ctx) {
        this.licenseKey = licenseKey;
        this.ctx = ctx;
    }

    @Scheduled(fixedRate = 60000L)
    public void validateLicense() {
        if (StringUtils.isEmpty(licenseKey)) {
            LOGGER.error("*** CRITICAL: LICENSE INVALID");
            Thread shutdownThread = new Thread(this::shutdownApp);
            shutdownThread.setContextClassLoader(this.getClass().getClassLoader());
            shutdownThread.start();
        }
    }

    private void shutdownApp() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException ignored) {}

        // could also be ctx.close(), but whatever floats your boat...
        SpringApplication.exit(ctx, () -> 0);
    }
}

Upvotes: 0

Related Questions