From e30391661d526ca6882295f4b5e9b3191516f3ba Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 14 Jul 2023 14:37:28 +0200 Subject: [PATCH 1/2] Document repeatable annotation semantics for @Scheduled Closes gh-23959 --- .../modules/ROOT/pages/integration/scheduling.adoc | 6 ++++++ .../springframework/scheduling/annotation/Scheduled.java | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/integration/scheduling.adoc b/framework-docs/modules/ROOT/pages/integration/scheduling.adoc index eae7ff2e70..ee08e1a9f4 100644 --- a/framework-docs/modules/ROOT/pages/integration/scheduling.adoc +++ b/framework-docs/modules/ROOT/pages/integration/scheduling.adoc @@ -380,6 +380,12 @@ Notice that the methods to be scheduled must have void returns and must not acce arguments. If the method needs to interact with other objects from the application context, those would typically have been provided through dependency injection. +`@Scheduled` can be used as a repeatable annotation. If several scheduled declarations +are found on the same method, each of them will be processed independently, with a +separate trigger firing for each of them. As a consequence, such co-located schedules +may overlap and execute multiple times in parallel or in immediate succession. +Please make sure that your specified cron expressions etc do not accidentally overlap. + [NOTE] ==== As of Spring Framework 4.3, `@Scheduled` methods are supported on beans of any scope. diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java index a202e7846b..41e88d3f50 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java @@ -29,8 +29,8 @@ import org.springframework.scheduling.config.ScheduledTaskRegistrar; /** * Annotation that marks a method to be scheduled. Exactly one of the - * {@link #cron}, {@link #fixedDelay}, or {@link #fixedRate} attributes must be - * specified. + * {@link #cron}, {@link #fixedDelay}, or {@link #fixedRate} attributes + * must be specified. * *

The annotated method must expect no arguments. It will typically have * a {@code void} return type; if not, the returned value will be ignored @@ -42,7 +42,10 @@ import org.springframework.scheduling.config.ScheduledTaskRegistrar; * XML element or {@link EnableScheduling @EnableScheduling} annotation. * *

This annotation can be used as a {@linkplain Repeatable repeatable} - * annotation. + * annotation. If several scheduled declarations are found on the same method, + * each of them will be processed independently, with a separate trigger firing + * for each of them. As a consequence, such co-located schedules may overlap + * and execute multiple times in parallel or in immediate succession. * *

This annotation may be used as a meta-annotation to create custom * composed annotations with attribute overrides. From 384246c360070616e625d549250a92e28fbc2ff7 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 14 Jul 2023 14:37:34 +0200 Subject: [PATCH 2/2] Polishing --- .../jmx/export/MBeanExporterTests.java | 36 +++++++++---------- ...bstractFallbackSQLExceptionTranslator.java | 9 +++-- .../jdbc/support/JdbcAccessor.java | 25 +++++++------ .../jdbc/support/JdbcTransactionManager.java | 36 ++++++++++--------- .../SQLErrorCodeSQLExceptionTranslator.java | 27 +++++++------- .../SQLExceptionSubclassTranslator.java | 6 ++-- .../SQLStateSQLExceptionTranslator.java | 19 ++++++++-- 7 files changed, 90 insertions(+), 68 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/jmx/export/MBeanExporterTests.java b/spring-context/src/test/java/org/springframework/jmx/export/MBeanExporterTests.java index 695c760d6f..2fb7bed7f9 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/MBeanExporterTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/MBeanExporterTests.java @@ -354,7 +354,7 @@ public class MBeanExporterTests extends AbstractMBeanServerTests { exporter.setBeans(beansToExport); exporter.setServer(getServer()); exporter.setBeanFactory(factory); - exporter.setAutodetectMode(MBeanExporter.AUTODETECT_NONE); + exporter.setAutodetect(false); // MBean has a bad ObjectName, so if said MBean is autodetected, an exception will be thrown... start(exporter); } @@ -524,18 +524,16 @@ public class MBeanExporterTests extends AbstractMBeanServerTests { } @Test - void notRunningInBeanFactoryAndPassedBeanNameToExport() throws Exception { + void notRunningInBeanFactoryAndPassedBeanNameToExport() { Map beans = Map.of(OBJECT_NAME, "beanName"); exporter.setBeans(beans); - assertThatExceptionOfType(MBeanExportException.class) - .isThrownBy(() -> start(exporter)); + assertThatExceptionOfType(MBeanExportException.class).isThrownBy(() -> start(exporter)); } @Test - void notRunningInBeanFactoryAndAutodetectionIsOn() throws Exception { - exporter.setAutodetectMode(MBeanExporter.AUTODETECT_ALL); - assertThatExceptionOfType(MBeanExportException.class) - .isThrownBy(() -> start(exporter)); + void notRunningInBeanFactoryAndAutodetectionIsOn() { + exporter.setAutodetect(true); + assertThatExceptionOfType(MBeanExportException.class).isThrownBy(() -> start(exporter)); } @Test // SPR-2158 @@ -556,7 +554,7 @@ public class MBeanExporterTests extends AbstractMBeanServerTests { } @Test // SPR-3302 - void beanNameCanBeUsedInNotificationListenersMap() throws Exception { + void beanNameCanBeUsedInNotificationListenersMap() { String beanName = "charlesDexterWard"; BeanDefinitionBuilder testBean = BeanDefinitionBuilder.rootBeanDefinition(JmxTestBean.class); @@ -576,7 +574,7 @@ public class MBeanExporterTests extends AbstractMBeanServerTests { } @Test - void wildcardCanBeUsedInNotificationListenersMap() throws Exception { + void wildcardCanBeUsedInNotificationListenersMap() { String beanName = "charlesDexterWard"; BeanDefinitionBuilder testBean = BeanDefinitionBuilder.rootBeanDefinition(JmxTestBean.class); @@ -636,24 +634,23 @@ public class MBeanExporterTests extends AbstractMBeanServerTests { exporter.setServer(getServer()); exporter.setAssembler(new NamedBeanAutodetectCapableMBeanInfoAssemblerStub(firstBeanName, secondBeanName)); exporter.setBeanFactory(factory); - exporter.setAutodetectMode(MBeanExporter.AUTODETECT_ALL); + exporter.setAutodetect(true); exporter.addExcludedBean(secondBeanName); start(exporter); - assertIsRegistered("Bean not autodetected in (AUTODETECT_ALL) mode", - ObjectNameManager.getInstance(firstBeanName)); - assertIsNotRegistered("Bean should have been excluded", - ObjectNameManager.getInstance(secondBeanName)); + assertIsRegistered("Bean not autodetected", ObjectNameManager.getInstance(firstBeanName)); + assertIsNotRegistered("Bean should have been excluded", ObjectNameManager.getInstance(secondBeanName)); } @Test void registerFactoryBean() throws MalformedObjectNameException { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); - factory.registerBeanDefinition("spring:type=FactoryBean", new RootBeanDefinition(ProperSomethingFactoryBean.class)); + factory.registerBeanDefinition("spring:type=FactoryBean", + new RootBeanDefinition(ProperSomethingFactoryBean.class)); exporter.setServer(getServer()); exporter.setBeanFactory(factory); - exporter.setAutodetectMode(MBeanExporter.AUTODETECT_ALL); + exporter.setAutodetect(true); start(exporter); assertIsRegistered("Non-null FactoryBean object registered", @@ -663,11 +660,12 @@ public class MBeanExporterTests extends AbstractMBeanServerTests { @Test void ignoreNullObjectFromFactoryBean() throws MalformedObjectNameException { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); - factory.registerBeanDefinition("spring:type=FactoryBean", new RootBeanDefinition(NullSomethingFactoryBean.class)); + factory.registerBeanDefinition("spring:type=FactoryBean", + new RootBeanDefinition(NullSomethingFactoryBean.class)); exporter.setServer(getServer()); exporter.setBeanFactory(factory); - exporter.setAutodetectMode(MBeanExporter.AUTODETECT_ALL); + exporter.setAutodetect(true); start(exporter); assertIsNotRegistered("Null FactoryBean object not registered", diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java index 44b8838b21..f1546734ac 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -31,6 +31,8 @@ import org.springframework.util.Assert; * * @author Juergen Hoeller * @since 2.5.6 + * @see #doTranslate + * @see #setFallbackTranslator */ public abstract class AbstractFallbackSQLExceptionTranslator implements SQLExceptionTranslator { @@ -42,8 +44,8 @@ public abstract class AbstractFallbackSQLExceptionTranslator implements SQLExcep /** - * Override the default SQL state fallback translator - * (typically a {@link SQLStateSQLExceptionTranslator}). + * Set the fallback translator to use when this translator cannot find a + * specific match itself. */ public void setFallbackTranslator(@Nullable SQLExceptionTranslator fallback) { this.fallbackTranslator = fallback; @@ -51,6 +53,7 @@ public abstract class AbstractFallbackSQLExceptionTranslator implements SQLExcep /** * Return the fallback exception translator, if any. + * @see #setFallbackTranslator */ @Nullable public SQLExceptionTranslator getFallbackTranslator() { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcAccessor.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcAccessor.java index 8b4bd9c304..5178b4a0f4 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcAccessor.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -80,10 +80,11 @@ public abstract class JdbcAccessor implements InitializingBean { } /** - * Specify the database product name for the DataSource that this accessor uses. - * This allows to initialize an SQLErrorCodeSQLExceptionTranslator without - * obtaining a Connection from the DataSource to get the meta-data. + * Specify the database product name for the {@code DataSource} that this accessor uses. + * This allows for initializing a {@link SQLErrorCodeSQLExceptionTranslator} without + * obtaining a {@code Connection} from the {@code DataSource} to get the meta-data. * @param dbName the database product name that identifies the error codes entry + * @see #setExceptionTranslator * @see SQLErrorCodeSQLExceptionTranslator#setDatabaseProductName * @see java.sql.DatabaseMetaData#getDatabaseProductName() */ @@ -98,22 +99,20 @@ public abstract class JdbcAccessor implements InitializingBean { /** * Set the exception translator for this instance. - *

If no custom translator is provided, a default - * {@link SQLErrorCodeSQLExceptionTranslator} is used - * which examines the SQLException's vendor-specific error code. + *

A {@link SQLErrorCodeSQLExceptionTranslator} used by default if a user-provided + * `sql-error-codes.xml` file has been found in the root of the classpath. Otherwise, + * {@link SQLExceptionSubclassTranslator} serves as the default translator as of 6.0. * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator - * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator + * @see org.springframework.jdbc.support.SQLExceptionSubclassTranslator */ public void setExceptionTranslator(SQLExceptionTranslator exceptionTranslator) { this.exceptionTranslator = exceptionTranslator; } /** - * Return the exception translator for this instance. - *

Creates a default {@link SQLErrorCodeSQLExceptionTranslator} - * for the specified DataSource if none set, or a - * {@link SQLStateSQLExceptionTranslator} in case of no DataSource. - * @see #getDataSource() + * Return the exception translator to use for this instance, + * creating a default if necessary. + * @see #setExceptionTranslator */ public SQLExceptionTranslator getExceptionTranslator() { SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcTransactionManager.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcTransactionManager.java index bda750f472..e03987499a 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcTransactionManager.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,8 +59,8 @@ public class JdbcTransactionManager extends DataSourceTransactionManager { /** - * Create a new JdbcTransactionManager instance. - * A DataSource has to be set to be able to use it. + * Create a new {@code JdbcTransactionManager} instance. + * A {@code DataSource} has to be set to be able to use it. * @see #setDataSource */ public JdbcTransactionManager() { @@ -68,7 +68,7 @@ public class JdbcTransactionManager extends DataSourceTransactionManager { } /** - * Create a new JdbcTransactionManager instance. + * Create a new {@code JdbcTransactionManager} instance. * @param dataSource the JDBC DataSource to manage transactions for */ public JdbcTransactionManager(DataSource dataSource) { @@ -79,13 +79,15 @@ public class JdbcTransactionManager extends DataSourceTransactionManager { /** - * Specify the database product name for the DataSource that this transaction manager - * uses. This allows to initialize an SQLErrorCodeSQLExceptionTranslator without - * obtaining a Connection from the DataSource to get the meta-data. + * Specify the database product name for the {@code DataSource} that this + * transaction manager operates on. + * This allows for initializing a {@link SQLErrorCodeSQLExceptionTranslator} without + * obtaining a {@code Connection} from the {@code DataSource} to get the meta-data. * @param dbName the database product name that identifies the error codes entry - * @see JdbcAccessor#setDatabaseProductName + * @see #setExceptionTranslator * @see SQLErrorCodeSQLExceptionTranslator#setDatabaseProductName * @see java.sql.DatabaseMetaData#getDatabaseProductName() + * @see JdbcAccessor#setDatabaseProductName */ public void setDatabaseProductName(String dbName) { if (SQLErrorCodeSQLExceptionTranslator.hasUserProvidedErrorCodesFile()) { @@ -97,22 +99,22 @@ public class JdbcTransactionManager extends DataSourceTransactionManager { } /** - * Set the exception translator for this instance. - *

If no custom translator is provided, a default - * {@link SQLErrorCodeSQLExceptionTranslator} is used - * which examines the SQLException's vendor-specific error code. - * @see JdbcAccessor#setExceptionTranslator + * Set the exception translator for this transaction manager. + *

A {@link SQLErrorCodeSQLExceptionTranslator} used by default if a user-provided + * `sql-error-codes.xml` file has been found in the root of the classpath. Otherwise, + * {@link SQLExceptionSubclassTranslator} serves as the default translator as of 6.0. * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator + * @see org.springframework.jdbc.support.SQLExceptionSubclassTranslator + * @see JdbcAccessor#setExceptionTranslator */ public void setExceptionTranslator(SQLExceptionTranslator exceptionTranslator) { this.exceptionTranslator = exceptionTranslator; } /** - * Return the exception translator for this instance. - *

Creates a default {@link SQLErrorCodeSQLExceptionTranslator} - * for the specified DataSource if none set. - * @see #getDataSource() + * Return the exception translator to use for this instance, + * creating a default if necessary. + * @see #setExceptionTranslator */ public SQLExceptionTranslator getExceptionTranslator() { SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java index 25d1513a78..bdc7656cc5 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java @@ -60,6 +60,11 @@ import org.springframework.util.function.SupplierUtils; * of the class path (e.g. in the "/WEB-INF/classes" directory), as long as the * Spring JDBC package is loaded from the same ClassLoader. * + *

This translator is commonly used by default if a user-provided `sql-error-codes.xml` + * file has been found in the root of the classpath, as a signal to use this strategy. + * Otherwise, {@link SQLExceptionSubclassTranslator} serves as the default translator + * as of 6.0. + * * @author Rod Johnson * @author Thomas Risberg * @author Juergen Hoeller @@ -75,11 +80,10 @@ public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExcep private static final int MESSAGE_SQL_THROWABLE_CONSTRUCTOR = 4; private static final int MESSAGE_SQL_SQLEX_CONSTRUCTOR = 5; - private static final boolean USER_PROVIDED_ERROR_CODES_FILE_PRESENT = - new ClassPathResource(SQLErrorCodesFactory.SQL_ERROR_CODE_OVERRIDE_PATH, SQLErrorCodesFactory.class.getClassLoader()).exists(); - + private static final boolean userProvidedErrorCodesFilePresent = + new ClassPathResource(SQLErrorCodesFactory.SQL_ERROR_CODE_OVERRIDE_PATH, + SQLErrorCodesFactory.class.getClassLoader()).exists(); - /** Error codes used by this translator. */ @Nullable private SingletonSupplier sqlErrorCodes; @@ -198,9 +202,9 @@ public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExcep if (sqlErrorCodes != null) { SQLExceptionTranslator customTranslator = sqlErrorCodes.getCustomSqlExceptionTranslator(); if (customTranslator != null) { - DataAccessException customDex = customTranslator.translate(task, sql, sqlEx); - if (customDex != null) { - return customDex; + dae = customTranslator.translate(task, sql, sqlEx); + if (dae != null) { + return dae; } } } @@ -228,11 +232,10 @@ public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExcep for (CustomSQLErrorCodesTranslation customTranslation : customTranslations) { if (Arrays.binarySearch(customTranslation.getErrorCodes(), errorCode) >= 0 && customTranslation.getExceptionClass() != null) { - DataAccessException customException = createCustomException( - task, sql, sqlEx, customTranslation.getExceptionClass()); - if (customException != null) { + dae = createCustomException(task, sql, sqlEx, customTranslation.getExceptionClass()); + if (dae != null) { logTranslation(task, sql, sqlEx, true); - return customException; + return dae; } } } @@ -426,7 +429,7 @@ public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExcep * in the root of the classpath. */ static boolean hasUserProvidedErrorCodesFile() { - return USER_PROVIDED_ERROR_CODES_FILE_PRESENT; + return userProvidedErrorCodesFilePresent; } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java index 058348f7ec..eb5fdcd250 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -51,6 +51,8 @@ import org.springframework.lang.Nullable; *

Falls back to a standard {@link SQLStateSQLExceptionTranslator} if the JDBC * driver does not actually expose JDBC 4 compliant {@code SQLException} subclasses. * + *

This translator serves as the default translator as of 6.0. + * * @author Thomas Risberg * @author Juergen Hoeller * @since 2.5 @@ -72,7 +74,7 @@ public class SQLExceptionSubclassTranslator extends AbstractFallbackSQLException return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex); } if (ex instanceof SQLTransactionRollbackException) { - if ("40001".equals(ex.getSQLState())) { + if (SQLStateSQLExceptionTranslator.indicatesCannotAcquireLock(ex.getSQLState())) { return new CannotAcquireLockException(buildMessage(task, sql, ex), ex); } return new PessimisticLockingFailureException(buildMessage(task, sql, ex), ex); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java index f8d64b9485..5954618b3e 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java @@ -39,11 +39,16 @@ import org.springframework.lang.Nullable; * does not require special initialization (no database vendor detection, etc.). * For more precise translation, consider {@link SQLErrorCodeSQLExceptionTranslator}. * + *

This translator is commonly used as a {@link #setFallbackTranslator fallback} + * behind a primary translator such as {@link SQLErrorCodeSQLExceptionTranslator} or + * {@link SQLExceptionSubclassTranslator}. + * * @author Rod Johnson * @author Juergen Hoeller * @author Thomas Risberg * @see java.sql.SQLException#getSQLState() * @see SQLErrorCodeSQLExceptionTranslator + * @see SQLExceptionSubclassTranslator */ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLExceptionTranslator { @@ -111,7 +116,7 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex); } else if (PESSIMISTIC_LOCKING_FAILURE_CODES.contains(classCode)) { - if ("40001".equals(sqlState)) { + if (indicatesCannotAcquireLock(sqlState)) { return new CannotAcquireLockException(buildMessage(task, sql, ex), ex); } return new PessimisticLockingFailureException(buildMessage(task, sql, ex), ex); @@ -148,9 +153,10 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException return sqlState; } + /** * Check whether the given SQL state (and the associated error code in case - * of a generic SQL state value) indicate a duplicate key exception: + * of a generic SQL state value) indicate a {@link DuplicateKeyException}: * either SQL state 23505 as a specific indication, or the generic SQL state * 23000 with well-known vendor codes (1 for Oracle, 1062 for MySQL/MariaDB, * 2601/2627 for MS SQL Server). @@ -163,4 +169,13 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException (errorCode == 1 || errorCode == 1062 || errorCode == 2601 || errorCode == 2627))); } + /** + * Check whether the given SQL state indicates a {@link CannotAcquireLockException}, + * with SQL state 40001 as a specific indication. + * @param sqlState the SQL state value + */ + static boolean indicatesCannotAcquireLock(@Nullable String sqlState) { + return "40001".equals(sqlState); + } + }