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);