Skip to content

Commit 8190e78

Browse files
committed
SQLErrorCodesFactory provides unregisterDatabase method
This commit also migrates from a WeakHashMap to a ConcurrentReferenceHashMap, allowing for concurrent access to existing cache entries. Issue: SPR-15006 (cherry picked from commit b825528)
1 parent 4571975 commit 8190e78

File tree

1 file changed

+76
-53
lines changed

1 file changed

+76
-53
lines changed

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

Lines changed: 76 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2016 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.
@@ -18,7 +18,6 @@
1818

1919
import java.util.Collections;
2020
import java.util.Map;
21-
import java.util.WeakHashMap;
2221
import javax.sql.DataSource;
2322

2423
import org.apache.commons.logging.Log;
@@ -30,6 +29,7 @@
3029
import org.springframework.core.io.ClassPathResource;
3130
import org.springframework.core.io.Resource;
3231
import org.springframework.util.Assert;
32+
import org.springframework.util.ConcurrentReferenceHashMap;
3333
import org.springframework.util.PatternMatchUtils;
3434

3535
/**
@@ -85,7 +85,8 @@ public static SQLErrorCodesFactory getInstance() {
8585
/**
8686
* Map to cache the SQLErrorCodes instance per DataSource.
8787
*/
88-
private final Map<DataSource, SQLErrorCodes> dataSourceCache = new WeakHashMap<DataSource, SQLErrorCodes>(16);
88+
private final Map<DataSource, SQLErrorCodes> dataSourceCache =
89+
new ConcurrentReferenceHashMap<DataSource, SQLErrorCodes>(16);
8990

9091

9192
/**
@@ -153,33 +154,33 @@ protected Resource loadResource(String path) {
153154
/**
154155
* Return the {@link SQLErrorCodes} instance for the given database.
155156
* <p>No need for a database metadata lookup.
156-
* @param dbName the database name (must not be {@code null})
157+
* @param databaseName the database name (must not be {@code null})
157158
* @return the {@code SQLErrorCodes} instance for the given database
158159
* @throws IllegalArgumentException if the supplied database name is {@code null}
159160
*/
160-
public SQLErrorCodes getErrorCodes(String dbName) {
161-
Assert.notNull(dbName, "Database product name must not be null");
161+
public SQLErrorCodes getErrorCodes(String databaseName) {
162+
Assert.notNull(databaseName, "Database product name must not be null");
162163

163-
SQLErrorCodes sec = this.errorCodesMap.get(dbName);
164+
SQLErrorCodes sec = this.errorCodesMap.get(databaseName);
164165
if (sec == null) {
165166
for (SQLErrorCodes candidate : this.errorCodesMap.values()) {
166-
if (PatternMatchUtils.simpleMatch(candidate.getDatabaseProductNames(), dbName)) {
167+
if (PatternMatchUtils.simpleMatch(candidate.getDatabaseProductNames(), databaseName)) {
167168
sec = candidate;
168169
break;
169170
}
170171
}
171172
}
172173
if (sec != null) {
173-
checkCustomTranslatorRegistry(dbName, sec);
174+
checkCustomTranslatorRegistry(databaseName, sec);
174175
if (logger.isDebugEnabled()) {
175-
logger.debug("SQL error codes for '" + dbName + "' found");
176+
logger.debug("SQL error codes for '" + databaseName + "' found");
176177
}
177178
return sec;
178179
}
179180

180181
// Could not find the database among the defined ones.
181182
if (logger.isDebugEnabled()) {
182-
logger.debug("SQL error codes for '" + dbName + "' not found");
183+
logger.debug("SQL error codes for '" + databaseName + "' not found");
183184
}
184185
return new SQLErrorCodes();
185186
}
@@ -196,75 +197,97 @@ public SQLErrorCodes getErrorCodes(String dbName) {
196197
public SQLErrorCodes getErrorCodes(DataSource dataSource) {
197198
Assert.notNull(dataSource, "DataSource must not be null");
198199
if (logger.isDebugEnabled()) {
199-
logger.debug("Looking up default SQLErrorCodes for DataSource [" + dataSource + "]");
200+
logger.debug("Looking up default SQLErrorCodes for DataSource [" + identify(dataSource) + "]");
200201
}
201202

202-
synchronized (this.dataSourceCache) {
203-
// Let's avoid looking up database product info if we can.
204-
SQLErrorCodes sec = this.dataSourceCache.get(dataSource);
205-
if (sec != null) {
206-
if (logger.isDebugEnabled()) {
207-
logger.debug("SQLErrorCodes found in cache for DataSource [" +
208-
dataSource.getClass().getName() + '@' + Integer.toHexString(dataSource.hashCode()) + "]");
209-
}
210-
return sec;
211-
}
212-
// We could not find it - got to look it up.
213-
try {
214-
String dbName = (String) JdbcUtils.extractDatabaseMetaData(dataSource, "getDatabaseProductName");
215-
if (dbName != null) {
216-
if (logger.isDebugEnabled()) {
217-
logger.debug("Database product name cached for DataSource [" +
218-
dataSource.getClass().getName() + '@' + Integer.toHexString(dataSource.hashCode()) +
219-
"]: name is '" + dbName + "'");
203+
// Try efficient lock-free access for existing cache entry
204+
SQLErrorCodes sec = this.dataSourceCache.get(dataSource);
205+
if (sec == null) {
206+
synchronized (this.dataSourceCache) {
207+
// Double-check within full dataSourceCache lock
208+
sec = this.dataSourceCache.get(dataSource);
209+
if (sec == null) {
210+
// We could not find it - got to look it up.
211+
try {
212+
String name = (String) JdbcUtils.extractDatabaseMetaData(dataSource, "getDatabaseProductName");
213+
if (name != null) {
214+
return registerDatabase(dataSource, name);
215+
}
216+
}
217+
catch (MetaDataAccessException ex) {
218+
logger.warn("Error while extracting database name - falling back to empty error codes", ex);
219+
// Fallback is to return an empty SQLErrorCodes instance.
220+
return new SQLErrorCodes();
220221
}
221-
sec = getErrorCodes(dbName);
222-
this.dataSourceCache.put(dataSource, sec);
223-
return sec;
224222
}
225223
}
226-
catch (MetaDataAccessException ex) {
227-
logger.warn("Error while extracting database product name - falling back to empty error codes", ex);
228-
}
229224
}
230225

231-
// Fallback is to return an empty SQLErrorCodes instance.
232-
return new SQLErrorCodes();
226+
if (logger.isDebugEnabled()) {
227+
logger.debug("SQLErrorCodes found in cache for DataSource [" + identify(dataSource) + "]");
228+
}
229+
230+
return sec;
233231
}
234232

235233
/**
236234
* Associate the specified database name with the given {@link DataSource}.
237235
* @param dataSource the {@code DataSource} identifying the database
238-
* @param dbName the corresponding database name as stated in the error codes
236+
* @param databaseName the corresponding database name as stated in the error codes
239237
* definition file (must not be {@code null})
240-
* @return the corresponding {@code SQLErrorCodes} object
238+
* @return the corresponding {@code SQLErrorCodes} object (never {@code null})
239+
* @see #unregisterDatabase(DataSource)
241240
*/
242-
public SQLErrorCodes registerDatabase(DataSource dataSource, String dbName) {
243-
synchronized (this.dataSourceCache) {
244-
SQLErrorCodes sec = getErrorCodes(dbName);
245-
this.dataSourceCache.put(dataSource, sec);
246-
return sec;
241+
public SQLErrorCodes registerDatabase(DataSource dataSource, String databaseName) {
242+
SQLErrorCodes sec = getErrorCodes(databaseName);
243+
if (logger.isDebugEnabled()) {
244+
logger.debug("Caching SQL error codes for DataSource [" + identify(dataSource) +
245+
"]: database product name is '" + databaseName + "'");
247246
}
247+
this.dataSourceCache.put(dataSource, sec);
248+
return sec;
249+
}
250+
251+
/**
252+
* Clear the cache for the specified {@link DataSource}, if registered.
253+
* @param dataSource the {@code DataSource} identifying the database
254+
* @return the corresponding {@code SQLErrorCodes} object,
255+
* or {@code null} if not registered
256+
* @since 4.3.5
257+
* @see #registerDatabase(DataSource, String)
258+
*/
259+
public SQLErrorCodes unregisterDatabase(DataSource dataSource) {
260+
return this.dataSourceCache.remove(dataSource);
261+
}
262+
263+
/**
264+
* Build an identification String for the given {@link DataSource},
265+
* primarily for logging purposes.
266+
* @param dataSource the {@code DataSource} to introspect
267+
* @return the identification String
268+
*/
269+
private String identify(DataSource dataSource) {
270+
return dataSource.getClass().getName() + '@' + Integer.toHexString(dataSource.hashCode());
248271
}
249272

250273
/**
251274
* Check the {@link CustomSQLExceptionTranslatorRegistry} for any entries.
252275
*/
253-
private void checkCustomTranslatorRegistry(String dbName, SQLErrorCodes dbCodes) {
276+
private void checkCustomTranslatorRegistry(String databaseName, SQLErrorCodes errorCodes) {
254277
SQLExceptionTranslator customTranslator =
255-
CustomSQLExceptionTranslatorRegistry.getInstance().findTranslatorForDatabase(dbName);
278+
CustomSQLExceptionTranslatorRegistry.getInstance().findTranslatorForDatabase(databaseName);
256279
if (customTranslator != null) {
257-
if (dbCodes.getCustomSqlExceptionTranslator() != null) {
280+
if (errorCodes.getCustomSqlExceptionTranslator() != null && logger.isWarnEnabled()) {
258281
logger.warn("Overriding already defined custom translator '" +
259-
dbCodes.getCustomSqlExceptionTranslator().getClass().getSimpleName() +
282+
errorCodes.getCustomSqlExceptionTranslator().getClass().getSimpleName() +
260283
" with '" + customTranslator.getClass().getSimpleName() +
261-
"' found in the CustomSQLExceptionTranslatorRegistry for database " + dbName);
284+
"' found in the CustomSQLExceptionTranslatorRegistry for database '" + databaseName + "'");
262285
}
263-
else {
286+
else if (logger.isInfoEnabled()) {
264287
logger.info("Using custom translator '" + customTranslator.getClass().getSimpleName() +
265-
"' found in the CustomSQLExceptionTranslatorRegistry for database " + dbName);
288+
"' found in the CustomSQLExceptionTranslatorRegistry for database '" + databaseName + "'");
266289
}
267-
dbCodes.setCustomSqlExceptionTranslator(customTranslator);
290+
errorCodes.setCustomSqlExceptionTranslator(customTranslator);
268291
}
269292
}
270293

0 commit comments

Comments
 (0)