Merge branch '6.2.x'
# Conflicts: # spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java
This commit is contained in:
commit
907c1db7a6
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2023 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -104,7 +104,7 @@ public abstract class AbstractCacheInvoker {
|
||||||
return valueLoader.call();
|
return valueLoader.call();
|
||||||
}
|
}
|
||||||
catch (Exception ex2) {
|
catch (Exception ex2) {
|
||||||
throw new RuntimeException(ex2);
|
throw new Cache.ValueRetrievalException(key, valueLoader, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -122,16 +122,12 @@ public abstract class AbstractCacheInvoker {
|
||||||
try {
|
try {
|
||||||
return cache.retrieve(key);
|
return cache.retrieve(key);
|
||||||
}
|
}
|
||||||
catch (Cache.ValueRetrievalException ex) {
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
catch (RuntimeException ex) {
|
catch (RuntimeException ex) {
|
||||||
getErrorHandler().handleCacheGetError(ex, cache, key);
|
getErrorHandler().handleCacheGetError(ex, cache, key);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute {@link Cache#retrieve(Object, Supplier)} on the specified
|
* Execute {@link Cache#retrieve(Object, Supplier)} on the specified
|
||||||
* {@link Cache} and invoke the error handler if an exception occurs.
|
* {@link Cache} and invoke the error handler if an exception occurs.
|
||||||
|
|
@ -144,9 +140,6 @@ public abstract class AbstractCacheInvoker {
|
||||||
try {
|
try {
|
||||||
return cache.retrieve(key, valueLoader);
|
return cache.retrieve(key, valueLoader);
|
||||||
}
|
}
|
||||||
catch (Cache.ValueRetrievalException ex) {
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
catch (RuntimeException ex) {
|
catch (RuntimeException ex) {
|
||||||
getErrorHandler().handleCacheGetError(ex, cache, key);
|
getErrorHandler().handleCacheGetError(ex, cache, key);
|
||||||
return valueLoader.get();
|
return valueLoader.get();
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
|
|
@ -440,13 +441,40 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
|
||||||
return cacheHit;
|
return cacheHit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
private @Nullable Object executeSynchronized(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
|
private @Nullable Object executeSynchronized(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
|
||||||
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
|
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
|
||||||
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
|
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
|
||||||
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
|
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
|
||||||
Cache cache = context.getCaches().iterator().next();
|
Cache cache = context.getCaches().iterator().next();
|
||||||
if (CompletableFuture.class.isAssignableFrom(method.getReturnType())) {
|
if (CompletableFuture.class.isAssignableFrom(method.getReturnType())) {
|
||||||
return doRetrieve(cache, key, () -> (CompletableFuture<?>) invokeOperation(invoker));
|
AtomicBoolean invokeFailure = new AtomicBoolean(false);
|
||||||
|
CompletableFuture<?> result = doRetrieve(cache, key,
|
||||||
|
() -> {
|
||||||
|
CompletableFuture<?> invokeResult = ((CompletableFuture<?>) invokeOperation(invoker));
|
||||||
|
if (invokeResult == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return invokeResult.exceptionallyCompose(ex -> {
|
||||||
|
invokeFailure.set(true);
|
||||||
|
return CompletableFuture.failedFuture(ex);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return result.exceptionallyCompose(ex -> {
|
||||||
|
if (!(ex instanceof RuntimeException rex)) {
|
||||||
|
return CompletableFuture.failedFuture(ex);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
getErrorHandler().handleCacheGetError(rex, cache, key);
|
||||||
|
if (invokeFailure.get()) {
|
||||||
|
return CompletableFuture.failedFuture(ex);
|
||||||
|
}
|
||||||
|
return (CompletableFuture) invokeOperation(invoker);
|
||||||
|
}
|
||||||
|
catch (Throwable ex2) {
|
||||||
|
return CompletableFuture.failedFuture(ex2);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (this.reactiveCachingHandler != null) {
|
if (this.reactiveCachingHandler != null) {
|
||||||
Object returnValue = this.reactiveCachingHandler.executeSynchronized(invoker, method, cache, key);
|
Object returnValue = this.reactiveCachingHandler.executeSynchronized(invoker, method, cache, key);
|
||||||
|
|
@ -505,9 +533,17 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
|
||||||
if (CompletableFuture.class.isAssignableFrom(context.getMethod().getReturnType())) {
|
if (CompletableFuture.class.isAssignableFrom(context.getMethod().getReturnType())) {
|
||||||
CompletableFuture<?> result = doRetrieve(cache, key);
|
CompletableFuture<?> result = doRetrieve(cache, key);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
return result.exceptionally(ex -> {
|
return result.exceptionallyCompose(ex -> {
|
||||||
getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key);
|
if (!(ex instanceof RuntimeException rex)) {
|
||||||
return null;
|
return CompletableFuture.failedFuture(ex);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
getErrorHandler().handleCacheGetError(rex, cache, key);
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
catch (Throwable ex2) {
|
||||||
|
return CompletableFuture.failedFuture(ex2);
|
||||||
|
}
|
||||||
}).thenCompose(value -> (CompletableFuture<?>) evaluate(
|
}).thenCompose(value -> (CompletableFuture<?>) evaluate(
|
||||||
(value != null ? CompletableFuture.completedFuture(unwrapCacheValue(value)) : null),
|
(value != null ? CompletableFuture.completedFuture(unwrapCacheValue(value)) : null),
|
||||||
invoker, method, contexts));
|
invoker, method, contexts));
|
||||||
|
|
@ -1075,31 +1111,71 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
|
||||||
|
|
||||||
private final ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance();
|
private final ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance();
|
||||||
|
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
public @Nullable Object executeSynchronized(CacheOperationInvoker invoker, Method method, Cache cache, Object key) {
|
public @Nullable Object executeSynchronized(CacheOperationInvoker invoker, Method method, Cache cache, Object key) {
|
||||||
|
AtomicBoolean invokeFailure = new AtomicBoolean(false);
|
||||||
ReactiveAdapter adapter = this.registry.getAdapter(method.getReturnType());
|
ReactiveAdapter adapter = this.registry.getAdapter(method.getReturnType());
|
||||||
if (adapter != null) {
|
if (adapter != null) {
|
||||||
if (adapter.isMultiValue()) {
|
if (adapter.isMultiValue()) {
|
||||||
// Flux or similar
|
// Flux or similar
|
||||||
return adapter.fromPublisher(Flux.from(Mono.fromFuture(
|
return adapter.fromPublisher(Flux.from(Mono.fromFuture(
|
||||||
cache.retrieve(key,
|
doRetrieve(cache, key,
|
||||||
() -> Flux.from(adapter.toPublisher(invokeOperation(invoker))).collectList().toFuture())))
|
() -> Flux.from(adapter.toPublisher(invokeOperation(invoker))).collectList().doOnError(ex -> invokeFailure.set(true)).toFuture())))
|
||||||
.flatMap(Flux::fromIterable));
|
.flatMap(Flux::fromIterable)
|
||||||
|
.onErrorResume(RuntimeException.class, ex -> {
|
||||||
|
try {
|
||||||
|
getErrorHandler().handleCacheGetError(ex, cache, key);
|
||||||
|
if (invokeFailure.get()) {
|
||||||
|
return Flux.error(ex);
|
||||||
|
}
|
||||||
|
return Flux.from(adapter.toPublisher(invokeOperation(invoker)));
|
||||||
|
}
|
||||||
|
catch (RuntimeException exception) {
|
||||||
|
return Flux.error(exception);
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Mono or similar
|
// Mono or similar
|
||||||
return adapter.fromPublisher(Mono.fromFuture(
|
return adapter.fromPublisher(Mono.fromFuture(
|
||||||
cache.retrieve(key,
|
doRetrieve(cache, key,
|
||||||
() -> Mono.from(adapter.toPublisher(invokeOperation(invoker))).toFuture())));
|
() -> Mono.from(adapter.toPublisher(invokeOperation(invoker))).doOnError(ex -> invokeFailure.set(true)).toFuture()))
|
||||||
|
.onErrorResume(RuntimeException.class, ex -> {
|
||||||
|
try {
|
||||||
|
getErrorHandler().handleCacheGetError(ex, cache, key);
|
||||||
|
if (invokeFailure.get()) {
|
||||||
|
return Mono.error(ex);
|
||||||
|
}
|
||||||
|
return Mono.from(adapter.toPublisher(invokeOperation(invoker)));
|
||||||
|
}
|
||||||
|
catch (RuntimeException exception) {
|
||||||
|
return Mono.error(exception);
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (KotlinDetector.isSuspendingFunction(method)) {
|
if (KotlinDetector.isSuspendingFunction(method)) {
|
||||||
return Mono.fromFuture(cache.retrieve(key, () -> {
|
return Mono.fromFuture(doRetrieve(cache, key, () -> {
|
||||||
Mono<?> mono = ((Mono<?>) invokeOperation(invoker));
|
Mono<?> mono = (Mono<?>) invokeOperation(invoker);
|
||||||
if (mono == null) {
|
if (mono != null) {
|
||||||
|
mono = mono.doOnError(ex -> invokeFailure.set(true));
|
||||||
|
}
|
||||||
|
else {
|
||||||
mono = Mono.empty();
|
mono = Mono.empty();
|
||||||
}
|
}
|
||||||
return mono.toFuture();
|
return mono.toFuture();
|
||||||
}));
|
})).onErrorResume(RuntimeException.class, ex -> {
|
||||||
|
try {
|
||||||
|
getErrorHandler().handleCacheGetError(ex, cache, key);
|
||||||
|
if (invokeFailure.get()) {
|
||||||
|
return Mono.error(ex);
|
||||||
|
}
|
||||||
|
return (Mono) invokeOperation(invoker);
|
||||||
|
}
|
||||||
|
catch (RuntimeException exception) {
|
||||||
|
return Mono.error(exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return NOT_HANDLED;
|
return NOT_HANDLED;
|
||||||
}
|
}
|
||||||
|
|
@ -1113,7 +1189,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
|
||||||
return NOT_HANDLED;
|
return NOT_HANDLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
public @Nullable Object findInCaches(CacheOperationContext context, Cache cache, Object key,
|
public @Nullable Object findInCaches(CacheOperationContext context, Cache cache, Object key,
|
||||||
CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
|
CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2024 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package org.springframework.context.aot;
|
package org.springframework.context.aot;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
import org.jspecify.annotations.Nullable;
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
@ -103,7 +104,7 @@ public abstract class AbstractAotProcessor<T> {
|
||||||
FileSystemUtils.deleteRecursively(path);
|
FileSystemUtils.deleteRecursively(path);
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
throw new RuntimeException("Failed to delete existing output in '" + path + "'");
|
throw new UncheckedIOException("Failed to delete existing output in '" + path + "'", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -204,9 +204,9 @@ abstract class ScheduledAnnotationReactiveSupport {
|
||||||
final Supplier<ScheduledTaskObservationContext> contextSupplier;
|
final Supplier<ScheduledTaskObservationContext> contextSupplier;
|
||||||
|
|
||||||
SubscribingRunnable(Publisher<?> publisher, boolean shouldBlock,
|
SubscribingRunnable(Publisher<?> publisher, boolean shouldBlock,
|
||||||
@Nullable String qualifier, List<Runnable> subscriptionTrackerRegistry,
|
@Nullable String qualifier, List<Runnable> subscriptionTrackerRegistry,
|
||||||
String displayName, Supplier<ObservationRegistry> observationRegistrySupplier,
|
String displayName, Supplier<ObservationRegistry> observationRegistrySupplier,
|
||||||
Supplier<ScheduledTaskObservationContext> contextSupplier) {
|
Supplier<ScheduledTaskObservationContext> contextSupplier) {
|
||||||
|
|
||||||
this.publisher = publisher;
|
this.publisher = publisher;
|
||||||
this.shouldBlock = shouldBlock;
|
this.shouldBlock = shouldBlock;
|
||||||
|
|
@ -234,7 +234,7 @@ abstract class ScheduledAnnotationReactiveSupport {
|
||||||
latch.await();
|
latch.await();
|
||||||
}
|
}
|
||||||
catch (InterruptedException ex) {
|
catch (InterruptedException ex) {
|
||||||
throw new RuntimeException(ex);
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2024 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -19,7 +19,9 @@ package org.springframework.cache.annotation;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import org.jspecify.annotations.Nullable;
|
import org.jspecify.annotations.Nullable;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
@ -40,6 +42,7 @@ import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable;
|
import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -58,8 +61,8 @@ class ReactiveCachingTests {
|
||||||
LateCacheHitDeterminationWithValueWrapperConfig.class})
|
LateCacheHitDeterminationWithValueWrapperConfig.class})
|
||||||
void cacheHitDetermination(Class<?> configClass) {
|
void cacheHitDetermination(Class<?> configClass) {
|
||||||
|
|
||||||
AnnotationConfigApplicationContext ctx =
|
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
|
||||||
new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class);
|
configClass, ReactiveCacheableService.class);
|
||||||
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
|
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
|
||||||
|
|
||||||
Object key = new Object();
|
Object key = new Object();
|
||||||
|
|
@ -119,68 +122,6 @@ class ReactiveCachingTests {
|
||||||
ctx.close();
|
ctx.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void cacheErrorHandlerWithLoggingCacheErrorHandler() {
|
|
||||||
AnnotationConfigApplicationContext ctx =
|
|
||||||
new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class, ErrorHandlerCachingConfiguration.class);
|
|
||||||
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
|
|
||||||
|
|
||||||
Object key = new Object();
|
|
||||||
Long r1 = service.cacheFuture(key).join();
|
|
||||||
|
|
||||||
assertThat(r1).isNotNull();
|
|
||||||
assertThat(r1).as("cacheFuture").isEqualTo(0L);
|
|
||||||
|
|
||||||
key = new Object();
|
|
||||||
|
|
||||||
r1 = service.cacheMono(key).block();
|
|
||||||
|
|
||||||
assertThat(r1).isNotNull();
|
|
||||||
assertThat(r1).as("cacheMono").isEqualTo(1L);
|
|
||||||
|
|
||||||
key = new Object();
|
|
||||||
|
|
||||||
r1 = service.cacheFlux(key).blockFirst();
|
|
||||||
|
|
||||||
assertThat(r1).isNotNull();
|
|
||||||
assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void cacheErrorHandlerWithLoggingCacheErrorHandlerAndMethodError() {
|
|
||||||
AnnotationConfigApplicationContext ctx =
|
|
||||||
new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveFailureCacheableService.class, ErrorHandlerCachingConfiguration.class);
|
|
||||||
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
|
|
||||||
|
|
||||||
Object key = new Object();
|
|
||||||
StepVerifier.create(service.cacheMono(key))
|
|
||||||
.expectErrorMessage("mono service error")
|
|
||||||
.verify();
|
|
||||||
|
|
||||||
key = new Object();
|
|
||||||
StepVerifier.create(service.cacheFlux(key))
|
|
||||||
.expectErrorMessage("flux service error")
|
|
||||||
.verify();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void cacheErrorHandlerWithSimpleCacheErrorHandler() {
|
|
||||||
AnnotationConfigApplicationContext ctx =
|
|
||||||
new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class);
|
|
||||||
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
|
|
||||||
|
|
||||||
Throwable completableFuturThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join());
|
|
||||||
assertThat(completableFuturThrowable).isInstanceOf(CompletionException.class)
|
|
||||||
.extracting(Throwable::getCause)
|
|
||||||
.isInstanceOf(UnsupportedOperationException.class);
|
|
||||||
|
|
||||||
Throwable monoThrowable = catchThrowable(() -> service.cacheMono(new Object()).block());
|
|
||||||
assertThat(monoThrowable).isInstanceOf(UnsupportedOperationException.class);
|
|
||||||
|
|
||||||
Throwable fluxThrowable = catchThrowable(() -> service.cacheFlux(new Object()).blockFirst());
|
|
||||||
assertThat(fluxThrowable).isInstanceOf(UnsupportedOperationException.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ValueSource(classes = {EarlyCacheHitDeterminationConfig.class,
|
@ValueSource(classes = {EarlyCacheHitDeterminationConfig.class,
|
||||||
EarlyCacheHitDeterminationWithoutNullValuesConfig.class,
|
EarlyCacheHitDeterminationWithoutNullValuesConfig.class,
|
||||||
|
|
@ -188,8 +129,8 @@ class ReactiveCachingTests {
|
||||||
LateCacheHitDeterminationWithValueWrapperConfig.class})
|
LateCacheHitDeterminationWithValueWrapperConfig.class})
|
||||||
void fluxCacheDoesntDependOnFirstRequest(Class<?> configClass) {
|
void fluxCacheDoesntDependOnFirstRequest(Class<?> configClass) {
|
||||||
|
|
||||||
AnnotationConfigApplicationContext ctx =
|
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
|
||||||
new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class);
|
configClass, ReactiveCacheableService.class);
|
||||||
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
|
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
|
||||||
|
|
||||||
Object key = new Object();
|
Object key = new Object();
|
||||||
|
|
@ -207,6 +148,117 @@ class ReactiveCachingTests {
|
||||||
ctx.close();
|
ctx.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cacheErrorHandlerWithSimpleCacheErrorHandler() {
|
||||||
|
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
|
||||||
|
ExceptionCacheManager.class, ReactiveCacheableService.class);
|
||||||
|
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
|
||||||
|
|
||||||
|
Throwable completableFutureThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join());
|
||||||
|
assertThat(completableFutureThrowable).isInstanceOf(CompletionException.class)
|
||||||
|
.extracting(Throwable::getCause)
|
||||||
|
.isInstanceOf(UnsupportedOperationException.class);
|
||||||
|
|
||||||
|
Throwable monoThrowable = catchThrowable(() -> service.cacheMono(new Object()).block());
|
||||||
|
assertThat(monoThrowable).isInstanceOf(UnsupportedOperationException.class);
|
||||||
|
|
||||||
|
Throwable fluxThrowable = catchThrowable(() -> service.cacheFlux(new Object()).blockFirst());
|
||||||
|
assertThat(fluxThrowable).isInstanceOf(UnsupportedOperationException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cacheErrorHandlerWithSimpleCacheErrorHandlerAndSync() {
|
||||||
|
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
|
||||||
|
ExceptionCacheManager.class, ReactiveSyncCacheableService.class);
|
||||||
|
ReactiveSyncCacheableService service = ctx.getBean(ReactiveSyncCacheableService.class);
|
||||||
|
|
||||||
|
Throwable completableFutureThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join());
|
||||||
|
assertThat(completableFutureThrowable).isInstanceOf(CompletionException.class)
|
||||||
|
.extracting(Throwable::getCause)
|
||||||
|
.isInstanceOf(UnsupportedOperationException.class);
|
||||||
|
|
||||||
|
Throwable monoThrowable = catchThrowable(() -> service.cacheMono(new Object()).block());
|
||||||
|
assertThat(monoThrowable).isInstanceOf(UnsupportedOperationException.class);
|
||||||
|
|
||||||
|
Throwable fluxThrowable = catchThrowable(() -> service.cacheFlux(new Object()).blockFirst());
|
||||||
|
assertThat(fluxThrowable).isInstanceOf(UnsupportedOperationException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cacheErrorHandlerWithLoggingCacheErrorHandler() {
|
||||||
|
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
|
||||||
|
ExceptionCacheManager.class, ReactiveCacheableService.class, ErrorHandlerCachingConfiguration.class);
|
||||||
|
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
|
||||||
|
|
||||||
|
Long r1 = service.cacheFuture(new Object()).join();
|
||||||
|
assertThat(r1).isNotNull();
|
||||||
|
assertThat(r1).as("cacheFuture").isEqualTo(0L);
|
||||||
|
|
||||||
|
r1 = service.cacheMono(new Object()).block();
|
||||||
|
assertThat(r1).isNotNull();
|
||||||
|
assertThat(r1).as("cacheMono").isEqualTo(1L);
|
||||||
|
|
||||||
|
r1 = service.cacheFlux(new Object()).blockFirst();
|
||||||
|
assertThat(r1).isNotNull();
|
||||||
|
assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cacheErrorHandlerWithLoggingCacheErrorHandlerAndSync() {
|
||||||
|
AnnotationConfigApplicationContext ctx =
|
||||||
|
new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveSyncCacheableService.class, ErrorHandlerCachingConfiguration.class);
|
||||||
|
ReactiveSyncCacheableService service = ctx.getBean(ReactiveSyncCacheableService.class);
|
||||||
|
|
||||||
|
Long r1 = service.cacheFuture(new Object()).join();
|
||||||
|
assertThat(r1).isNotNull();
|
||||||
|
assertThat(r1).as("cacheFuture").isEqualTo(0L);
|
||||||
|
|
||||||
|
r1 = service.cacheMono(new Object()).block();
|
||||||
|
assertThat(r1).isNotNull();
|
||||||
|
assertThat(r1).as("cacheMono").isEqualTo(1L);
|
||||||
|
|
||||||
|
r1 = service.cacheFlux(new Object()).blockFirst();
|
||||||
|
assertThat(r1).isNotNull();
|
||||||
|
assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cacheErrorHandlerWithLoggingCacheErrorHandlerAndOperationException() {
|
||||||
|
AnnotationConfigApplicationContext ctx =
|
||||||
|
new AnnotationConfigApplicationContext(EarlyCacheHitDeterminationConfig.class, ReactiveFailureCacheableService.class, ErrorHandlerCachingConfiguration.class);
|
||||||
|
ReactiveFailureCacheableService service = ctx.getBean(ReactiveFailureCacheableService.class);
|
||||||
|
|
||||||
|
assertThatExceptionOfType(CompletionException.class).isThrownBy(() -> service.cacheFuture(new Object()).join())
|
||||||
|
.withMessage(IllegalStateException.class.getName() + ": future service error");
|
||||||
|
|
||||||
|
StepVerifier.create(service.cacheMono(new Object()))
|
||||||
|
.expectErrorMessage("mono service error")
|
||||||
|
.verify();
|
||||||
|
|
||||||
|
StepVerifier.create(service.cacheFlux(new Object()))
|
||||||
|
.expectErrorMessage("flux service error")
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cacheErrorHandlerWithLoggingCacheErrorHandlerAndOperationExceptionAndSync() {
|
||||||
|
AnnotationConfigApplicationContext ctx =
|
||||||
|
new AnnotationConfigApplicationContext(EarlyCacheHitDeterminationConfig.class, ReactiveSyncFailureCacheableService.class, ErrorHandlerCachingConfiguration.class);
|
||||||
|
ReactiveSyncFailureCacheableService service = ctx.getBean(ReactiveSyncFailureCacheableService.class);
|
||||||
|
|
||||||
|
assertThatExceptionOfType(CompletionException.class).isThrownBy(() -> service.cacheFuture(new Object()).join())
|
||||||
|
.withMessage(IllegalStateException.class.getName() + ": future service error");
|
||||||
|
|
||||||
|
StepVerifier.create(service.cacheMono(new Object()))
|
||||||
|
.expectErrorMessage("mono service error")
|
||||||
|
.verify();
|
||||||
|
|
||||||
|
StepVerifier.create(service.cacheFlux(new Object()))
|
||||||
|
.expectErrorMessage("flux service error")
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@CacheConfig(cacheNames = "first")
|
@CacheConfig(cacheNames = "first")
|
||||||
static class ReactiveCacheableService {
|
static class ReactiveCacheableService {
|
||||||
|
|
||||||
|
|
@ -232,16 +284,94 @@ class ReactiveCachingTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@CacheConfig(cacheNames = "first")
|
@CacheConfig(cacheNames = "first")
|
||||||
static class ReactiveFailureCacheableService extends ReactiveCacheableService {
|
static class ReactiveSyncCacheableService {
|
||||||
|
|
||||||
|
private final AtomicLong counter = new AtomicLong();
|
||||||
|
|
||||||
|
@Cacheable(sync = true)
|
||||||
|
CompletableFuture<Long> cacheFuture(Object arg) {
|
||||||
|
return CompletableFuture.completedFuture(this.counter.getAndIncrement());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable(sync = true)
|
||||||
|
Mono<Long> cacheMono(Object arg) {
|
||||||
|
return Mono.defer(() -> Mono.just(this.counter.getAndIncrement()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable(sync = true)
|
||||||
|
Flux<Long> cacheFlux(Object arg) {
|
||||||
|
return Flux.defer(() -> Flux.just(this.counter.getAndIncrement(), 0L, -1L, -2L, -3L));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@CacheConfig(cacheNames = "first")
|
||||||
|
static class ReactiveFailureCacheableService {
|
||||||
|
|
||||||
|
private final AtomicBoolean cacheFutureInvoked = new AtomicBoolean();
|
||||||
|
|
||||||
|
private final AtomicBoolean cacheMonoInvoked = new AtomicBoolean();
|
||||||
|
|
||||||
|
private final AtomicBoolean cacheFluxInvoked = new AtomicBoolean();
|
||||||
|
|
||||||
|
@Cacheable
|
||||||
|
CompletableFuture<Long> cacheFuture(Object arg) {
|
||||||
|
if (!this.cacheFutureInvoked.compareAndSet(false, true)) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("future service invoked twice"));
|
||||||
|
}
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("future service error"));
|
||||||
|
}
|
||||||
|
|
||||||
@Cacheable
|
@Cacheable
|
||||||
Mono<Long> cacheMono(Object arg) {
|
Mono<Long> cacheMono(Object arg) {
|
||||||
|
if (!this.cacheMonoInvoked.compareAndSet(false, true)) {
|
||||||
|
return Mono.error(new IllegalStateException("mono service invoked twice"));
|
||||||
|
}
|
||||||
return Mono.error(new IllegalStateException("mono service error"));
|
return Mono.error(new IllegalStateException("mono service error"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cacheable
|
@Cacheable
|
||||||
Flux<Long> cacheFlux(Object arg) {
|
Flux<Long> cacheFlux(Object arg) {
|
||||||
|
if (!this.cacheFluxInvoked.compareAndSet(false, true)) {
|
||||||
|
return Flux.error(new IllegalStateException("flux service invoked twice"));
|
||||||
|
}
|
||||||
|
return Flux.error(new IllegalStateException("flux service error"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@CacheConfig(cacheNames = "first")
|
||||||
|
static class ReactiveSyncFailureCacheableService {
|
||||||
|
|
||||||
|
private final AtomicBoolean cacheFutureInvoked = new AtomicBoolean();
|
||||||
|
|
||||||
|
private final AtomicBoolean cacheMonoInvoked = new AtomicBoolean();
|
||||||
|
|
||||||
|
private final AtomicBoolean cacheFluxInvoked = new AtomicBoolean();
|
||||||
|
|
||||||
|
@Cacheable(sync = true)
|
||||||
|
CompletableFuture<Long> cacheFuture(Object arg) {
|
||||||
|
if (!this.cacheFutureInvoked.compareAndSet(false, true)) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("future service invoked twice"));
|
||||||
|
}
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("future service error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable(sync = true)
|
||||||
|
Mono<Long> cacheMono(Object arg) {
|
||||||
|
if (!this.cacheMonoInvoked.compareAndSet(false, true)) {
|
||||||
|
return Mono.error(new IllegalStateException("mono service invoked twice"));
|
||||||
|
}
|
||||||
|
return Mono.error(new IllegalStateException("mono service error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable(sync = true)
|
||||||
|
Flux<Long> cacheFlux(Object arg) {
|
||||||
|
if (!this.cacheFluxInvoked.compareAndSet(false, true)) {
|
||||||
|
return Flux.error(new IllegalStateException("flux service invoked twice"));
|
||||||
|
}
|
||||||
return Flux.error(new IllegalStateException("flux service error"));
|
return Flux.error(new IllegalStateException("flux service error"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -323,6 +453,7 @@ class ReactiveCachingTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
static class ErrorHandlerCachingConfiguration implements CachingConfigurer {
|
static class ErrorHandlerCachingConfiguration implements CachingConfigurer {
|
||||||
|
|
||||||
|
|
@ -333,6 +464,7 @@ class ReactiveCachingTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@EnableCaching
|
@EnableCaching
|
||||||
static class ExceptionCacheManager {
|
static class ExceptionCacheManager {
|
||||||
|
|
@ -345,11 +477,12 @@ class ReactiveCachingTests {
|
||||||
return new ConcurrentMapCache(name, isAllowNullValues()) {
|
return new ConcurrentMapCache(name, isAllowNullValues()) {
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<?> retrieve(Object key) {
|
public CompletableFuture<?> retrieve(Object key) {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.failedFuture(new UnsupportedOperationException("Test exception on retrieve"));
|
||||||
throw new UnsupportedOperationException("Test exception on retrieve");
|
}
|
||||||
});
|
@Override
|
||||||
|
public <T> CompletableFuture<T> retrieve(Object key, Supplier<CompletableFuture<T>> valueLoader) {
|
||||||
|
return CompletableFuture.failedFuture(new UnsupportedOperationException("Test exception on retrieve"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void put(Object key, @Nullable Object value) {
|
public void put(Object key, @Nullable Object value) {
|
||||||
throw new UnsupportedOperationException("Test exception on put");
|
throw new UnsupportedOperationException("Test exception on put");
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2023 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -189,7 +189,7 @@ public abstract class AbstractMockMvcBuilder<B extends AbstractMockMvcBuilder<B>
|
||||||
filterDecorator.initIfRequired(servletContext);
|
filterDecorator.initIfRequired(servletContext);
|
||||||
}
|
}
|
||||||
catch (ServletException ex) {
|
catch (ServletException ex) {
|
||||||
throw new RuntimeException("Failed to initialize Filter " + filter, ex);
|
throw new IllegalStateException("Failed to initialize Filter " + filter, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2024 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -27,7 +27,6 @@ import java.util.Locale;
|
||||||
*/
|
*/
|
||||||
public interface SmartView extends View {
|
public interface SmartView extends View {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the view performs a redirect.
|
* Whether the view performs a redirect.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2024 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -404,7 +404,7 @@ public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodRetur
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
throw new RuntimeException("Failed to render " + modelAndView, ex);
|
throw new IllegalStateException("Failed to render " + modelAndView, ex);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
RequestContextHolder.resetRequestAttributes();
|
RequestContextHolder.resetRequestAttributes();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue