diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BasicBatchConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BasicBatchConfigurer.java index 15f8ca3e36..dac961ecc2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BasicBatchConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BasicBatchConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -26,6 +26,7 @@ import org.springframework.batch.core.launch.support.SimpleJobLauncher; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.repository.support.JobRepositoryFactoryBean; import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.autoconfigure.batch.BatchProperties.Isolation; import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.jdbc.datasource.DataSourceTransactionManager; @@ -139,7 +140,8 @@ public class BasicBatchConfigurer implements BatchConfigurer, InitializingBean { * @return the isolation level or {@code null} to use the default */ protected String determineIsolationLevel() { - return null; + Isolation isolation = this.properties.getJdbc().getIsolationLevelForCreate(); + return (isolation != null) ? isolation.toIsolationName() : null; } protected PlatformTransactionManager createTransactionManager() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchProperties.java index 08e7c1e6b5..efe9a140b2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchProperties.java @@ -66,6 +66,12 @@ public class BatchProperties { private static final String DEFAULT_SCHEMA_LOCATION = "classpath:org/springframework/" + "batch/core/schema-@@platform@@.sql"; + /** + * Transaction isolation level to use when creating job meta-data for new jobs. + * Auto-detected based on whether JPA is being used or not. + */ + private Isolation isolationLevelForCreate; + /** * Path to the SQL file to use to initialize the database schema. */ @@ -87,6 +93,14 @@ public class BatchProperties { */ private DatabaseInitializationMode initializeSchema = DatabaseInitializationMode.EMBEDDED; + public Isolation getIsolationLevelForCreate() { + return this.isolationLevelForCreate; + } + + public void setIsolationLevelForCreate(Isolation isolationLevelForCreate) { + this.isolationLevelForCreate = isolationLevelForCreate; + } + public String getSchema() { return this.schema; } @@ -121,4 +135,45 @@ public class BatchProperties { } + /** + * Available transaction isolation levels. + */ + public enum Isolation { + + /** + * Use the default isolation level of the underlying datastore. + */ + DEFAULT, + + /** + * Indicates that dirty reads, non-repeatable reads and phantom reads can occur. + */ + READ_UNCOMMITTED, + + /** + * Indicates that dirty reads are prevented; non-repeatable reads and phantom + * reads can occur. + */ + READ_COMMITTED, + + /** + * Indicates that dirty reads and non-repeatable reads are prevented; phantom + * reads can occur. + */ + REPEATABLE_READ, + + /** + * Indicate that dirty reads, non-repeatable reads and phantom reads are + * prevented. + */ + SERIALIZABLE; + + private static final String PREFIX = "ISOLATION_"; + + String toIsolationName() { + return PREFIX + name(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JpaBatchConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JpaBatchConfigurer.java index d7ebdc61e6..1d168debe1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JpaBatchConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JpaBatchConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 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. @@ -22,6 +22,7 @@ import javax.sql.DataSource; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.boot.autoconfigure.batch.BatchProperties.Isolation; import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.transaction.PlatformTransactionManager; @@ -54,8 +55,15 @@ public class JpaBatchConfigurer extends BasicBatchConfigurer { @Override protected String determineIsolationLevel() { - logger.warn("JPA does not support custom isolation levels, so locks may not be taken when launching Jobs"); - return "ISOLATION_DEFAULT"; + String name = super.determineIsolationLevel(); + if (name != null) { + return name; + } + else { + logger.warn("JPA does not support custom isolation levels, so locks may not be taken when launching Jobs. " + + "To silence this warning, set 'spring.batch.jdbc.isolation-level-for-create' to 'default'."); + return Isolation.DEFAULT.toIsolationName(); + } } @Override diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java index 01fd02c7ca..502df3940a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java @@ -23,6 +23,7 @@ import javax.persistence.EntityManagerFactory; import javax.sql.DataSource; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.Job; @@ -57,6 +58,8 @@ import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; import org.springframework.boot.sql.init.DatabaseInitializationMode; import org.springframework.boot.sql.init.DatabaseInitializationSettings; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @@ -78,6 +81,7 @@ import static org.mockito.Mockito.mock; * @author Vedran Pavic * @author Kazuki Shimizu */ +@ExtendWith(OutputCaptureExtension.class) class BatchAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -211,6 +215,30 @@ class BatchAutoConfigurationTests { }); } + @Test + void testDefaultIsolationLevelWithJpaLogsWarning(CapturedOutput output) { + this.contextRunner.withUserConfiguration(TestConfiguration.class, EmbeddedDataSourceConfiguration.class, + HibernateJpaAutoConfiguration.class).run((context) -> { + assertThat(context.getBean(BasicBatchConfigurer.class).determineIsolationLevel()) + .isEqualTo("ISOLATION_DEFAULT"); + assertThat(output).contains("JPA does not support custom isolation levels") + .contains("set 'spring.batch.jdbc.isolation-level-for-create' to 'default'"); + }); + } + + @Test + void testCustomIsolationLevelWithJpaDoesNotLogWarning(CapturedOutput output) { + this.contextRunner.withPropertyValues("spring.batch.jdbc.isolation-level-for-create=default") + .withUserConfiguration(TestConfiguration.class, EmbeddedDataSourceConfiguration.class, + HibernateJpaAutoConfiguration.class) + .run((context) -> { + assertThat(context.getBean(BasicBatchConfigurer.class).determineIsolationLevel()) + .isEqualTo("ISOLATION_DEFAULT"); + assertThat(output).doesNotContain("JPA does not support custom isolation levels") + .doesNotContain("set 'spring.batch.jdbc.isolation-level-for-create' to 'default'"); + }); + } + @Test void testRenamePrefix() { this.contextRunner diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationWithoutJpaTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationWithoutJpaTests.java index 6c7f56287f..98b07f757e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationWithoutJpaTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationWithoutJpaTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -61,6 +61,7 @@ class BatchAutoConfigurationWithoutJpaTests { .contains("DataSourceTransactionManager"); assertThat(context.getBean(BatchProperties.class).getJdbc().getInitializeSchema()) .isEqualTo(DatabaseInitializationMode.EMBEDDED); + assertThat(context.getBean(BasicBatchConfigurer.class).determineIsolationLevel()).isNull(); assertThat(new JdbcTemplate(context.getBean(DataSource.class)) .queryForList("select * from BATCH_JOB_EXECUTION")).isEmpty(); assertThat(context.getBean(JobExplorer.class).findRunningJobExecutions("test")).isEmpty(); @@ -84,6 +85,15 @@ class BatchAutoConfigurationWithoutJpaTests { }); } + @Test + void jdbcWithCustomIsolationLevel() { + this.contextRunner.withUserConfiguration(DefaultConfiguration.class, EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.datasource.generate-unique-name=true", + "spring.batch.jdbc.isolation-level-for-create=read_committed") + .run((context) -> assertThat(context.getBean(BasicBatchConfigurer.class).determineIsolationLevel()) + .isEqualTo("ISOLATION_READ_COMMITTED")); + } + @EnableBatchProcessing @TestAutoConfigurationPackage(City.class) static class DefaultConfiguration {