Reputation: 331
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
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:
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:
@SpringBootApplication
public class Main {
public static void main(...) {
LicenceChecker.checkLicence();
SpringApplication.run(Main.class);
}
}
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:
META-INF/spring.factories
file and put in there:org.springframework.boot.env.EnvironmentPostProcessor=\
foo.bar.LicenceCheckingEnvironmentPostProcessor
That should work
Upvotes: 2
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