Make Quartz DataSource init back off without spring-boot-jdbc

Previously, setting spring.quartz.job-store-type to jdbc assumed that
spring-boot-jdbc was on the classpath. If it was not, the context
would fail to refresh as DataSourceScriptDatabaseInitializer was not
available.

This commit updates the auto-configuration so that the DataSource
initialization will back off when spring-boot-jdbc is absent. This
allows an app to use Quartz without spring-boot-jdbc and
spring-boot-sql. This remains true even when using the JDBC job
store as long as something else has created the DataSource and
initialized its schema to contain the tables that Quartz requires.

Closes gh-46243
This commit is contained in:
Andy Wilkinson 2025-07-02 09:11:39 +01:00
parent 5db3cacde8
commit a52d5538ee
7 changed files with 192 additions and 118 deletions

View File

@ -30,8 +30,6 @@ dependencies {
api("org.quartz-scheduler:quartz")
api("org.springframework:spring-context-support")
implementation(project(":spring-boot-project:spring-boot-sql"))
optional(project(":spring-boot-project:spring-boot-actuator-autoconfigure"))
optional(project(":spring-boot-project:spring-boot-autoconfigure"))
optional(project(":spring-boot-project:spring-boot-jdbc"))

View File

@ -34,6 +34,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
import org.springframework.boot.sql.autoconfigure.init.OnDatabaseInitializationCondition;
import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer;
import org.springframework.context.ApplicationContext;
@ -114,7 +115,7 @@ public class QuartzAutoConfiguration {
};
}
private DataSource getDataSource(DataSource dataSource, ObjectProvider<DataSource> quartzDataSource) {
private static DataSource getDataSource(DataSource dataSource, ObjectProvider<DataSource> quartzDataSource) {
DataSource dataSourceIfAvailable = quartzDataSource.getIfAvailable();
return (dataSourceIfAvailable != null) ? dataSourceIfAvailable : dataSource;
}
@ -127,20 +128,26 @@ public class QuartzAutoConfiguration {
: transactionManager.getIfUnique();
}
@Bean
@ConditionalOnMissingBean
@Conditional(OnQuartzDatasourceInitializationCondition.class)
public QuartzDataSourceScriptDatabaseInitializer quartzDataSourceScriptDatabaseInitializer(
DataSource dataSource, @QuartzDataSource ObjectProvider<DataSource> quartzDataSource,
QuartzProperties properties) {
DataSource dataSourceToUse = getDataSource(dataSource, quartzDataSource);
return new QuartzDataSourceScriptDatabaseInitializer(dataSourceToUse, properties);
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(QuartzJdbcProperties.class)
@ConditionalOnClass(DataSourceScriptDatabaseInitializer.class)
static class QuartzDataSourceInitializationConfiguration {
static class OnQuartzDatasourceInitializationCondition extends OnDatabaseInitializationCondition {
@Bean
@ConditionalOnMissingBean
@Conditional(OnQuartzDatasourceInitializationCondition.class)
QuartzDataSourceScriptDatabaseInitializer quartzDataSourceScriptDatabaseInitializer(DataSource dataSource,
@QuartzDataSource ObjectProvider<DataSource> quartzDataSource, QuartzJdbcProperties properties) {
DataSource dataSourceToUse = getDataSource(dataSource, quartzDataSource);
return new QuartzDataSourceScriptDatabaseInitializer(dataSourceToUse, properties);
}
static class OnQuartzDatasourceInitializationCondition extends OnDatabaseInitializationCondition {
OnQuartzDatasourceInitializationCondition() {
super("Quartz", "spring.quartz.jdbc.initialize-schema");
}
OnQuartzDatasourceInitializationCondition() {
super("Quartz", "spring.quartz.jdbc.initialize-schema");
}
}

View File

@ -47,8 +47,8 @@ public class QuartzDataSourceScriptDatabaseInitializer extends DataSourceScriptD
* @param properties the Quartz properties
* @see #getSettings
*/
public QuartzDataSourceScriptDatabaseInitializer(DataSource dataSource, QuartzProperties properties) {
this(dataSource, getSettings(dataSource, properties), properties.getJdbc().getCommentPrefix());
public QuartzDataSourceScriptDatabaseInitializer(DataSource dataSource, QuartzJdbcProperties properties) {
this(dataSource, getSettings(dataSource, properties), properties.getCommentPrefix());
}
/**
@ -84,15 +84,15 @@ public class QuartzDataSourceScriptDatabaseInitializer extends DataSourceScriptD
* @see #QuartzDataSourceScriptDatabaseInitializer(DataSource,
* DatabaseInitializationSettings)
*/
public static DatabaseInitializationSettings getSettings(DataSource dataSource, QuartzProperties properties) {
public static DatabaseInitializationSettings getSettings(DataSource dataSource, QuartzJdbcProperties properties) {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(resolveSchemaLocations(dataSource, properties.getJdbc()));
settings.setMode(properties.getJdbc().getInitializeSchema());
settings.setSchemaLocations(resolveSchemaLocations(dataSource, properties));
settings.setMode(properties.getInitializeSchema());
settings.setContinueOnError(true);
return settings;
}
private static List<String> resolveSchemaLocations(DataSource dataSource, QuartzProperties.Jdbc properties) {
private static List<String> resolveSchemaLocations(DataSource dataSource, QuartzJdbcProperties properties) {
PlatformPlaceholderDatabaseDriverResolver platformResolver = new PlatformPlaceholderDatabaseDriverResolver();
platformResolver = platformResolver.withDriverPlatform(DatabaseDriver.DB2, "db2_v95");
platformResolver = platformResolver.withDriverPlatform(DatabaseDriver.MYSQL, "mysql_innodb");

View File

@ -0,0 +1,93 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.quartz.autoconfigure;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.sql.init.DatabaseInitializationMode;
/**
* Configuration properties for the Quartz Scheduler integration when using a JDBC job
* store.
*
* @author Vedran Pavic
* @author Stephane Nicoll
* @since 4.0.0
*/
@ConfigurationProperties("spring.quartz.jdbc")
public class QuartzJdbcProperties {
private static final String DEFAULT_SCHEMA_LOCATION = "classpath:org/quartz/impl/"
+ "jdbcjobstore/tables_@@platform@@.sql";
/**
* Path to the SQL file to use to initialize the database schema.
*/
private String schema = DEFAULT_SCHEMA_LOCATION;
/**
* Platform to use in initialization scripts if the @@platform@@ placeholder is used.
* Auto-detected by default.
*/
private String platform;
/**
* Database schema initialization mode.
*/
private DatabaseInitializationMode initializeSchema = DatabaseInitializationMode.EMBEDDED;
/**
* Prefixes for single-line comments in SQL initialization scripts.
*/
private List<String> commentPrefix = new ArrayList<>(Arrays.asList("#", "--"));
public String getSchema() {
return this.schema;
}
public void setSchema(String schema) {
this.schema = schema;
}
public String getPlatform() {
return this.platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public DatabaseInitializationMode getInitializeSchema() {
return this.initializeSchema;
}
public void setInitializeSchema(DatabaseInitializationMode initializeSchema) {
this.initializeSchema = initializeSchema;
}
public List<String> getCommentPrefix() {
return this.commentPrefix;
}
public void setCommentPrefix(List<String> commentPrefix) {
this.commentPrefix = commentPrefix;
}
}

View File

@ -17,14 +17,10 @@
package org.springframework.boot.quartz.autoconfigure;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.sql.init.DatabaseInitializationMode;
/**
* Configuration properties for the Quartz Scheduler integration.
@ -73,8 +69,6 @@ public class QuartzProperties {
*/
private final Map<String, String> properties = new HashMap<>();
private final Jdbc jdbc = new Jdbc();
public JobStoreType getJobStoreType() {
return this.jobStoreType;
}
@ -127,68 +121,4 @@ public class QuartzProperties {
return this.properties;
}
public Jdbc getJdbc() {
return this.jdbc;
}
public static class Jdbc {
private static final String DEFAULT_SCHEMA_LOCATION = "classpath:org/quartz/impl/"
+ "jdbcjobstore/tables_@@platform@@.sql";
/**
* Path to the SQL file to use to initialize the database schema.
*/
private String schema = DEFAULT_SCHEMA_LOCATION;
/**
* Platform to use in initialization scripts if the @@platform@@ placeholder is
* used. Auto-detected by default.
*/
private String platform;
/**
* Database schema initialization mode.
*/
private DatabaseInitializationMode initializeSchema = DatabaseInitializationMode.EMBEDDED;
/**
* Prefixes for single-line comments in SQL initialization scripts.
*/
private List<String> commentPrefix = new ArrayList<>(Arrays.asList("#", "--"));
public String getSchema() {
return this.schema;
}
public void setSchema(String schema) {
this.schema = schema;
}
public String getPlatform() {
return this.platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public DatabaseInitializationMode getInitializeSchema() {
return this.initializeSchema;
}
public void setInitializeSchema(DatabaseInitializationMode initializeSchema) {
this.initializeSchema = initializeSchema;
}
public List<String> getCommentPrefix() {
return this.commentPrefix;
}
public void setCommentPrefix(List<String> commentPrefix) {
this.commentPrefix = commentPrefix;
}
}
}

View File

@ -16,10 +16,15 @@
package org.springframework.boot.quartz.autoconfigure;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Collections;
import java.util.Enumeration;
import java.util.concurrent.Executor;
import javax.sql.DataSource;
@ -34,6 +39,8 @@ import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
@ -52,6 +59,7 @@ import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerA
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
import org.springframework.boot.liquibase.autoconfigure.LiquibaseAutoConfiguration;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
@ -70,8 +78,11 @@ import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
@ -133,6 +144,48 @@ class QuartzAutoConfigurationTests {
});
}
@Test
void dataSourceInitializationBacksOffWithoutSpringBootJdbc() {
this.contextRunner.withBean(DataSource.class, QuartzAutoConfigurationTests::createTestDataSource)
.withBean(SchedulerFactoryBeanCustomizer.class, () -> (schedulerFactoryBean) -> {
// Mock out the scheduler so that the context doesn't fail to start
// due to missing tables that the JDBC job store requires.
try {
SchedulerFactory factory = mock(SchedulerFactory.class);
given(factory.getScheduler()).willReturn(mock(Scheduler.class));
given(factory.getScheduler(anyString())).willReturn(mock(Scheduler.class));
schedulerFactoryBean.setSchedulerFactory(factory);
}
catch (SchedulerException ex) {
throw new RuntimeException(ex);
}
})
.withClassLoader(new FilteredClassLoader("org.springframework.boot.jdbc", "org.springframework.boot.sql") {
@Override
public Enumeration<URL> getResources(String name) throws IOException {
Enumeration<URL> resources = super.getResources(name);
if (!name.equals("META-INF/spring.factories")) {
return resources;
}
// Hide META-INF/spring.factories files with entries from
// org.springframework.boot.jdbc
return Collections.enumeration(Collections.list(resources).stream().filter((url) -> {
try (InputStream input = url.openStream()) {
String content = StreamUtils.copyToString(input, StandardCharsets.UTF_8);
return !content.contains("org.springframework.boot.jdbc.");
}
catch (IOException ex) {
return true;
}
}).toList());
}
})
.withPropertyValues("spring.quartz.job-store-type=jdbc")
.run((context) -> assertThat(context).doesNotHaveBean(QuartzDataSourceScriptDatabaseInitializer.class));
}
@Test
void withDataSourceNoTransactionManager() {
this.contextRunner.withUserConfiguration(QuartzJobsConfiguration.class)
@ -368,6 +421,18 @@ class QuartzAutoConfigurationTests {
};
}
private static DataSource createTestDataSource() {
DataSourceProperties properties = new DataSourceProperties();
properties.setGenerateUniqueName(true);
try {
properties.afterPropertiesSet();
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
return properties.initializeDataSourceBuilder().build();
}
@Import(ComponentThatUsesScheduler.class)
@Configuration(proxyBeanMethods = false)
static class BaseQuartzConfiguration {
@ -486,23 +551,16 @@ class QuartzAutoConfigurationTests {
@Bean
@Primary
DataSource applicationDataSource() throws Exception {
DataSource applicationDataSource() {
return createTestDataSource();
}
@QuartzDataSource
@Bean
DataSource quartzDataSource() throws Exception {
DataSource quartzDataSource() {
return createTestDataSource();
}
private DataSource createTestDataSource() throws Exception {
DataSourceProperties properties = new DataSourceProperties();
properties.setGenerateUniqueName(true);
properties.afterPropertiesSet();
return properties.initializeDataSourceBuilder().build();
}
}
@Configuration(proxyBeanMethods = false)
@ -536,18 +594,6 @@ class QuartzAutoConfigurationTests {
return new DataSourceTransactionManager(this.quartzDataSource);
}
private DataSource createTestDataSource() {
DataSourceProperties properties = new DataSourceProperties();
properties.setGenerateUniqueName(true);
try {
properties.afterPropertiesSet();
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
return properties.initializeDataSourceBuilder().build();
}
}
@Configuration(proxyBeanMethods = false)
@ -555,7 +601,7 @@ class QuartzAutoConfigurationTests {
@Bean
QuartzDataSourceScriptDatabaseInitializer customInitializer(DataSource dataSource,
QuartzProperties properties) {
QuartzJdbcProperties properties) {
return new QuartzDataSourceScriptDatabaseInitializer(dataSource, properties);
}

View File

@ -39,8 +39,8 @@ class QuartzDataSourceScriptDatabaseInitializerTests {
@Test
void getSettingsWithPlatformDoesNotTouchDataSource() {
DataSource dataSource = mock(DataSource.class);
QuartzProperties properties = new QuartzProperties();
properties.getJdbc().setPlatform("test");
QuartzJdbcProperties properties = new QuartzJdbcProperties();
properties.setPlatform("test");
DatabaseInitializationSettings settings = QuartzDataSourceScriptDatabaseInitializer.getSettings(dataSource,
properties);
assertThat(settings.getSchemaLocations())
@ -50,9 +50,9 @@ class QuartzDataSourceScriptDatabaseInitializerTests {
@Test
void customizeSetCommentPrefixes() {
QuartzProperties properties = new QuartzProperties();
properties.getJdbc().setPlatform("test");
properties.getJdbc().setCommentPrefix(Arrays.asList("##", "--"));
QuartzJdbcProperties properties = new QuartzJdbcProperties();
properties.setPlatform("test");
properties.setCommentPrefix(Arrays.asList("##", "--"));
QuartzDataSourceScriptDatabaseInitializer initializer = new QuartzDataSourceScriptDatabaseInitializer(
mock(DataSource.class), properties);
ResourceDatabasePopulator populator = mock(ResourceDatabasePopulator.class);