From a52d5538eef8650a7bd3e3709b3b4df1c768a5ae Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 2 Jul 2025 09:11:39 +0100 Subject: [PATCH] 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 --- .../spring-boot-quartz/build.gradle | 2 - .../QuartzAutoConfiguration.java | 33 ++++--- ...tzDataSourceScriptDatabaseInitializer.java | 12 +-- .../autoconfigure/QuartzJdbcProperties.java | 93 +++++++++++++++++++ .../autoconfigure/QuartzProperties.java | 70 -------------- .../QuartzAutoConfigurationTests.java | 90 +++++++++++++----- ...aSourceScriptDatabaseInitializerTests.java | 10 +- 7 files changed, 192 insertions(+), 118 deletions(-) create mode 100644 spring-boot-project/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzJdbcProperties.java diff --git a/spring-boot-project/spring-boot-quartz/build.gradle b/spring-boot-project/spring-boot-quartz/build.gradle index 913624aab3a..bfc1a471d9b 100644 --- a/spring-boot-project/spring-boot-quartz/build.gradle +++ b/spring-boot-project/spring-boot-quartz/build.gradle @@ -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")) diff --git a/spring-boot-project/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzAutoConfiguration.java b/spring-boot-project/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzAutoConfiguration.java index f8e693aa01f..ff9b87b77b0 100644 --- a/spring-boot-project/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzAutoConfiguration.java +++ b/spring-boot-project/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzAutoConfiguration.java @@ -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 quartzDataSource) { + private static DataSource getDataSource(DataSource dataSource, ObjectProvider 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 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 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"); } } diff --git a/spring-boot-project/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzDataSourceScriptDatabaseInitializer.java b/spring-boot-project/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzDataSourceScriptDatabaseInitializer.java index a457b4d9033..acb875979a5 100644 --- a/spring-boot-project/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzDataSourceScriptDatabaseInitializer.java +++ b/spring-boot-project/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzDataSourceScriptDatabaseInitializer.java @@ -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 resolveSchemaLocations(DataSource dataSource, QuartzProperties.Jdbc properties) { + private static List resolveSchemaLocations(DataSource dataSource, QuartzJdbcProperties properties) { PlatformPlaceholderDatabaseDriverResolver platformResolver = new PlatformPlaceholderDatabaseDriverResolver(); platformResolver = platformResolver.withDriverPlatform(DatabaseDriver.DB2, "db2_v95"); platformResolver = platformResolver.withDriverPlatform(DatabaseDriver.MYSQL, "mysql_innodb"); diff --git a/spring-boot-project/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzJdbcProperties.java b/spring-boot-project/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzJdbcProperties.java new file mode 100644 index 00000000000..4adf537d44b --- /dev/null +++ b/spring-boot-project/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzJdbcProperties.java @@ -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 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 getCommentPrefix() { + return this.commentPrefix; + } + + public void setCommentPrefix(List commentPrefix) { + this.commentPrefix = commentPrefix; + } + +} diff --git a/spring-boot-project/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzProperties.java b/spring-boot-project/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzProperties.java index 58a94836f8b..707f0bd6e1a 100644 --- a/spring-boot-project/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzProperties.java +++ b/spring-boot-project/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/autoconfigure/QuartzProperties.java @@ -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 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 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 getCommentPrefix() { - return this.commentPrefix; - } - - public void setCommentPrefix(List commentPrefix) { - this.commentPrefix = commentPrefix; - } - - } - } diff --git a/spring-boot-project/spring-boot-quartz/src/test/java/org/springframework/boot/quartz/autoconfigure/QuartzAutoConfigurationTests.java b/spring-boot-project/spring-boot-quartz/src/test/java/org/springframework/boot/quartz/autoconfigure/QuartzAutoConfigurationTests.java index a22315ad8b7..e0288a2a5c0 100644 --- a/spring-boot-project/spring-boot-quartz/src/test/java/org/springframework/boot/quartz/autoconfigure/QuartzAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-quartz/src/test/java/org/springframework/boot/quartz/autoconfigure/QuartzAutoConfigurationTests.java @@ -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 getResources(String name) throws IOException { + Enumeration 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); } diff --git a/spring-boot-project/spring-boot-quartz/src/test/java/org/springframework/boot/quartz/autoconfigure/QuartzDataSourceScriptDatabaseInitializerTests.java b/spring-boot-project/spring-boot-quartz/src/test/java/org/springframework/boot/quartz/autoconfigure/QuartzDataSourceScriptDatabaseInitializerTests.java index 69b06819f22..38831e6033c 100644 --- a/spring-boot-project/spring-boot-quartz/src/test/java/org/springframework/boot/quartz/autoconfigure/QuartzDataSourceScriptDatabaseInitializerTests.java +++ b/spring-boot-project/spring-boot-quartz/src/test/java/org/springframework/boot/quartz/autoconfigure/QuartzDataSourceScriptDatabaseInitializerTests.java @@ -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);