diff --git a/spring-context-support/src/main/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManager.java index b2cc1c6db0db..df05bf0ec079 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManager.java +++ b/spring-context-support/src/main/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManager.java @@ -16,7 +16,10 @@ package org.springframework.cache.transaction; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.support.AbstractCacheManager; /** @@ -25,6 +28,7 @@ * on explicitly through the {@link #setTransactionAware} bean property. * * @author Juergen Hoeller + * @author Seonghun Lee * @since 3.2 * @see #setTransactionAware * @see TransactionAwareCacheDecorator @@ -34,6 +38,8 @@ public abstract class AbstractTransactionSupportingCacheManager extends Abstract private boolean transactionAware = false; + private @Nullable CacheErrorHandler errorHandler; + /** * Set whether this CacheManager should expose transaction-aware Cache objects. @@ -52,10 +58,35 @@ public boolean isTransactionAware() { return this.transactionAware; } + /** + * Set the {@link CacheErrorHandler} for {@link Cache#put}, {@link Cache#evict} + * and {@link Cache#clear} failures in the after-commit phase of a transaction, + * applied when this CacheManager is {@linkplain #setTransactionAware + * transaction-aware}. + *
By default, such failures are propagated to the caller of the transaction + * commit, bypassing any error handler configured at the cache interception + * level, since the deferred operation runs outside the intercepted cache + * invocation. + * @since 7.1 + * @see TransactionAwareCacheDecorator + */ + public void setErrorHandler(@Nullable CacheErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + /** + * Return the {@link CacheErrorHandler} for after-commit cache operation + * failures, if any. + * @since 7.1 + */ + public @Nullable CacheErrorHandler getErrorHandler() { + return this.errorHandler; + } + @Override protected Cache decorateCache(Cache cache) { - return (isTransactionAware() ? new TransactionAwareCacheDecorator(cache) : cache); + return (isTransactionAware() ? new TransactionAwareCacheDecorator(cache, getErrorHandler()) : cache); } } diff --git a/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java b/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java index b3de98cbb457..3ba97cb7216f 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java +++ b/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java @@ -23,6 +23,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; @@ -42,6 +43,7 @@ * @author Juergen Hoeller * @author Stephane Nicoll * @author Stas Volsky + * @author Seonghun Lee * @since 3.2 * @see TransactionAwareCacheManagerProxy */ @@ -49,14 +51,34 @@ public class TransactionAwareCacheDecorator implements Cache { private final Cache targetCache; + private final @Nullable CacheErrorHandler errorHandler; + /** * Create a new TransactionAwareCache for the given target Cache. * @param targetCache the target Cache to decorate */ public TransactionAwareCacheDecorator(Cache targetCache) { + this(targetCache, null); + } + + /** + * Create a new TransactionAwareCache for the given target Cache, using the + * given {@link CacheErrorHandler} for {@link #put}, {@link #evict} and + * {@link #clear} failures in the after-commit phase of a transaction. + *
Without an error handler, such failures are propagated to the caller
+ * of the transaction commit, bypassing any error handler configured at the
+ * cache interception level, since the deferred operation runs outside the
+ * intercepted cache invocation.
+ * @param targetCache the target Cache to decorate
+ * @param errorHandler the error handler to invoke for cache operation
+ * failures in the after-commit phase (may be {@code null} to propagate them)
+ * @since 7.1
+ */
+ public TransactionAwareCacheDecorator(Cache targetCache, @Nullable CacheErrorHandler errorHandler) {
Assert.notNull(targetCache, "Target Cache must not be null");
this.targetCache = targetCache;
+ this.errorHandler = errorHandler;
}
@@ -108,7 +130,16 @@ public void put(final Object key, final @Nullable Object value) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
- TransactionAwareCacheDecorator.this.targetCache.put(key, value);
+ try {
+ TransactionAwareCacheDecorator.this.targetCache.put(key, value);
+ }
+ catch (RuntimeException ex) {
+ if (TransactionAwareCacheDecorator.this.errorHandler == null) {
+ throw ex;
+ }
+ TransactionAwareCacheDecorator.this.errorHandler.handleCachePutError(
+ ex, TransactionAwareCacheDecorator.this.targetCache, key, value);
+ }
}
});
}
@@ -128,7 +159,16 @@ public void evict(final Object key) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
- TransactionAwareCacheDecorator.this.targetCache.evict(key);
+ try {
+ TransactionAwareCacheDecorator.this.targetCache.evict(key);
+ }
+ catch (RuntimeException ex) {
+ if (TransactionAwareCacheDecorator.this.errorHandler == null) {
+ throw ex;
+ }
+ TransactionAwareCacheDecorator.this.errorHandler.handleCacheEvictError(
+ ex, TransactionAwareCacheDecorator.this.targetCache, key);
+ }
}
});
}
@@ -148,7 +188,16 @@ public void clear() {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
- targetCache.clear();
+ try {
+ TransactionAwareCacheDecorator.this.targetCache.clear();
+ }
+ catch (RuntimeException ex) {
+ if (TransactionAwareCacheDecorator.this.errorHandler == null) {
+ throw ex;
+ }
+ TransactionAwareCacheDecorator.this.errorHandler.handleCacheClearError(
+ ex, TransactionAwareCacheDecorator.this.targetCache);
+ }
}
});
}
diff --git a/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java b/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java
index 3fa4f0a83b70..979e029cfe1f 100644
--- a/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java
+++ b/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java
@@ -16,19 +16,26 @@
package org.springframework.cache.transaction;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.springframework.cache.Cache;
import org.springframework.cache.concurrent.ConcurrentMapCache;
+import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.transaction.testfixture.CallCountingTransactionManager;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* @author Stephane Nicoll
* @author Juergen Hoeller
+ * @author Seonghun Lee
*/
class TransactionAwareCacheDecoratorTests {
@@ -221,4 +228,100 @@ void invalidateTransactional() { // no transactional support for invalidate
assertThat(target.get(key)).isNull();
}
+ @Test // gh-28554
+ void putTransactionalWithFailureAndErrorHandler() {
+ Cache target = new FailingCache("testCache");
+ CollectingCacheErrorHandler errorHandler = new CollectingCacheErrorHandler();
+ Cache cache = new TransactionAwareCacheDecorator(target, errorHandler);
+ Object key = new Object();
+
+ txTemplate.executeWithoutResult(s -> cache.put(key, "123"));
+
+ assertThat(errorHandler.handled).singleElement().isInstanceOf(SimulatedFailure.class);
+ }
+
+ @Test // gh-28554
+ void evictTransactionalWithFailureAndErrorHandler() {
+ Cache target = new FailingCache("testCache");
+ CollectingCacheErrorHandler errorHandler = new CollectingCacheErrorHandler();
+ Cache cache = new TransactionAwareCacheDecorator(target, errorHandler);
+
+ txTemplate.executeWithoutResult(s -> cache.evict(new Object()));
+
+ assertThat(errorHandler.handled).singleElement().isInstanceOf(SimulatedFailure.class);
+ }
+
+ @Test // gh-28554
+ void clearTransactionalWithFailureAndErrorHandler() {
+ Cache target = new FailingCache("testCache");
+ CollectingCacheErrorHandler errorHandler = new CollectingCacheErrorHandler();
+ Cache cache = new TransactionAwareCacheDecorator(target, errorHandler);
+
+ txTemplate.executeWithoutResult(s -> cache.clear());
+
+ assertThat(errorHandler.handled).singleElement().isInstanceOf(SimulatedFailure.class);
+ }
+
+ @Test // gh-28554
+ void putTransactionalWithFailureAndNoErrorHandler() {
+ Cache target = new FailingCache("testCache");
+ Cache cache = new TransactionAwareCacheDecorator(target);
+ Object key = new Object();
+
+ assertThatExceptionOfType(SimulatedFailure.class)
+ .isThrownBy(() -> txTemplate.executeWithoutResult(s -> cache.put(key, "123")));
+ }
+
+
+ @SuppressWarnings("serial")
+ private static class SimulatedFailure extends RuntimeException {
+ }
+
+ private static class FailingCache extends ConcurrentMapCache {
+
+ FailingCache(String name) {
+ super(name);
+ }
+
+ @Override
+ public void put(Object key, @Nullable Object value) {
+ throw new SimulatedFailure();
+ }
+
+ @Override
+ public void evict(Object key) {
+ throw new SimulatedFailure();
+ }
+
+ @Override
+ public void clear() {
+ throw new SimulatedFailure();
+ }
+ }
+
+ private static class CollectingCacheErrorHandler implements CacheErrorHandler {
+
+ final List