Skip to content

Commit 670b9fd

Browse files
committed
Retry SQLErrorCodesFactory retrieval if DatabaseMetaData access failed
Includes deprecation of JdbcUtils.extractDatabaseMetaData(DataSource, String) in favor of the now generified version of extractDatabaseMetaData(DataSource, DatabaseMetaDataCallback). Closes gh-25681 Closes gh-25686
1 parent c2363a6 commit 670b9fd

File tree

9 files changed

+152
-72
lines changed

9 files changed

+152
-72
lines changed

spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.scheduling.quartz;
1818

1919
import java.sql.Connection;
20+
import java.sql.DatabaseMetaData;
2021
import java.sql.SQLException;
2122

2223
import javax.sql.DataSource;
@@ -147,7 +148,8 @@ public void initialize() {
147148

148149
// No, if HSQL is the platform, we really don't want to use locks...
149150
try {
150-
String productName = JdbcUtils.extractDatabaseMetaData(this.dataSource, "getDatabaseProductName");
151+
String productName = JdbcUtils.extractDatabaseMetaData(this.dataSource,
152+
DatabaseMetaData::getDatabaseProductName);
151153
productName = JdbcUtils.commonDatabaseName(productName);
152154
if (productName != null && productName.toLowerCase().contains("hsql")) {
153155
setUseDBLocks(false);

spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProviderFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ private CallMetaDataProviderFactory() {
7373
*/
7474
public static CallMetaDataProvider createMetaDataProvider(DataSource dataSource, final CallMetaDataContext context) {
7575
try {
76-
return (CallMetaDataProvider) JdbcUtils.extractDatabaseMetaData(dataSource, databaseMetaData -> {
76+
return JdbcUtils.extractDatabaseMetaData(dataSource, databaseMetaData -> {
7777
String databaseProductName = JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName());
7878
boolean accessProcedureColumnMetaData = context.isAccessCallParameterMetaData();
7979
if (context.isFunction()) {

spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProviderFactory.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -49,9 +49,8 @@ private TableMetaDataProviderFactory() {
4949
*/
5050
public static TableMetaDataProvider createMetaDataProvider(DataSource dataSource, TableMetaDataContext context) {
5151
try {
52-
return (TableMetaDataProvider) JdbcUtils.extractDatabaseMetaData(dataSource, databaseMetaData -> {
53-
String databaseProductName =
54-
JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName());
52+
return JdbcUtils.extractDatabaseMetaData(dataSource, databaseMetaData -> {
53+
String databaseProductName = JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName());
5554
boolean accessTableColumnMetaData = context.isAccessTableColumnMetaData();
5655
TableMetaDataProvider provider;
5756

spring-jdbc/src/main/java/org/springframework/jdbc/support/DatabaseMetaDataCallback.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,10 +26,12 @@
2626
* and handled correctly by the JdbcUtils class.
2727
*
2828
* @author Thomas Risberg
29-
* @see JdbcUtils#extractDatabaseMetaData
29+
* @author Juergen Hoeller
30+
* @param <T> the result type
31+
* @see JdbcUtils#extractDatabaseMetaData(javax.sql.DataSource, DatabaseMetaDataCallback)
3032
*/
3133
@FunctionalInterface
32-
public interface DatabaseMetaDataCallback {
34+
public interface DatabaseMetaDataCallback<T> {
3335

3436
/**
3537
* Implementations must implement this method to process the meta-data
@@ -42,6 +44,6 @@ public interface DatabaseMetaDataCallback {
4244
* @throws MetaDataAccessException in case of other failures while
4345
* extracting meta-data (for example, reflection failure)
4446
*/
45-
Object processMetaData(DatabaseMetaData dbmd) throws SQLException, MetaDataAccessException;
47+
T processMetaData(DatabaseMetaData dbmd) throws SQLException, MetaDataAccessException;
4648

4749
}

spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -315,26 +315,44 @@ else if (obj instanceof java.sql.Date) {
315315

316316
/**
317317
* Extract database meta-data via the given DatabaseMetaDataCallback.
318-
* <p>This method will open a connection to the database and retrieve the database meta-data.
319-
* Since this method is called before the exception translation feature is configured for
320-
* a datasource, this method can not rely on the SQLException translation functionality.
321-
* <p>Any exceptions will be wrapped in a MetaDataAccessException. This is a checked exception
322-
* and any calling code should catch and handle this exception. You can just log the
323-
* error and hope for the best, but there is probably a more serious error that will
324-
* reappear when you try to access the database again.
318+
* <p>This method will open a connection to the database and retrieve its meta-data.
319+
* Since this method is called before the exception translation feature is configured
320+
* for a DataSource, this method can not rely on SQLException translation itself.
321+
* <p>Any exceptions will be wrapped in a MetaDataAccessException. This is a checked
322+
* exception and any calling code should catch and handle this exception. You can just
323+
* log the error and hope for the best, but there is probably a more serious error that
324+
* will reappear when you try to access the database again.
325325
* @param dataSource the DataSource to extract meta-data for
326326
* @param action callback that will do the actual work
327327
* @return object containing the extracted information, as returned by
328328
* the DatabaseMetaDataCallback's {@code processMetaData} method
329329
* @throws MetaDataAccessException if meta-data access failed
330+
* @see java.sql.DatabaseMetaData
330331
*/
331-
public static Object extractDatabaseMetaData(DataSource dataSource, DatabaseMetaDataCallback action)
332+
public static <T> T extractDatabaseMetaData(DataSource dataSource, DatabaseMetaDataCallback<T> action)
332333
throws MetaDataAccessException {
333334

334335
Connection con = null;
335336
try {
336337
con = DataSourceUtils.getConnection(dataSource);
337-
DatabaseMetaData metaData = con.getMetaData();
338+
DatabaseMetaData metaData;
339+
try {
340+
metaData = con.getMetaData();
341+
}
342+
catch (SQLException ex) {
343+
if (DataSourceUtils.isConnectionTransactional(con, dataSource)) {
344+
// Probably a closed thread-bound Connection - retry against fresh Connection
345+
DataSourceUtils.releaseConnection(con, dataSource);
346+
con = null;
347+
logger.debug("Failed to obtain DatabaseMetaData from transactional Connection - " +
348+
"retrying against fresh Connection", ex);
349+
con = dataSource.getConnection();
350+
metaData = con.getMetaData();
351+
}
352+
else {
353+
throw ex;
354+
}
355+
}
338356
if (metaData == null) {
339357
// should only happen in test environments
340358
throw new MetaDataAccessException("DatabaseMetaData returned by Connection [" + con + "] was null");
@@ -365,7 +383,11 @@ public static Object extractDatabaseMetaData(DataSource dataSource, DatabaseMeta
365383
* @throws MetaDataAccessException if we couldn't access the DatabaseMetaData
366384
* or failed to invoke the specified method
367385
* @see java.sql.DatabaseMetaData
386+
* @deprecated as of 5.2.9, in favor of
387+
* {@link #extractDatabaseMetaData(DataSource, DatabaseMetaDataCallback)}
388+
* with a lambda expression or method reference and a generically typed result
368389
*/
390+
@Deprecated
369391
@SuppressWarnings("unchecked")
370392
public static <T> T extractDatabaseMetaData(DataSource dataSource, final String metaDataMethodName)
371393
throws MetaDataAccessException {

spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -35,6 +35,8 @@
3535
import org.springframework.jdbc.BadSqlGrammarException;
3636
import org.springframework.jdbc.InvalidResultSetAccessException;
3737
import org.springframework.lang.Nullable;
38+
import org.springframework.util.function.SingletonSupplier;
39+
import org.springframework.util.function.SupplierUtils;
3840

3941
/**
4042
* Implementation of {@link SQLExceptionTranslator} that analyzes vendor-specific error codes.
@@ -76,7 +78,7 @@ public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExcep
7678

7779
/** Error codes used by this translator. */
7880
@Nullable
79-
private SQLErrorCodes sqlErrorCodes;
81+
private SingletonSupplier<SQLErrorCodes> sqlErrorCodes;
8082

8183

8284
/**
@@ -120,7 +122,7 @@ public SQLErrorCodeSQLExceptionTranslator(String dbName) {
120122
*/
121123
public SQLErrorCodeSQLExceptionTranslator(SQLErrorCodes sec) {
122124
this();
123-
this.sqlErrorCodes = sec;
125+
this.sqlErrorCodes = SingletonSupplier.of(sec);
124126
}
125127

126128

@@ -134,7 +136,9 @@ public SQLErrorCodeSQLExceptionTranslator(SQLErrorCodes sec) {
134136
* @see java.sql.DatabaseMetaData#getDatabaseProductName()
135137
*/
136138
public void setDataSource(DataSource dataSource) {
137-
this.sqlErrorCodes = SQLErrorCodesFactory.getInstance().getErrorCodes(dataSource);
139+
this.sqlErrorCodes =
140+
SingletonSupplier.of(() -> SQLErrorCodesFactory.getInstance().resolveErrorCodes(dataSource));
141+
this.sqlErrorCodes.get(); // try early initialization - otherwise the supplier will retry later
138142
}
139143

140144
/**
@@ -146,15 +150,15 @@ public void setDataSource(DataSource dataSource) {
146150
* @see java.sql.DatabaseMetaData#getDatabaseProductName()
147151
*/
148152
public void setDatabaseProductName(String dbName) {
149-
this.sqlErrorCodes = SQLErrorCodesFactory.getInstance().getErrorCodes(dbName);
153+
this.sqlErrorCodes = SingletonSupplier.of(SQLErrorCodesFactory.getInstance().getErrorCodes(dbName));
150154
}
151155

152156
/**
153157
* Set custom error codes to be used for translation.
154158
* @param sec custom error codes to use
155159
*/
156160
public void setSqlErrorCodes(@Nullable SQLErrorCodes sec) {
157-
this.sqlErrorCodes = sec;
161+
this.sqlErrorCodes = SingletonSupplier.ofNullable(sec);
158162
}
159163

160164
/**
@@ -164,7 +168,7 @@ public void setSqlErrorCodes(@Nullable SQLErrorCodes sec) {
164168
*/
165169
@Nullable
166170
public SQLErrorCodes getSqlErrorCodes() {
167-
return this.sqlErrorCodes;
171+
return SupplierUtils.resolve(this.sqlErrorCodes);
168172
}
169173

170174

@@ -175,7 +179,6 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL
175179
if (sqlEx instanceof BatchUpdateException && sqlEx.getNextException() != null) {
176180
SQLException nestedSqlEx = sqlEx.getNextException();
177181
if (nestedSqlEx.getErrorCode() > 0 || nestedSqlEx.getSQLState() != null) {
178-
logger.debug("Using nested SQLException from the BatchUpdateException");
179182
sqlEx = nestedSqlEx;
180183
}
181184
}
@@ -187,8 +190,9 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL
187190
}
188191

189192
// Next, try the custom SQLException translator, if available.
190-
if (this.sqlErrorCodes != null) {
191-
SQLExceptionTranslator customTranslator = this.sqlErrorCodes.getCustomSqlExceptionTranslator();
193+
SQLErrorCodes sqlErrorCodes = getSqlErrorCodes();
194+
if (sqlErrorCodes != null) {
195+
SQLExceptionTranslator customTranslator = sqlErrorCodes.getCustomSqlExceptionTranslator();
192196
if (customTranslator != null) {
193197
DataAccessException customDex = customTranslator.translate(task, sql, sqlEx);
194198
if (customDex != null) {
@@ -198,9 +202,9 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL
198202
}
199203

200204
// Check SQLErrorCodes with corresponding error code, if available.
201-
if (this.sqlErrorCodes != null) {
205+
if (sqlErrorCodes != null) {
202206
String errorCode;
203-
if (this.sqlErrorCodes.isUseSqlStateForTranslation()) {
207+
if (sqlErrorCodes.isUseSqlStateForTranslation()) {
204208
errorCode = sqlEx.getSQLState();
205209
}
206210
else {
@@ -215,7 +219,7 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL
215219

216220
if (errorCode != null) {
217221
// Look for defined custom translations first.
218-
CustomSQLErrorCodesTranslation[] customTranslations = this.sqlErrorCodes.getCustomTranslations();
222+
CustomSQLErrorCodesTranslation[] customTranslations = sqlErrorCodes.getCustomTranslations();
219223
if (customTranslations != null) {
220224
for (CustomSQLErrorCodesTranslation customTranslation : customTranslations) {
221225
if (Arrays.binarySearch(customTranslation.getErrorCodes(), errorCode) >= 0 &&
@@ -230,43 +234,43 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL
230234
}
231235
}
232236
// Next, look for grouped error codes.
233-
if (Arrays.binarySearch(this.sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0) {
237+
if (Arrays.binarySearch(sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0) {
234238
logTranslation(task, sql, sqlEx, false);
235239
return new BadSqlGrammarException(task, (sql != null ? sql : ""), sqlEx);
236240
}
237-
else if (Arrays.binarySearch(this.sqlErrorCodes.getInvalidResultSetAccessCodes(), errorCode) >= 0) {
241+
else if (Arrays.binarySearch(sqlErrorCodes.getInvalidResultSetAccessCodes(), errorCode) >= 0) {
238242
logTranslation(task, sql, sqlEx, false);
239243
return new InvalidResultSetAccessException(task, (sql != null ? sql : ""), sqlEx);
240244
}
241-
else if (Arrays.binarySearch(this.sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) {
245+
else if (Arrays.binarySearch(sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) {
242246
logTranslation(task, sql, sqlEx, false);
243247
return new DuplicateKeyException(buildMessage(task, sql, sqlEx), sqlEx);
244248
}
245-
else if (Arrays.binarySearch(this.sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode) >= 0) {
249+
else if (Arrays.binarySearch(sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode) >= 0) {
246250
logTranslation(task, sql, sqlEx, false);
247251
return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx);
248252
}
249-
else if (Arrays.binarySearch(this.sqlErrorCodes.getPermissionDeniedCodes(), errorCode) >= 0) {
253+
else if (Arrays.binarySearch(sqlErrorCodes.getPermissionDeniedCodes(), errorCode) >= 0) {
250254
logTranslation(task, sql, sqlEx, false);
251255
return new PermissionDeniedDataAccessException(buildMessage(task, sql, sqlEx), sqlEx);
252256
}
253-
else if (Arrays.binarySearch(this.sqlErrorCodes.getDataAccessResourceFailureCodes(), errorCode) >= 0) {
257+
else if (Arrays.binarySearch(sqlErrorCodes.getDataAccessResourceFailureCodes(), errorCode) >= 0) {
254258
logTranslation(task, sql, sqlEx, false);
255259
return new DataAccessResourceFailureException(buildMessage(task, sql, sqlEx), sqlEx);
256260
}
257-
else if (Arrays.binarySearch(this.sqlErrorCodes.getTransientDataAccessResourceCodes(), errorCode) >= 0) {
261+
else if (Arrays.binarySearch(sqlErrorCodes.getTransientDataAccessResourceCodes(), errorCode) >= 0) {
258262
logTranslation(task, sql, sqlEx, false);
259263
return new TransientDataAccessResourceException(buildMessage(task, sql, sqlEx), sqlEx);
260264
}
261-
else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotAcquireLockCodes(), errorCode) >= 0) {
265+
else if (Arrays.binarySearch(sqlErrorCodes.getCannotAcquireLockCodes(), errorCode) >= 0) {
262266
logTranslation(task, sql, sqlEx, false);
263267
return new CannotAcquireLockException(buildMessage(task, sql, sqlEx), sqlEx);
264268
}
265-
else if (Arrays.binarySearch(this.sqlErrorCodes.getDeadlockLoserCodes(), errorCode) >= 0) {
269+
else if (Arrays.binarySearch(sqlErrorCodes.getDeadlockLoserCodes(), errorCode) >= 0) {
266270
logTranslation(task, sql, sqlEx, false);
267271
return new DeadlockLoserDataAccessException(buildMessage(task, sql, sqlEx), sqlEx);
268272
}
269-
else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotSerializeTransactionCodes(), errorCode) >= 0) {
273+
else if (Arrays.binarySearch(sqlErrorCodes.getCannotSerializeTransactionCodes(), errorCode) >= 0) {
270274
logTranslation(task, sql, sqlEx, false);
271275
return new CannotSerializeTransactionException(buildMessage(task, sql, sqlEx), sqlEx);
272276
}
@@ -276,7 +280,7 @@ else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotSerializeTransactionCod
276280
// We couldn't identify it more precisely - let's hand it over to the SQLState fallback translator.
277281
if (logger.isDebugEnabled()) {
278282
String codes;
279-
if (this.sqlErrorCodes != null && this.sqlErrorCodes.isUseSqlStateForTranslation()) {
283+
if (sqlErrorCodes != null && sqlErrorCodes.isUseSqlStateForTranslation()) {
280284
codes = "SQL state '" + sqlEx.getSQLState() + "', error code '" + sqlEx.getErrorCode();
281285
}
282286
else {

0 commit comments

Comments
 (0)