From 35bff550972a3a5c94f337a32da9e5f90c6bae83 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 3 Nov 2020 15:56:44 +0000 Subject: [PATCH] Ensure that Quartz can be auto-configured with a Quartz-specific TM Previously, Quartz could be configured with a specific DataSource using `@QuartzDataSource` but it was not possible to configure a Quartz-specific transaction manager. This could result in the different DataSources being used by Quartz itself and Quart'z DataSourceTransactionManager. This commit introduces a new qualifier, `@QuartzTransactionManager`, that can be used to avoid the above-described problem. Any `@QuartzTransactionManager`-annotated bean will be used by the Quartz auto-configure configuration instead of the application's main `TransactionManager`. If no such qualified bean is present, the application's main TransactionManager, if any, will be used as before. Fixes gh-20184 --- .../quartz/QuartzAutoConfiguration.java | 14 ++++- .../quartz/QuartzDataSource.java | 3 +- .../quartz/QuartzTransactionManager.java | 42 ++++++++++++++ .../quartz/QuartzAutoConfigurationTests.java | 58 +++++++++++++++++++ .../main/asciidoc/spring-boot-features.adoc | 1 + 5 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzTransactionManager.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.java index 64d437534bf..862578bb877 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.java @@ -106,11 +106,13 @@ public class QuartzAutoConfiguration { @Order(0) public SchedulerFactoryBeanCustomizer dataSourceCustomizer(QuartzProperties properties, DataSource dataSource, @QuartzDataSource ObjectProvider quartzDataSource, - ObjectProvider transactionManager) { + ObjectProvider transactionManager, + @QuartzTransactionManager ObjectProvider quartzTransactionManager) { return (schedulerFactoryBean) -> { DataSource dataSourceToUse = getDataSource(dataSource, quartzDataSource); schedulerFactoryBean.setDataSource(dataSourceToUse); - PlatformTransactionManager txManager = transactionManager.getIfUnique(); + PlatformTransactionManager txManager = getTransactionManager(transactionManager, + quartzTransactionManager); if (txManager != null) { schedulerFactoryBean.setTransactionManager(txManager); } @@ -122,6 +124,14 @@ public class QuartzAutoConfiguration { return (dataSourceIfAvailable != null) ? dataSourceIfAvailable : dataSource; } + private PlatformTransactionManager getTransactionManager( + ObjectProvider transactionManager, + ObjectProvider quartzTransactionManager) { + PlatformTransactionManager transactionManagerIfAvailable = quartzTransactionManager.getIfAvailable(); + return (transactionManagerIfAvailable != null) ? transactionManagerIfAvailable + : transactionManager.getIfUnique(); + } + @Bean @ConditionalOnMissingBean public QuartzDataSourceInitializer quartzDataSourceInitializer(DataSource dataSource, diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzDataSource.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzDataSource.java index e703bd49e4d..615b48e1a0b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzDataSource.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzDataSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -30,6 +30,7 @@ import org.springframework.beans.factory.annotation.Qualifier; * {@code @Primary}. * * @author Madhura Bhave + * @see QuartzDataSource * @since 2.0.2 */ @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE }) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzTransactionManager.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzTransactionManager.java new file mode 100644 index 00000000000..9e56a89dcc2 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzTransactionManager.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2020 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.autoconfigure.quartz; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.annotation.Qualifier; + +/** + * Qualifier annotation for a TransactionManager to be injected into Quartz + * auto-configuration. Can be used on a secondary transaction manager, if there is another + * one marked as {@code @Primary}. + * + * @author Andy Wilkinson + * @see QuartzDataSource + * @since 2.2.11 + */ +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Qualifier +public @interface QuartzTransactionManager { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfigurationTests.java index cb677e5ba6c..32837389972 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfigurationTests.java @@ -60,9 +60,11 @@ import org.springframework.context.annotation.Primary; import org.springframework.core.env.Environment; import org.springframework.core.io.ClassPathResource; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.scheduling.quartz.LocalDataSourceJobStore; import org.springframework.scheduling.quartz.QuartzJobBean; import org.springframework.scheduling.quartz.SchedulerFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; @@ -136,6 +138,17 @@ class QuartzAutoConfigurationTests { .run(assertDataSourceJobStore("quartzDataSource")); } + @Test + void transactionManagerWithQuartzTransactionManagerUsedWhenMultiplePresent() { + this.contextRunner + .withUserConfiguration(QuartzJobsConfiguration.class, MultipleTransactionManagersConfiguration.class) + .withPropertyValues("spring.quartz.job-store-type=jdbc").run((context) -> { + SchedulerFactoryBean schedulerFactoryBean = context.getBean(SchedulerFactoryBean.class); + assertThat(schedulerFactoryBean).extracting("transactionManager") + .isEqualTo(context.getBean("quartzTransactionManager")); + }); + } + private ContextConsumer assertDataSourceJobStore(String datasourceName) { return (context) -> { assertThat(context).hasSingleBean(Scheduler.class); @@ -431,6 +444,51 @@ class QuartzAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class MultipleTransactionManagersConfiguration extends BaseQuartzConfiguration { + + private final DataSource primaryDataSource = createTestDataSource(); + + private final DataSource quartzDataSource = createTestDataSource(); + + @Bean + @Primary + DataSource applicationDataSource() { + return this.primaryDataSource; + } + + @Bean + @QuartzDataSource + DataSource quartzDataSource() { + return this.quartzDataSource; + } + + @Bean + @Primary + PlatformTransactionManager applicationTransactionManager() { + return new DataSourceTransactionManager(this.primaryDataSource); + } + + @Bean + @QuartzTransactionManager + PlatformTransactionManager quartzTransactionManager() { + 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(); + } + + } + static class ComponentThatUsesScheduler { ComponentThatUsesScheduler(Scheduler scheduler) { diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index b4f5ffd0338..23be0f27854 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -5819,6 +5819,7 @@ It is also possible to provide a custom script by setting the configprop:spring. To have Quartz use a `DataSource` other than the application's main `DataSource`, declare a `DataSource` bean, annotating its `@Bean` method with `@QuartzDataSource`. Doing so ensures that the Quartz-specific `DataSource` is used by both the `SchedulerFactoryBean` and for schema initialization. +Similarly, to have Quartz use a `TransactionManager` other than the application's main `TransactionManager` declare a `TransactionManager` bean, annotating its `@Bean` method with `@QuartzTransactionManager`. By default, jobs created by configuration will not overwrite already registered jobs that have been read from a persistent job store. To enable overwriting existing job definitions set the configprop:spring.quartz.overwrite-existing-jobs[] property.