From e964ced1ada6ab960768cc6629cd4dc9d5ddf7f2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 28 Mar 2026 11:11:46 +0100 Subject: [PATCH 1/4] Make ApplicationListenerMethodAdapter#getTargetMethod() public Closes gh-36558 --- .../ApplicationListenerMethodAdapter.java | 77 +++++++++---------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index e2f7c2bec6a..d5cf67ecb28 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -126,10 +126,10 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe } private static List resolveDeclaredEventTypes(Method method, @Nullable EventListener ann) { - int count = (KotlinDetector.isSuspendingFunction(method) ? method.getParameterCount() - 1 : method.getParameterCount()); + int count = (KotlinDetector.isSuspendingFunction(method) ? method.getParameterCount() - 1 : + method.getParameterCount()); if (count > 1) { - throw new IllegalStateException( - "Maximum one parameter is allowed for event listener method: " + method); + throw new IllegalStateException("Maximum one parameter is allowed for event listener method: " + method); } if (ann != null) { @@ -156,6 +156,35 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe } + /** + * Return the target listener method. + * @since 7.0.7 in public form (with protected visibility since 5.3) + */ + public final Method getTargetMethod() { + return this.targetMethod; + } + + /** + * Return the condition to use. + *

Matches the {@code condition} attribute of the {@link EventListener} + * annotation or any matching attribute on a composed annotation that + * is meta-annotated with {@code @EventListener}. + */ + protected final @Nullable String getCondition() { + return this.condition; + } + + /** + * Return whether default execution is applicable for the target listener. + * @since 6.2 + * @see #onApplicationEvent + * @see EventListener#defaultExecution() + */ + protected final boolean isDefaultExecution() { + return this.defaultExecution; + } + + /** * Initialize this instance. */ @@ -167,7 +196,7 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe @Override public void onApplicationEvent(ApplicationEvent event) { - if (isDefaultExecution()) { + if (this.defaultExecution) { processEvent(event); } } @@ -222,22 +251,11 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe * @see #getListenerId() */ protected String getDefaultListenerId() { - Method method = getTargetMethod(); StringJoiner sj = new StringJoiner(",", "(", ")"); - for (Class paramType : method.getParameterTypes()) { + for (Class paramType : this.targetMethod.getParameterTypes()) { sj.add(paramType.getName()); } - return ClassUtils.getQualifiedMethodName(method) + sj; - } - - /** - * Return whether default execution is applicable for the target listener. - * @since 6.2 - * @see #onApplicationEvent - * @see EventListener#defaultExecution() - */ - protected boolean isDefaultExecution() { - return this.defaultExecution; + return ClassUtils.getQualifiedMethodName(this.targetMethod) + sj; } @@ -274,11 +292,10 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe if (args == null) { return false; } - String condition = getCondition(); - if (StringUtils.hasText(condition)) { + if (StringUtils.hasText(this.condition)) { Assert.notNull(this.evaluator, "EventExpressionEvaluator must not be null"); return this.evaluator.condition( - condition, event, this.targetMethod, this.methodKey, args); + this.condition, event, this.targetMethod, this.methodKey, args); } return true; } @@ -402,24 +419,6 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe return this.applicationContext.getBean(this.beanName); } - /** - * Return the target listener method. - * @since 5.3 - */ - protected Method getTargetMethod() { - return this.targetMethod; - } - - /** - * Return the condition to use. - *

Matches the {@code condition} attribute of the {@link EventListener} - * annotation or any matching attribute on a composed annotation that - * is meta-annotated with {@code @EventListener}. - */ - protected @Nullable String getCondition() { - return this.condition; - } - /** * Add additional details such as the bean type and method signature to * the given error message. @@ -427,7 +426,7 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe */ protected String getDetailedErrorMessage(Object bean, @Nullable String message) { StringBuilder sb = (StringUtils.hasLength(message) ? new StringBuilder(message).append('\n') : new StringBuilder()); - sb.append("HandlerMethod details: \n"); + sb.append("ApplicationListenerMethodAdapter details: \n"); sb.append("Bean [").append(bean.getClass().getName()).append("]\n"); sb.append("Method [").append(this.method.toGenericString()).append("]\n"); return sb.toString(); From 529a6fc932f4a72156c676fbe24995d9dc1854e0 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 28 Mar 2026 11:11:52 +0100 Subject: [PATCH 2/4] Add documentation notes on error handling with sync=true See gh-36531 --- .../springframework/cache/annotation/Cacheable.java | 10 ++++++++++ .../cache/interceptor/CacheErrorHandler.java | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java index fa24ab07524..4f0ccd85fd6 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java @@ -193,8 +193,18 @@ public @interface Cacheable { * This is effectively a hint and the chosen cache provider might not actually * support it in a synchronized fashion. Check your provider documentation for * more details on the actual semantics. + *

Note that `sync=true` leads to a combined callback operation against the + * cache provider. If this combined operation fails on initial cache access, + * there is no separate put operation to attempt anymore. Whereas for a default + * `sync=false` setup, there are independent get and put steps: If the get step + * fails but its error is suppressed in the {@code CacheErrorHandler} setup, + * there will still be a put attempt after calling the underlying method. * @since 4.3 * @see org.springframework.cache.Cache#get(Object, Callable) + * @see org.springframework.cache.Cache#get(Object) + * @see org.springframework.cache.Cache#put(Object, Object) + * @see org.springframework.cache.interceptor.CacheErrorHandler#handleCacheGetError + * @see org.springframework.cache.interceptor.CacheErrorHandler#handleCachePutError */ boolean sync() default false; diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheErrorHandler.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheErrorHandler.java index d77ab171384..60cc938ec59 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheErrorHandler.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheErrorHandler.java @@ -16,6 +16,8 @@ package org.springframework.cache.interceptor; +import java.util.concurrent.Callable; + import org.jspecify.annotations.Nullable; import org.springframework.cache.Cache; @@ -39,10 +41,17 @@ public interface CacheErrorHandler { * Handle the given runtime exception thrown by the cache provider when * retrieving an item with the specified {@code key}, possibly * rethrowing it as a fatal exception. + *

Note that for a default {@code @Cacheable} setup, this will be called + * after an initial cache access failure, whereas the subsequent put step may + * independently fail and be handled in {@link #handleCachePutError} still. + * However, for {@code @Cacheable(sync=true)}, there is only a combined get step + * with {@code handleCacheGetError} being called in case of failure; there won't + * be a separate put attempt after initial cache access failure anymore. * @param exception the exception thrown by the cache provider * @param cache the cache * @param key the key used to get the item * @see Cache#get(Object) + * @see Cache#get(Object, Callable) */ void handleCacheGetError(RuntimeException exception, Cache cache, Object key); From b81a63fb5c2972ccdba3c0ea5abf912971919cad Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 28 Mar 2026 11:12:28 +0100 Subject: [PATCH 3/4] Polishing --- .../java/org/springframework/core/MethodParameter.java | 1 + .../springframework/core/task/SimpleAsyncTaskExecutor.java | 3 +-- .../core/task/support/TaskExecutorAdapter.java | 1 - .../web/servlet/function/SseServerResponse.java | 7 +------ 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/MethodParameter.java b/spring-core/src/main/java/org/springframework/core/MethodParameter.java index 79fff8944cf..c55b9805180 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodParameter.java +++ b/spring-core/src/main/java/org/springframework/core/MethodParameter.java @@ -822,6 +822,7 @@ public class MethodParameter { * @since 5.0 */ public static MethodParameter forParameter(Parameter parameter) { + Assert.notNull(parameter, "Parameter must not be null"); return forExecutable(parameter.getDeclaringExecutable(), findParameterIndex(parameter)); } diff --git a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java index 46c506f2c81..d09482040e5 100644 --- a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java +++ b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java @@ -324,8 +324,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator catch (Throwable ex) { // Release concurrency permit if thread creation fails this.concurrencyThrottle.afterAccess(); - throw new TaskRejectedException( - "Failed to start execution thread for task: " + task, ex); + throw new TaskRejectedException("Failed to start execution thread for task: " + task, ex); } } else if (this.activeThreads != null) { diff --git a/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java b/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java index fa7c290613c..b9a81cecf6a 100644 --- a/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java +++ b/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java @@ -42,7 +42,6 @@ import org.springframework.util.Assert; * @see java.util.concurrent.ExecutorService * @see java.util.concurrent.Executors */ -@SuppressWarnings("deprecation") public class TaskExecutorAdapter implements AsyncTaskExecutor { private final Executor concurrentExecutor; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java index b12a672c6d5..1ca6712f64a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java @@ -105,7 +105,6 @@ final class SseServerResponse extends AbstractServerResponse { private static final byte[] NL_NL = new byte[]{'\n', '\n'}; - private final ServerHttpResponse outputMessage; private final DeferredResult deferredResult; @@ -118,7 +117,6 @@ final class SseServerResponse extends AbstractServerResponse { private boolean sendFailed; - public DefaultSseBuilder(HttpServletResponse response, Context context, DeferredResult deferredResult, HttpHeaders httpHeaders) { this.outputMessage = new ServletServerHttpResponse(response); @@ -184,7 +182,6 @@ final class SseServerResponse extends AbstractServerResponse { @Override public void data(Object object) throws IOException { Assert.notNull(object, "Object must not be null"); - if (object instanceof String text) { writeString(text); } @@ -206,7 +203,6 @@ final class SseServerResponse extends AbstractServerResponse { this.builder.append("data:"); try { this.outputMessage.getBody().write(builderBytes()); - Class dataClass = data.getClass(); for (HttpMessageConverter converter : this.messageConverters) { if (converter.canWrite(dataClass, MediaType.APPLICATION_JSON)) { @@ -291,8 +287,7 @@ final class SseServerResponse extends AbstractServerResponse { public HttpHeaders getHeaders() { return this.mutableHeaders; } - } - } + } From f0f447a73d8942b5257bf629952bcb3c11566412 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 28 Mar 2026 11:13:04 +0100 Subject: [PATCH 4/4] Upgrade to Netty 4.2.12 --- framework-platform/framework-platform.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index f4530ef4eaf..cd5e77f6c56 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -9,7 +9,7 @@ javaPlatform { dependencies { api(platform("com.fasterxml.jackson:jackson-bom:2.20.2")) api(platform("io.micrometer:micrometer-bom:1.16.4")) - api(platform("io.netty:netty-bom:4.2.10.Final")) + api(platform("io.netty:netty-bom:4.2.12.Final")) api(platform("io.projectreactor:reactor-bom:2025.0.4")) api(platform("io.rsocket:rsocket-bom:1.1.5")) api(platform("org.apache.groovy:groovy-bom:5.0.4"))