From 78447aa7502d4559e6d2b2976766935f6bd0c532 Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Mon, 27 Aug 2018 23:58:08 +0200 Subject: [PATCH] Allow the transaction manager to be overridden in DefaultBatchConfigurer Before this commit, it was not possible to override the transaction manager by subclassing DefaultBatchConfigurer and overriding the getTransactionManager method. This commit uses the getTransactionManager method in the initialize method in order to take into account the transaction manager provided by the user. Resolves BATCH-2294 --- .../annotation/DefaultBatchConfigurer.java | 12 +- .../annotation/EnableBatchProcessing.java | 31 ++++ .../TransactionManagerConfigurationTests.java | 69 ++++++++ ...ConfigurationWithBatchConfigurerTests.java | 146 +++++++++++++++++ ...figurationWithoutBatchConfigurerTests.java | 148 ++++++++++++++++++ 5 files changed, 401 insertions(+), 5 deletions(-) create mode 100644 spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/TransactionManagerConfigurationTests.java create mode 100644 spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/TransactionManagerConfigurationWithBatchConfigurerTests.java create mode 100644 spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/TransactionManagerConfigurationWithoutBatchConfigurerTests.java diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/DefaultBatchConfigurer.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/DefaultBatchConfigurer.java index 389f392351..f1c0ce7225 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/DefaultBatchConfigurer.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/DefaultBatchConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -59,7 +59,8 @@ public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } - if(this.transactionManager == null) { + if(getTransactionManager() == null) { + logger.warn("No transaction manager was provided, using a DataSourceTransactionManager"); this.transactionManager = new DataSourceTransactionManager(this.dataSource); } } @@ -96,11 +97,12 @@ public void initialize() { if(dataSource == null) { logger.warn("No datasource was provided...using a Map based JobRepository"); - if(this.transactionManager == null) { + if(getTransactionManager() == null) { + logger.warn("No transaction manager was provided, using a ResourcelessTransactionManager"); this.transactionManager = new ResourcelessTransactionManager(); } - MapJobRepositoryFactoryBean jobRepositoryFactory = new MapJobRepositoryFactoryBean(this.transactionManager); + MapJobRepositoryFactoryBean jobRepositoryFactory = new MapJobRepositoryFactoryBean(getTransactionManager()); jobRepositoryFactory.afterPropertiesSet(); this.jobRepository = jobRepositoryFactory.getObject(); @@ -128,7 +130,7 @@ protected JobLauncher createJobLauncher() throws Exception { protected JobRepository createJobRepository() throws Exception { JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean(); factory.setDataSource(dataSource); - factory.setTransactionManager(transactionManager); + factory.setTransactionManager(getTransactionManager()); factory.afterPropertiesSet(); return factory.getObject(); } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java index 5fa35dfb2f..c725d592a9 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java @@ -111,6 +111,37 @@ * job repository and transaction manager into every step * * + * The transaction manager provided by this annotation will be of type: + * + * + * + * In order to use a custom transaction manager, a custom {@link BatchConfigurer} should be provided. For example: + * + *
+ * @Configuration
+ * @EnableBatchProcessing
+ * public class AppConfig extends DefaultBatchConfigurer {
+ *
+ *    @Bean
+ *    public Job job() {
+ *       ...
+ *    }
+ *
+ *    @Override
+ *    public PlatformTransactionManager getTransactionManager() {
+ *       return new MyTransactionManager();
+ *    }
+ *
+ *  ...
+ *
+ * }
+ * 
+ * * If the configuration is specified as modular=true then the context will also contain an * {@link AutomaticJobRegistrar}. The job registrar is useful for modularizing your configuration if there are multiple * jobs. It works by creating separate child application contexts containing job configurations and registering those diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/TransactionManagerConfigurationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/TransactionManagerConfigurationTests.java new file mode 100644 index 0000000000..2219488169 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/TransactionManagerConfigurationTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2018 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 + * + * http://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.batch.core.configuration.annotation; + +import javax.sql.DataSource; + +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.aop.Advisor; +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.Advised; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.interceptor.TransactionInterceptor; + +/** + * @author Mahmoud Ben Hassine + */ +@RunWith(MockitoJUnitRunner.class) +public abstract class TransactionManagerConfigurationTests { + + @Mock + protected static PlatformTransactionManager transactionManager; + + @Mock + protected static PlatformTransactionManager transactionManager2; + + /* + * The transaction manager set on JobRepositoryFactoryBean in DefaultBatchConfigurer.createJobRepository + * ends up in the TransactionInterceptor advise applied to the (proxied) JobRepository. + * This method extracts the advise from the proxy and returns the transaction manager. + */ + PlatformTransactionManager getTransactionManagerSetOnJobRepository(JobRepository jobRepository) throws Exception { + TargetSource targetSource = ((Advised) jobRepository).getTargetSource(); // proxy created in SimpleBatchConfiguration.createLazyProxy + Advised target = (Advised) targetSource.getTarget(); // initial proxy created in AbstractJobRepositoryFactoryBean.initializeProxy + Advisor[] advisors = target.getAdvisors(); + for (Advisor advisor : advisors) { + if (advisor.getAdvice() instanceof TransactionInterceptor) { + TransactionInterceptor transactionInterceptor = (TransactionInterceptor) advisor.getAdvice(); + return transactionInterceptor.getTransactionManager(); + } + } + return null; + } + + static DataSource createDataSource() { + return new EmbeddedDatabaseBuilder() + .addScript("classpath:org/springframework/batch/core/schema-drop-hsqldb.sql") + .addScript("classpath:org/springframework/batch/core/schema-hsqldb.sql") + .build(); + } +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/TransactionManagerConfigurationWithBatchConfigurerTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/TransactionManagerConfigurationWithBatchConfigurerTests.java new file mode 100644 index 0000000000..d222230387 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/TransactionManagerConfigurationWithBatchConfigurerTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2018 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 + * + * http://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.batch.core.configuration.annotation; + +import javax.sql.DataSource; + +import org.junit.Assert; +import org.junit.Test; + +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.test.util.AopTestUtils; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * @author Mahmoud Ben Hassine + */ +public class TransactionManagerConfigurationWithBatchConfigurerTests extends TransactionManagerConfigurationTests { + + @Test + public void testConfigurationWithNoDataSourceAndNoTransactionManager() throws Exception { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BatchConfigurationWithNoDataSourceAndNoTransactionManager.class); + BatchConfigurer batchConfigurer = applicationContext.getBean(BatchConfigurer.class); + + PlatformTransactionManager platformTransactionManager = batchConfigurer.getTransactionManager(); + Assert.assertTrue(platformTransactionManager instanceof ResourcelessTransactionManager); + Assert.assertSame(getTransactionManagerSetOnJobRepository(applicationContext.getBean(JobRepository.class)), platformTransactionManager); + } + + @Test + public void testConfigurationWithNoDataSourceAndTransactionManager() throws Exception { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BatchConfigurationWithNoDataSourceAndTransactionManager.class); + BatchConfigurer batchConfigurer = applicationContext.getBean(BatchConfigurer.class); + + PlatformTransactionManager platformTransactionManager = batchConfigurer.getTransactionManager(); + Assert.assertSame(transactionManager, platformTransactionManager); + Assert.assertSame(getTransactionManagerSetOnJobRepository(applicationContext.getBean(JobRepository.class)), transactionManager); + } + + @Test + public void testConfigurationWithDataSourceAndNoTransactionManager() throws Exception { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BatchConfigurationWithDataSourceAndNoTransactionManager.class); + BatchConfigurer batchConfigurer = applicationContext.getBean(BatchConfigurer.class); + + PlatformTransactionManager platformTransactionManager = batchConfigurer.getTransactionManager(); + Assert.assertTrue(platformTransactionManager instanceof DataSourceTransactionManager); + DataSourceTransactionManager dataSourceTransactionManager = AopTestUtils.getTargetObject(platformTransactionManager); + Assert.assertEquals(applicationContext.getBean(DataSource.class), dataSourceTransactionManager.getDataSource()); + Assert.assertSame(getTransactionManagerSetOnJobRepository(applicationContext.getBean(JobRepository.class)), platformTransactionManager); + } + + @Test + public void testConfigurationWithDataSourceAndTransactionManager() throws Exception { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BatchConfigurationWithDataSourceAndTransactionManager.class); + BatchConfigurer batchConfigurer = applicationContext.getBean(BatchConfigurer.class); + + PlatformTransactionManager platformTransactionManager = batchConfigurer.getTransactionManager(); + Assert.assertSame(transactionManager, platformTransactionManager); + Assert.assertSame(getTransactionManagerSetOnJobRepository(applicationContext.getBean(JobRepository.class)), transactionManager); + } + + @EnableBatchProcessing + public static class BatchConfigurationWithNoDataSourceAndNoTransactionManager { + @Bean + public BatchConfigurer batchConfigurer() { + return new DefaultBatchConfigurer(); + } + } + + @EnableBatchProcessing + public static class BatchConfigurationWithNoDataSourceAndTransactionManager { + + @Bean + public PlatformTransactionManager transactionManager() { + return transactionManager; + } + + @Bean + public BatchConfigurer batchConfigurer() { + return new DefaultBatchConfigurer() { + @Override + public PlatformTransactionManager getTransactionManager() { + return transactionManager(); + } + }; + } + + } + + @EnableBatchProcessing + public static class BatchConfigurationWithDataSourceAndNoTransactionManager { + @Bean + public DataSource dataSource() { + return createDataSource(); + } + + @Bean + public BatchConfigurer batchConfigurer(DataSource dataSource) { + return new DefaultBatchConfigurer(dataSource); + } + } + + @EnableBatchProcessing + public static class BatchConfigurationWithDataSourceAndTransactionManager { + + @Bean + public DataSource dataSource() { + return createDataSource(); + } + + @Bean + public PlatformTransactionManager transactionManager() { + return transactionManager; + } + + @Bean + public BatchConfigurer batchConfigurer(DataSource dataSource) { + return new DefaultBatchConfigurer(dataSource) { + @Override + public PlatformTransactionManager getTransactionManager() { + return transactionManager(); + } + }; + } + + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/TransactionManagerConfigurationWithoutBatchConfigurerTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/TransactionManagerConfigurationWithoutBatchConfigurerTests.java new file mode 100644 index 0000000000..57487998c4 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/TransactionManagerConfigurationWithoutBatchConfigurerTests.java @@ -0,0 +1,148 @@ +/* + * Copyright 2018 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 + * + * http://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.batch.core.configuration.annotation; + +import javax.sql.DataSource; + +import org.junit.Assert; +import org.junit.Test; + +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.test.util.AopTestUtils; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * @author Mahmoud Ben Hassine + */ +public class TransactionManagerConfigurationWithoutBatchConfigurerTests extends TransactionManagerConfigurationTests { + + @Test + public void testConfigurationWithNoDataSourceAndNoTransactionManager() throws Exception { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BatchConfigurationWithNoDataSourceAndNoTransactionManager.class); + Assert.assertTrue(applicationContext.containsBean("transactionManager")); + PlatformTransactionManager platformTransactionManager = applicationContext.getBean(PlatformTransactionManager.class); + Object targetObject = AopTestUtils.getTargetObject(platformTransactionManager); + Assert.assertTrue(targetObject instanceof ResourcelessTransactionManager); + Assert.assertSame(getTransactionManagerSetOnJobRepository(applicationContext.getBean(JobRepository.class)), targetObject); + } + + @Test + public void testConfigurationWithNoDataSourceAndTransactionManager() throws Exception { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BatchConfigurationWithNoDataSourceAndTransactionManager.class); + PlatformTransactionManager platformTransactionManager = applicationContext.getBean(PlatformTransactionManager.class); + Assert.assertSame(transactionManager, platformTransactionManager); + // In this case, the supplied transaction manager won't be used by batch and a ResourcelessTransactionManager will be used instead. + // The user has to provide a custom BatchConfigurer. + Assert.assertTrue(getTransactionManagerSetOnJobRepository(applicationContext.getBean(JobRepository.class)) instanceof ResourcelessTransactionManager); + } + + @Test + public void testConfigurationWithDataSourceAndNoTransactionManager() throws Exception { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BatchConfigurationWithDataSourceAndNoTransactionManager.class); + Assert.assertTrue(applicationContext.containsBean("transactionManager")); + PlatformTransactionManager platformTransactionManager = applicationContext.getBean(PlatformTransactionManager.class); + Object targetObject = AopTestUtils.getTargetObject(platformTransactionManager); + Assert.assertTrue(targetObject instanceof DataSourceTransactionManager); + DataSourceTransactionManager dataSourceTransactionManager = (DataSourceTransactionManager) targetObject; + Assert.assertEquals(applicationContext.getBean(DataSource.class), dataSourceTransactionManager.getDataSource()); + Assert.assertSame(getTransactionManagerSetOnJobRepository(applicationContext.getBean(JobRepository.class)), dataSourceTransactionManager); + } + + @Test + public void testConfigurationWithDataSourceAndOneTransactionManager() throws Exception { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BatchConfigurationWithDataSourceAndOneTransactionManager.class); + PlatformTransactionManager platformTransactionManager = applicationContext.getBean(PlatformTransactionManager.class); + Assert.assertSame(transactionManager, platformTransactionManager); + // In this case, the supplied transaction manager won't be used by batch and a DataSourceTransactionManager will be used instead. + // The user has to provide a custom BatchConfigurer. + Assert.assertTrue(getTransactionManagerSetOnJobRepository(applicationContext.getBean(JobRepository.class)) instanceof DataSourceTransactionManager); + } + + @Test + public void testConfigurationWithDataSourceAndMultipleTransactionManagers() throws Exception { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BatchConfigurationWithDataSourceAndMultipleTransactionManagers.class); + PlatformTransactionManager platformTransactionManager = applicationContext.getBean(PlatformTransactionManager.class); + Assert.assertSame(transactionManager2, platformTransactionManager); + // In this case, the supplied primary transaction manager won't be used by batch and a DataSourceTransactionManager will be used instead. + // The user has to provide a custom BatchConfigurer. + Assert.assertTrue(getTransactionManagerSetOnJobRepository(applicationContext.getBean(JobRepository.class)) instanceof DataSourceTransactionManager); + + } + + @EnableBatchProcessing + public static class BatchConfigurationWithNoDataSourceAndNoTransactionManager { + + } + + @EnableBatchProcessing + public static class BatchConfigurationWithNoDataSourceAndTransactionManager { + + @Bean + public PlatformTransactionManager transactionManager() { + return transactionManager; + } + } + + @EnableBatchProcessing + public static class BatchConfigurationWithDataSourceAndNoTransactionManager { + + @Bean + public DataSource dataSource() { + return createDataSource(); + } + } + + @EnableBatchProcessing + public static class BatchConfigurationWithDataSourceAndOneTransactionManager { + + @Bean + public DataSource dataSource() { + return createDataSource(); + } + + @Bean + public PlatformTransactionManager transactionManager() { + return transactionManager; + } + } + + @EnableBatchProcessing + public static class BatchConfigurationWithDataSourceAndMultipleTransactionManagers { + + @Bean + public DataSource dataSource() { + return createDataSource(); + } + + @Bean + public PlatformTransactionManager transactionManager() { + return transactionManager; + } + + @Primary + @Bean + public PlatformTransactionManager transactionManager2() { + return transactionManager2; + } + } +}