diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java index 2d45cc104f6c..f3159f1669d5 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -1084,6 +1084,9 @@ protected void doClose() { // Let subclasses do some final clean-up if they wish... onClose(); + // Reset common introspection caches to avoid class reference leaks. + resetCommonCaches(); + // Reset local application listeners to pre-refresh state. if (this.earlyApplicationListeners != null) { this.applicationListeners.clear(); diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index 4d740a8195be..5744e9a900de 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -1326,6 +1326,9 @@ public static boolean isSynthesizedAnnotation(@Nullable Annotation annotation) { public static void clearCache() { AnnotationTypeMappings.clearCache(); AnnotationsScanner.clearCache(); + AttributeMethods.cache.clear(); + RepeatableContainers.cache.clear(); + OrderUtils.orderCache.clear(); } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java index fe059f524152..e8b03231538a 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java @@ -39,9 +39,7 @@ final class AttributeMethods { static final AttributeMethods NONE = new AttributeMethods(null, new Method[0]); - - private static final Map, AttributeMethods> cache = - new ConcurrentReferenceHashMap<>(); + static final Map, AttributeMethods> cache = new ConcurrentReferenceHashMap<>(); private static final Comparator methodComparator = (m1, m2) -> { if (m1 != null && m2 != null) { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java index bc38dcec195f..19fbc577b885 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.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. @@ -41,7 +41,7 @@ public abstract class OrderUtils { private static final String JAVAX_PRIORITY_ANNOTATION = "jakarta.annotation.Priority"; /** Cache for @Order value (or NOT_ANNOTATED marker) per Class. */ - private static final Map orderCache = new ConcurrentReferenceHashMap<>(64); + static final Map orderCache = new ConcurrentReferenceHashMap<>(64); /** diff --git a/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java index c1b2fffd8628..7d9f4b135c46 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java @@ -43,6 +43,8 @@ */ public abstract class RepeatableContainers { + static final Map, Object> cache = new ConcurrentReferenceHashMap<>(); + @Nullable private final RepeatableContainers parent; @@ -141,8 +143,6 @@ public static RepeatableContainers none() { */ private static class StandardRepeatableContainers extends RepeatableContainers { - private static final Map, Object> cache = new ConcurrentReferenceHashMap<>(); - private static final Object NONE = new Object(); private static final StandardRepeatableContainers INSTANCE = new StandardRepeatableContainers(); diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java index e67eb5a4342a..34426cc33f48 100644 --- a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java +++ b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java @@ -77,7 +77,25 @@ public static Log getLog(String name) { */ @Deprecated public static LogFactory getFactory() { - return new LogFactory() {}; + return new LogFactory() { + @Override + public Object getAttribute(String name) { + return null; + } + @Override + public String[] getAttributeNames() { + return new String[0]; + } + @Override + public void removeAttribute(String name) { + } + @Override + public void setAttribute(String name, Object value) { + } + @Override + public void release() { + } + }; } /** @@ -106,29 +124,19 @@ public Log getInstance(String name) { // Just in case some code happens to call uncommon Commons Logging methods... @Deprecated - public Object getAttribute(String name) { - return null; - } + public abstract Object getAttribute(String name); @Deprecated - public String[] getAttributeNames() { - return new String[0]; - } + public abstract String[] getAttributeNames(); @Deprecated - public void removeAttribute(String name) { - // do nothing - } + public abstract void removeAttribute(String name); @Deprecated - public void setAttribute(String name, Object value) { - // do nothing - } + public abstract void setAttribute(String name, Object value); @Deprecated - public void release() { - // do nothing - } + public abstract void release(); @Deprecated public static void release(ClassLoader classLoader) { diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java index 1e961e68f62f..355d4a30bce4 100644 --- a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java +++ b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java @@ -80,4 +80,8 @@ public String[] getAttributeNames() { return this.attributes.keySet().toArray(new String[0]); } + @Override + public void release() { + } + } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateQueryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateQueryTests.java index 435fe9e2d586..2bf1b7f62077 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateQueryTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateQueryTests.java @@ -50,10 +50,10 @@ */ public class JdbcTemplateQueryTests { - private Connection connection = mock(); - private DataSource dataSource = mock(); + private Connection connection = mock(); + private Statement statement = mock(); private PreparedStatement preparedStatement = mock(); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java index d8a258eb5c49..59b298a6f001 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java @@ -75,18 +75,18 @@ */ public class JdbcTemplateTests { - private Connection connection = mock(); - private DataSource dataSource = mock(); + private Connection connection = mock(); + private Statement statement = mock(); private PreparedStatement preparedStatement = mock(); - private ResultSet resultSet = mock(); - private CallableStatement callableStatement = mock(); + private ResultSet resultSet = mock(); + private JdbcTemplate template = new JdbcTemplate(this.dataSource); @@ -136,9 +136,9 @@ public void testBogusUpdate() throws Exception { given(this.preparedStatement.executeUpdate()).willThrow(sqlException); Dispatcher d = new Dispatcher(idParam, sql); - assertThatExceptionOfType(UncategorizedSQLException.class).isThrownBy(() -> - this.template.update(d)) - .withCause(sqlException); + assertThatExceptionOfType(UncategorizedSQLException.class) + .isThrownBy(() -> this.template.update(d)) + .withCause(sqlException); verify(this.preparedStatement).setInt(1, idParam); verify(this.preparedStatement).close(); verify(this.connection, atLeastOnce()).close(); @@ -371,9 +371,9 @@ public void testSqlUpdateEncountersSqlException() throws Exception { given(this.statement.executeUpdate(sql)).willThrow(sqlException); given(this.connection.createStatement()).willReturn(this.statement); - assertThatExceptionOfType(DataAccessException.class).isThrownBy(() -> - this.template.update(sql)) - .withCause(sqlException); + assertThatExceptionOfType(DataAccessException.class) + .isThrownBy(() -> this.template.update(sql)) + .withCause(sqlException); verify(this.statement).close(); verify(this.connection, atLeastOnce()).close(); } @@ -700,9 +700,9 @@ public int getBatchSize() { }; try { - assertThatExceptionOfType(DataAccessException.class).isThrownBy(() -> - this.template.batchUpdate(sql, setter)) - .withCause(sqlException); + assertThatExceptionOfType(DataAccessException.class) + .isThrownBy(() -> this.template.batchUpdate(sql, setter)) + .withCause(sqlException); } finally { verify(this.preparedStatement, times(2)).addBatch(); @@ -804,9 +804,9 @@ public void testCouldNotGetConnectionForOperationOrExceptionTranslator() throws JdbcTemplate template = new JdbcTemplate(this.dataSource, false); RowCountCallbackHandler rcch = new RowCountCallbackHandler(); - assertThatExceptionOfType(CannotGetJdbcConnectionException.class).isThrownBy(() -> - template.query("SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3", rcch)) - .withCause(sqlException); + assertThatExceptionOfType(CannotGetJdbcConnectionException.class) + .isThrownBy(() -> template.query("SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3", rcch)) + .withCause(sqlException); } @Test @@ -818,9 +818,9 @@ public void testCouldNotGetConnectionForOperationWithLazyExceptionTranslator() t this.template.afterPropertiesSet(); RowCountCallbackHandler rcch = new RowCountCallbackHandler(); - assertThatExceptionOfType(CannotGetJdbcConnectionException.class).isThrownBy(() -> - this.template.query("SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3", rcch)) - .withCause(sqlException); + assertThatExceptionOfType(CannotGetJdbcConnectionException.class) + .isThrownBy(() -> this.template.query("SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3", rcch)) + .withCause(sqlException); } @Test @@ -858,9 +858,9 @@ private void doTestCouldNotGetConnectionInOperationWithExceptionTranslatorInitia this.template.afterPropertiesSet(); } RowCountCallbackHandler rcch = new RowCountCallbackHandler(); - assertThatExceptionOfType(CannotGetJdbcConnectionException.class).isThrownBy(() -> - this.template.query("SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3", rcch)) - .withCause(sqlException); + assertThatExceptionOfType(CannotGetJdbcConnectionException.class) + .isThrownBy(() -> this.template.query("SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3", rcch)) + .withCause(sqlException); } @Test @@ -887,9 +887,9 @@ public void testPreparedStatementSetterFails() throws Exception { given(this.preparedStatement.executeUpdate()).willThrow(sqlException); PreparedStatementSetter pss = ps -> ps.setString(1, name); - assertThatExceptionOfType(DataAccessException.class).isThrownBy(() -> - new JdbcTemplate(this.dataSource).update(sql, pss)) - .withCause(sqlException); + assertThatExceptionOfType(DataAccessException.class) + .isThrownBy(() -> new JdbcTemplate(this.dataSource).update(sql, pss)) + .withCause(sqlException); verify(this.preparedStatement).setString(1, name); verify(this.preparedStatement).close(); verify(this.connection, atLeastOnce()).close(); @@ -925,9 +925,9 @@ public void testFatalWarning() throws Exception { t.setIgnoreWarnings(false); ResultSetExtractor extractor = rs -> rs.getByte(1); - assertThatExceptionOfType(SQLWarningException.class).isThrownBy(() -> - t.query(sql, extractor)) - .withCause(warnings); + assertThatExceptionOfType(SQLWarningException.class) + .isThrownBy(() -> t.query(sql, extractor)) + .withCause(warnings); verify(this.resultSet).close(); verify(this.preparedStatement).close(); verify(this.connection).close(); @@ -968,7 +968,7 @@ public void testSQLErrorCodeTranslation() throws Exception { this.template.query(sql, (RowCallbackHandler) rs -> { throw sqlException; })) - .withCause(sqlException); + .withCause(sqlException); verify(this.resultSet).close(); verify(this.preparedStatement).close(); verify(this.connection, atLeastOnce()).close(); @@ -988,7 +988,7 @@ public void testSQLErrorCodeTranslationWithSpecifiedDatabaseName() throws Except this.template.query(sql, (RowCallbackHandler) rs -> { throw sqlException; })) - .withCause(sqlException); + .withCause(sqlException); verify(this.resultSet).close(); verify(this.preparedStatement).close(); verify(this.connection).close(); @@ -1018,7 +1018,7 @@ public void testUseCustomExceptionTranslator() throws Exception { template.query(sql, (RowCallbackHandler) rs -> { throw sqlException; })) - .withCause(sqlException); + .withCause(sqlException); verify(this.resultSet).close(); verify(this.preparedStatement).close(); verify(this.connection).close(); diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java index 0a4c6f101ceb..f3edbf16b9af 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java @@ -338,8 +338,8 @@ protected Mono doCleanupAfterCompletion(TransactionSynchronizationManager ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction; if (txObject.hasSavepoint()) { - // Just release the savepoint - return Mono.defer(txObject::releaseSavepoint); + // Just release the savepoint, keeping the transactional connection. + return txObject.releaseSavepoint(); } // Remove the connection holder from the context, if exposed. @@ -348,30 +348,25 @@ protected Mono doCleanupAfterCompletion(TransactionSynchronizationManager } // Reset connection. - Connection con = txObject.getConnectionHolder().getConnection(); - - Mono afterCleanup = Mono.empty(); - - Mono releaseConnectionStep = Mono.defer(() -> { - try { - if (txObject.isNewConnectionHolder()) { - if (logger.isDebugEnabled()) { - logger.debug("Releasing R2DBC Connection [" + con + "] after transaction"); - } - Mono releaseMono = ConnectionFactoryUtils.releaseConnection(con, obtainConnectionFactory()); - if (logger.isDebugEnabled()) { - releaseMono = releaseMono.doOnError( - ex -> logger.debug(String.format("Error ignored during cleanup: %s", ex))); - } - return releaseMono.onErrorComplete(); + try { + if (txObject.isNewConnectionHolder()) { + Connection con = txObject.getConnectionHolder().getConnection(); + if (logger.isDebugEnabled()) { + logger.debug("Releasing R2DBC Connection [" + con + "] after transaction"); } + Mono releaseMono = ConnectionFactoryUtils.releaseConnection(con, obtainConnectionFactory()); + if (logger.isDebugEnabled()) { + releaseMono = releaseMono.doOnError( + ex -> logger.debug(String.format("Error ignored during cleanup: %s", ex))); + } + return releaseMono.onErrorComplete(); } - finally { - txObject.getConnectionHolder().clear(); - } - return Mono.empty(); - }); - return afterCleanup.then(releaseConnectionStep); + } + finally { + txObject.getConnectionHolder().clear(); + } + + return Mono.empty(); }); } @@ -517,30 +512,35 @@ public boolean isTransactionActive() { } public boolean hasSavepoint() { - return this.savepointName != null; + return (this.savepointName != null); } public Mono createSavepoint() { ConnectionHolder holder = getConnectionHolder(); - this.savepointName = holder.nextSavepoint(); - return Mono.from(holder.getConnection().createSavepoint(this.savepointName)); + String currentSavepoint = holder.nextSavepoint(); + this.savepointName = currentSavepoint; + return Mono.from(holder.getConnection().createSavepoint(currentSavepoint)); } public Mono releaseSavepoint() { - String currentSavepointName = this.savepointName; + String currentSavepoint = this.savepointName; + if (currentSavepoint == null) { + return Mono.empty(); + } this.savepointName = null; - return Mono.from(getConnectionHolder().getConnection().releaseSavepoint(currentSavepointName)); + return Mono.from(getConnectionHolder().getConnection().releaseSavepoint(currentSavepoint)); } public Mono commit() { - Connection connection = getConnectionHolder().getConnection(); - return (this.savepointName != null ? Mono.empty() : Mono.from(connection.commitTransaction())); + return (hasSavepoint() ? Mono.empty() : + Mono.from(getConnectionHolder().getConnection().commitTransaction())); } public Mono rollback() { Connection connection = getConnectionHolder().getConnection(); - return (this.savepointName != null ? - Mono.from(connection.rollbackTransactionToSavepoint(this.savepointName)) : + String currentSavepoint = this.savepointName; + return (currentSavepoint != null ? + Mono.from(connection.rollbackTransactionToSavepoint(currentSavepoint)) : Mono.from(connection.rollbackTransaction())); }