Skip to content

Commit 0ecdf0b

Browse files
committed
Retry SQLErrorCodesFactory retrieval if DatabaseMetaData access failed
Closes gh-25681
1 parent 81d5e66 commit 0ecdf0b

File tree

5 files changed

+126
-56
lines changed

5 files changed

+126
-56
lines changed

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

Lines changed: 27 additions & 9 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.
@@ -299,26 +299,44 @@ else if (obj instanceof java.sql.Date) {
299299

300300
/**
301301
* Extract database meta-data via the given DatabaseMetaDataCallback.
302-
* <p>This method will open a connection to the database and retrieve the database meta-data.
303-
* Since this method is called before the exception translation feature is configured for
304-
* a datasource, this method can not rely on the SQLException translation functionality.
305-
* <p>Any exceptions will be wrapped in a MetaDataAccessException. This is a checked exception
306-
* and any calling code should catch and handle this exception. You can just log the
307-
* error and hope for the best, but there is probably a more serious error that will
308-
* reappear when you try to access the database again.
302+
* <p>This method will open a connection to the database and retrieve its meta-data.
303+
* Since this method is called before the exception translation feature is configured
304+
* for a DataSource, this method can not rely on SQLException translation itself.
305+
* <p>Any exceptions will be wrapped in a MetaDataAccessException. This is a checked
306+
* exception and any calling code should catch and handle this exception. You can just
307+
* log the error and hope for the best, but there is probably a more serious error that
308+
* will reappear when you try to access the database again.
309309
* @param dataSource the DataSource to extract meta-data for
310310
* @param action callback that will do the actual work
311311
* @return object containing the extracted information, as returned by
312312
* the DatabaseMetaDataCallback's {@code processMetaData} method
313313
* @throws MetaDataAccessException if meta-data access failed
314+
* @see java.sql.DatabaseMetaData
314315
*/
315316
public static Object extractDatabaseMetaData(DataSource dataSource, DatabaseMetaDataCallback action)
316317
throws MetaDataAccessException {
317318

318319
Connection con = null;
319320
try {
320321
con = DataSourceUtils.getConnection(dataSource);
321-
DatabaseMetaData metaData = con.getMetaData();
322+
DatabaseMetaData metaData;
323+
try {
324+
metaData = con.getMetaData();
325+
}
326+
catch (SQLException ex) {
327+
if (DataSourceUtils.isConnectionTransactional(con, dataSource)) {
328+
// Probably a closed thread-bound Connection - retry against fresh Connection
329+
DataSourceUtils.releaseConnection(con, dataSource);
330+
con = null;
331+
logger.debug("Failed to obtain DatabaseMetaData from transactional Connection - " +
332+
"retrying against fresh Connection", ex);
333+
con = dataSource.getConnection();
334+
metaData = con.getMetaData();
335+
}
336+
else {
337+
throw ex;
338+
}
339+
}
322340
if (metaData == null) {
323341
// should only happen in test environments
324342
throw new MetaDataAccessException("DatabaseMetaData returned by Connection [" + con + "] was null");

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

Lines changed: 31 additions & 27 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
/**
@@ -88,7 +90,7 @@ public SQLErrorCodeSQLExceptionTranslator() {
8890
}
8991

9092
/**
91-
* Create a SQL error code translator for the given DataSource.
93+
* Create an SQL error code translator for the given DataSource.
9294
* Invoking this constructor will cause a Connection to be obtained
9395
* from the DataSource to get the meta-data.
9496
* @param dataSource the DataSource to use to find meta-data and establish
@@ -101,7 +103,7 @@ public SQLErrorCodeSQLExceptionTranslator(DataSource dataSource) {
101103
}
102104

103105
/**
104-
* Create a SQL error code translator for the given database product name.
106+
* Create an SQL error code translator for the given database product name.
105107
* Invoking this constructor will avoid obtaining a Connection from the
106108
* DataSource to get the meta-data.
107109
* @param dbName the database product name that identifies the error codes entry
@@ -114,13 +116,13 @@ public SQLErrorCodeSQLExceptionTranslator(String dbName) {
114116
}
115117

116118
/**
117-
* Create a SQLErrorCode translator given these error codes.
119+
* Create an SQLErrorCode translator given these error codes.
118120
* Does not require a database meta-data lookup to be performed using a connection.
119121
* @param sec error codes
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 {

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

Lines changed: 21 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.
@@ -159,6 +159,7 @@ protected Resource loadResource(String path) {
159159
* <p>No need for a database meta-data lookup.
160160
* @param databaseName the database name (must not be {@code null})
161161
* @return the {@code SQLErrorCodes} instance for the given database
162+
* (never {@code null}; potentially empty)
162163
* @throws IllegalArgumentException if the supplied database name is {@code null}
163164
*/
164165
public SQLErrorCodes getErrorCodes(String databaseName) {
@@ -195,9 +196,26 @@ public SQLErrorCodes getErrorCodes(String databaseName) {
195196
* instance if no {@code SQLErrorCodes} were found.
196197
* @param dataSource the {@code DataSource} identifying the database
197198
* @return the corresponding {@code SQLErrorCodes} object
199+
* (never {@code null}; potentially empty)
198200
* @see java.sql.DatabaseMetaData#getDatabaseProductName()
199201
*/
200202
public SQLErrorCodes getErrorCodes(DataSource dataSource) {
203+
SQLErrorCodes sec = resolveErrorCodes(dataSource);
204+
return (sec != null ? sec : new SQLErrorCodes());
205+
}
206+
207+
/**
208+
* Return {@link SQLErrorCodes} for the given {@link DataSource},
209+
* evaluating "databaseProductName" from the
210+
* {@link java.sql.DatabaseMetaData}, or {@code null} if case
211+
* of a JDBC meta-data access problem.
212+
* @param dataSource the {@code DataSource} identifying the database
213+
* @return the corresponding {@code SQLErrorCodes} object,
214+
* or {@code null} in case of a JDBC meta-data access problem
215+
* @see java.sql.DatabaseMetaData#getDatabaseProductName()
216+
*/
217+
@Nullable
218+
SQLErrorCodes resolveErrorCodes(DataSource dataSource) {
201219
Assert.notNull(dataSource, "DataSource must not be null");
202220
if (logger.isDebugEnabled()) {
203221
logger.debug("Looking up default SQLErrorCodes for DataSource [" + identify(dataSource) + "]");
@@ -218,10 +236,9 @@ public SQLErrorCodes getErrorCodes(DataSource dataSource) {
218236
}
219237
}
220238
catch (MetaDataAccessException ex) {
221-
logger.warn("Error while extracting database name - falling back to empty error codes", ex);
239+
logger.warn("Error while extracting database name", ex);
222240
}
223-
// Fallback is to return an empty SQLErrorCodes instance.
224-
return new SQLErrorCodes();
241+
return null;
225242
}
226243
}
227244
}

spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslatorTests.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 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,12 +17,17 @@
1717
package org.springframework.jdbc.support;
1818

1919
import java.sql.BatchUpdateException;
20+
import java.sql.Connection;
2021
import java.sql.DataTruncation;
22+
import java.sql.DatabaseMetaData;
2123
import java.sql.SQLException;
2224

25+
import javax.sql.DataSource;
26+
2327
import org.junit.Rule;
2428
import org.junit.Test;
2529
import org.junit.rules.ExpectedException;
30+
import org.mockito.Mockito;
2631

2732
import org.springframework.dao.CannotAcquireLockException;
2833
import org.springframework.dao.CannotSerializeTransactionException;
@@ -36,6 +41,9 @@
3641
import org.springframework.lang.Nullable;
3742

3843
import static org.junit.Assert.*;
44+
import static org.mockito.BDDMockito.given;
45+
import static org.mockito.Mockito.mock;
46+
import static org.mockito.Mockito.verify;
3947

4048
/**
4149
* @author Rod Johnson
@@ -181,4 +189,28 @@ public void customExceptionTranslation() {
181189
customTranslation.setExceptionClass(String.class);
182190
}
183191

192+
@Test
193+
public void dataSourceInitialization() throws Exception {
194+
SQLException connectionException = new SQLException();
195+
SQLException duplicateKeyException = new SQLException("test", "", 1);
196+
197+
DataSource dataSource = mock(DataSource.class);
198+
given(dataSource.getConnection()).willThrow(connectionException);
199+
200+
SQLErrorCodeSQLExceptionTranslator sext = new SQLErrorCodeSQLExceptionTranslator(dataSource);
201+
assertFalse(sext.translate("test", null, duplicateKeyException) instanceof DuplicateKeyException);
202+
203+
DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
204+
given(databaseMetaData.getDatabaseProductName()).willReturn("Oracle");
205+
206+
Connection connection = mock(Connection.class);
207+
given(connection.getMetaData()).willReturn(databaseMetaData);
208+
209+
Mockito.reset(dataSource);
210+
given(dataSource.getConnection()).willReturn(connection);
211+
assertTrue(sext.translate("test", null, duplicateKeyException) instanceof DuplicateKeyException);
212+
213+
verify(connection).close();
214+
}
215+
184216
}

0 commit comments

Comments
 (0)