When using Spring Boot or just plain Spring Framework it might be that you want to delay the startup of your application until a proper connection to the database can be made. This might be even more the case when using container technologies, like Docker.
I recently came across the DatabaseStartupValidator which has been part of the Spring Framework since 2003!. This little gem will delay the further startup of your application until a connection to the database can be made. It, by default, will try every second to connect to the DB and simply catches the exception. It will try for 60 seconds and after that will fail if no connection can be made (all of these properties are configurable).
To use you simply need to declare a bean and inject the datasource (see Listing 1). You define a validation query (as of Spring 5.3 it will use the JDBC 4, isValid
method by default!). Spring Boot comes with a handy enum that already contains default validation queries for a range of supported database (used by the health check in Spring Boot Actuator).
@SpringBootApplication
public class DatabaseUpApplication {
public static void main(String[] args) {
SpringApplication.run(DatabaseUpApplication.class, args);
}
@Bean
public DatabaseStartupValidator databaseStartupValidator(DataSource dataSource) {
var dsv = new DatabaseStartupValidator();
dsv.setDataSource(dataSource);
dsv.setValidationQuery(DatabaseDriver.POSTGRESQL.getValidationQuery());
return dsv;
}
}
Listing 1: Configuration of DatabaseStartupValidator
However just defining this bean might not be enough, a little more configuration is needed. Beans that depend on the DataSource
like an EntityManagerFactory
, Flyway
or JdbcTemplate
need to depend on the DatabaseStartupValidator
as well. Now you could manually define all the beans and add an @DependsOn
on those @Bean
methods (you will loose some of the auto-configuration from Spring Boot that way) or you could use a BeanFactoryPostProcessor
to modify the beans.
@Bean
public static BeanFactoryPostProcessor dependsOnPostProcessor() {
return bf -> {
// Let beans that need the database depend on the DatabaseStartupValidator
// like the JPA EntityManagerFactory or Flyway
String[] flyway = bf.getBeanNamesForType(Flyway.class);
Stream.of(flyway)
.map(bf::getBeanDefinition)
.forEach(it -> it.setDependsOn("databaseStartupValidator"));
String[] jpa = bf.getBeanNamesForType(EntityManagerFactory.class);
Stream.of(jpa)
.map(bf::getBeanDefinition)
.forEach(it -> it.setDependsOn("databaseStartupValidator"));
};
}
Listing 2: BeanFactoryPostProcessor
to declare depending beans
In this sample we add the dependsOn
for the Flyway
and EntityManagerFactory
bean definitions (the recipe of how to create a bean not the actual bean itself). This will delay the execution of Flyway
and the EntityManagerFactory
until the DatabaseStartupValidator
is fully initialized.
Now when starting the code and delaying the start of the database the startup of the application will stall until it can reach the database. One can simulate this by starting the application and run the database in a Docker container and delay the start. The output would show something like the following:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.3.1.RELEASE)
2020-06-29 09:42:54.081 INFO 32104 --- [ main] b.d.databaseup.DatabaseUpApplication : Starting DatabaseUpApplication on iMac-van-Marten.local with PID 32104 (/Users/marten/Repositories/database-up/target/classes started by marten in /Users/marten/Repositories/database-up)
2020-06-29 09:42:54.084 INFO 32104 --- [ main] b.d.databaseup.DatabaseUpApplication : No active profile set, falling back to default profiles: default
2020-06-29 09:42:54.840 INFO 32104 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFERRED mode.
2020-06-29 09:42:54.887 INFO 32104 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 40ms. Found 1 JPA repository interfaces.
2020-06-29 09:42:55.412 INFO 32104 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2020-06-29 09:42:55.418 INFO 32104 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-06-29 09:42:55.419 INFO 32104 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.36]
2020-06-29 09:42:55.505 INFO 32104 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-06-29 09:42:55.505 INFO 32104 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1385 ms
2020-06-29 09:42:56.590 INFO 32104 --- [ main] o.s.j.support.DatabaseStartupValidator : Database has not started up yet - retrying in 1 seconds (timeout in 58.979 seconds)
2020-06-29 09:42:58.602 INFO 32104 --- [ main] o.s.j.support.DatabaseStartupValidator : Database has not started up yet - retrying in 1 seconds (timeout in 56.967 seconds)
2020-06-29 09:42:59.671 INFO 32104 --- [ main] o.s.j.support.DatabaseStartupValidator : Database startup detected after 4.102 seconds
2020-06-29 09:42:59.723 INFO 32104 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-06-29 09:42:59.759 INFO 32104 --- [ task-1] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2020-06-29 09:42:59.791 INFO 32104 --- [ task-1] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.4.17.Final
2020-06-29 09:42:59.895 INFO 32104 --- [ task-1] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.0.Final}
2020-06-29 09:42:59.958 INFO 32104 --- [ task-1] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.PostgreSQL10Dialect
2020-06-29 09:43:00.067 INFO 32104 --- [ main] o.f.c.internal.license.VersionPrinter : Flyway Community Edition 6.4.4 by Redgate
2020-06-29 09:43:00.076 INFO 32104 --- [ main] o.f.c.internal.database.DatabaseFactory : Database: jdbc:postgresql://localhost:5432/sample (PostgreSQL 12.3)
2020-06-29 09:43:00.125 INFO 32104 --- [ main] o.f.core.internal.command.DbValidate : Successfully validated 1 migration (execution time 00:00.022s)
2020-06-29 09:43:00.140 INFO 32104 --- [ main] o.f.core.internal.command.DbMigrate : Current version of schema "public": 1
2020-06-29 09:43:00.142 INFO 32104 --- [ main] o.f.core.internal.command.DbMigrate : Schema "public" is up to date. No migration necessary.
2020-06-29 09:43:00.213 INFO 32104 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2020-06-29 09:43:00.214 INFO 32104 --- [ main] DeferredRepositoryInitializationListener : Triggering deferred initialization of Spring Data repositories…
2020-06-29 09:43:00.335 INFO 32104 --- [ task-1] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2020-06-29 09:43:00.343 INFO 32104 --- [ task-1] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2020-06-29 09:43:00.464 INFO 32104 --- [ main] DeferredRepositoryInitializationListener : Spring Data repositories initialized!
2020-06-29 09:43:00.473 INFO 32104 --- [ main] b.d.databaseup.DatabaseUpApplication : Started DatabaseUpApplication in 6.672 seconds (JVM running for 7.269)
The source code for this blog can be found on GitHub.