From 1366d2926b924e3c8c70679cb3535ac8c13eff7e Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:32:01 +0100 Subject: [PATCH 001/108] Upgrade to antora-ui-spring version 0.4.18 Closes gh-33892 (cherry picked from commit fe8573c0abb6b5fda6c540bcc51829a101553e3e) --- framework-docs/antora-playbook.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/antora-playbook.yml b/framework-docs/antora-playbook.yml index bae9f7639d28..62f0f71a8a3f 100644 --- a/framework-docs/antora-playbook.yml +++ b/framework-docs/antora-playbook.yml @@ -36,4 +36,4 @@ runtime: failure_level: warn ui: bundle: - url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.17/ui-bundle.zip + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip From 3129afba1982a9d16c56967f88150cf0d1287e6e Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:50:22 +0100 Subject: [PATCH 002/108] Upgrade to Gradle 8.11 Closes gh-33895 (cherry picked from commit bb32df0a066d3e8bb16adc4966599330b504037f) --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df97d72b8b91..94113f200e61 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 55167b7f50a816eaefb3eb30e274fe6a4a74e65f Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:23:09 +0100 Subject: [PATCH 003/108] Fix SpEL examples in reference guide Closes gh-33907 (cherry picked from commit a12d40e10b3dab11b1b5dec37d924f4ff8611a28) --- .../pages/core/expressions/language-ref/constructors.adoc | 4 ++-- .../ROOT/pages/core/expressions/language-ref/functions.adoc | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc index a35513c9c1ec..1a1d0635663d 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc @@ -18,8 +18,8 @@ Java:: // create new Inventor instance within the add() method of List p.parseExpression( - "Members.add(new org.spring.samples.spel.inventor.Inventor( - 'Albert Einstein', 'German'))").getValue(societyContext); + "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") + .getValue(societyContext); ---- Kotlin:: diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc index 6e1e2bf81f64..53f4c8ff3a11 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc @@ -110,8 +110,8 @@ potentially more efficient use cases if the `MethodHandle` target and parameters been fully bound prior to registration; however, partially bound handles are also supported. -Consider the `String#formatted(String, Object...)` instance method, which produces a -message according to a template and a variable number of arguments. +Consider the `String#formatted(Object...)` instance method, which produces a message +according to a template and a variable number of arguments. You can register and use the `formatted` method as a `MethodHandle`, as the following example shows: From 92874adae9604e7b318cd19879b707f8093de7f5 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 14 Nov 2024 12:57:43 +0100 Subject: [PATCH 004/108] Polish SpEL documentation (cherry picked from commit 7196f3f5548640a29c0bcf30060d52d1dbdc03d3) --- .../expressions/language-ref/constructors.adoc | 14 +++++++------- .../expressions/language-ref/functions.adoc | 18 ++++++++++-------- .../spel/ConstructorInvocationTests.java | 2 ++ .../expression/spel/MethodInvocationTests.java | 2 ++ .../spel/SpelDocumentationTests.java | 3 ++- .../spel/VariableAndFunctionTests.java | 2 ++ 6 files changed, 25 insertions(+), 16 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc index 1a1d0635663d..48df78655721 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc @@ -12,12 +12,12 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- - Inventor einstein = p.parseExpression( - "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") + Inventor einstein = parser.parseExpression( + "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); // create new Inventor instance within the add() method of List - p.parseExpression( + parser.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext); ---- @@ -26,13 +26,13 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - val einstein = p.parseExpression( - "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") + val einstein = parser.parseExpression( + "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) // create new Inventor instance within the add() method of List - p.parseExpression( - "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") + parser.parseExpression( + "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) ---- ====== diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc index 53f4c8ff3a11..bb5589cbb209 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc @@ -151,10 +151,10 @@ Kotlin:: ---- ====== -As hinted above, binding a `MethodHandle` and registering the bound `MethodHandle` is also -supported. This is likely to be more performant if both the target and all the arguments -are bound. In that case no arguments are necessary in the SpEL expression, as the -following example shows: +As mentioned above, binding a `MethodHandle` and registering the bound `MethodHandle` is +also supported. This is likely to be more performant if both the target and all the +arguments are bound. In that case no arguments are necessary in the SpEL expression, as +the following example shows: [tabs] ====== @@ -168,9 +168,10 @@ Java:: String template = "This is a %s message with %s words: <%s>"; Object varargs = new Object[] { "prerecorded", 3, "Oh Hello World!", "ignored" }; MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted", - MethodType.methodType(String.class, Object[].class)) + MethodType.methodType(String.class, Object[].class)) .bindTo(template) - .bindTo(varargs); //here we have to provide arguments in a single array binding + // Here we have to provide the arguments in a single array binding: + .bindTo(varargs); context.setVariable("message", mh); // evaluates to "This is a prerecorded message with 3 words: " @@ -189,9 +190,10 @@ Kotlin:: val varargs = arrayOf("prerecorded", 3, "Oh Hello World!", "ignored") val mh = MethodHandles.lookup().findVirtual(String::class.java, "formatted", - MethodType.methodType(String::class.java, Array::class.java)) + MethodType.methodType(String::class.java, Array::class.java)) .bindTo(template) - .bindTo(varargs) //here we have to provide arguments in a single array binding + // Here we have to provide the arguments in a single array binding: + .bindTo(varargs) context.setVariable("message", mh) // evaluates to "This is a prerecorded message with 3 words: " diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ConstructorInvocationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ConstructorInvocationTests.java index abc80b037006..b5e0028a6de4 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ConstructorInvocationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ConstructorInvocationTests.java @@ -36,6 +36,8 @@ * Tests invocation of constructors. * * @author Andy Clement + * @see MethodInvocationTests + * @see VariableAndFunctionTests */ class ConstructorInvocationTests extends AbstractExpressionTests { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java index 3da7456189dd..05d1d10da468 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java @@ -46,6 +46,8 @@ * @author Andy Clement * @author Phillip Webb * @author Sam Brannen + * @see ConstructorInvocationTests + * @see VariableAndFunctionTests */ class MethodInvocationTests extends AbstractExpressionTests { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java index c223ab257037..2c4090d0a1e9 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java @@ -597,7 +597,8 @@ void registerFunctionViaMethodHandleFullyBound() throws Exception { MethodHandle methodHandle = MethodHandles.lookup().findVirtual(String.class, "formatted", MethodType.methodType(String.class, Object[].class)) .bindTo(template) - .bindTo(varargs); // here we have to provide arguments in a single array binding + // Here we have to provide the arguments in a single array binding: + .bindTo(varargs); context.registerFunction("message", methodHandle); String message = parser.parseExpression("#message()").getValue(context, String.class); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java index 29a4255254a1..38d7d047f210 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java @@ -32,6 +32,8 @@ * * @author Andy Clement * @author Sam Brannen + * @see ConstructorInvocationTests + * @see MethodInvocationTests */ class VariableAndFunctionTests extends AbstractExpressionTests { From 75a920bc9fab9f340f8b8de7ca06f165d40164c5 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Mon, 25 Nov 2024 23:54:03 +0700 Subject: [PATCH 005/108] Fix a typo in the filters documentation Backport of gh-33959 Closes gh-33971 --- framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc index 823171a36968..7f3369a168a8 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc @@ -26,7 +26,7 @@ available through the `ServletRequest.getParameter{asterisk}()` family of method -[[forwarded-headers]] +[[filters-forwarded-headers]] == Forwarded Headers [.small]#xref:web/webflux/reactive-spring.adoc#webflux-forwarded-headers[See equivalent in the Reactive stack]# From dd32f951925c8aabd03ce01bc7ee4f076fd5710f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 14 Nov 2024 16:14:46 +0100 Subject: [PATCH 006/108] Dispatch in UndertowHttpHandlerAdapter This ensures that the reactive handling of the request is dispatched from the Undertow IO thread, marking the exchange as async rather than ending it once the Undertow `handleRequest` method returns. See gh-33885 Closes gh-33969 --- .../reactive/UndertowHttpHandlerAdapter.java | 34 ++++++++++--------- ...pingMessageConversionIntegrationTests.java | 17 ++++++++++ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index 8c58eb159d8c..4517bc413247 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -66,25 +66,27 @@ public DataBufferFactory getDataBufferFactory() { @Override public void handleRequest(HttpServerExchange exchange) { - UndertowServerHttpRequest request = null; - try { - request = new UndertowServerHttpRequest(exchange, getDataBufferFactory()); - } - catch (URISyntaxException ex) { - if (logger.isWarnEnabled()) { - logger.debug("Failed to get request URI: " + ex.getMessage()); + exchange.dispatch(() -> { + UndertowServerHttpRequest request = null; + try { + request = new UndertowServerHttpRequest(exchange, getDataBufferFactory()); } - exchange.setStatusCode(400); - return; - } - ServerHttpResponse response = new UndertowServerHttpResponse(exchange, getDataBufferFactory(), request); + catch (URISyntaxException ex) { + if (logger.isWarnEnabled()) { + logger.debug("Failed to get request URI: " + ex.getMessage()); + } + exchange.setStatusCode(400); + return; + } + ServerHttpResponse response = new UndertowServerHttpResponse(exchange, getDataBufferFactory(), request); - if (request.getMethod() == HttpMethod.HEAD) { - response = new HttpHeadResponseDecorator(response); - } + if (request.getMethod() == HttpMethod.HEAD) { + response = new HttpHeadResponseDecorator(response); + } - HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(exchange, request); - this.httpHandler.handle(request, response).subscribe(resultSubscriber); + HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(exchange, request); + this.httpHandler.handle(request, response).subscribe(resultSubscriber); + }); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java index a11a8f8185c9..20ff79541904 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java @@ -17,6 +17,7 @@ package org.springframework.web.reactive.result.method.annotation; import java.nio.ByteBuffer; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -331,6 +332,17 @@ void personTransformWithFlux(HttpServer httpServer) throws Exception { assertThat(performPost("/person-transform/flux", JSON, req, JSON, PERSON_LIST).getBody()).isEqualTo(res); } + @ParameterizedHttpServerTest // see gh-33885 + void personTransformWithFluxDelayed(HttpServer httpServer) throws Exception { + startServer(httpServer); + + List req = asList(new Person("Robert"), new Person("Marie")); + List res = asList(new Person("ROBERT"), new Person("MARIE")); + assertThat(performPost("/person-transform/flux-delayed", JSON, req, JSON, PERSON_LIST)) + .satisfies(r -> assertThat(r.getBody()).isEqualTo(res)) + .satisfies(r -> assertThat(r.getHeaders().getContentLength()).isNotZero()); + } + @ParameterizedHttpServerTest void personTransformWithObservable(HttpServer httpServer) throws Exception { startServer(httpServer); @@ -632,6 +644,11 @@ Flux transformFlux(@RequestBody Flux persons) { return persons.map(person -> new Person(person.getName().toUpperCase())); } + @PostMapping("/flux-delayed") + Flux transformDelayed(@RequestBody Flux persons) { + return transformFlux(persons).delayElements(Duration.ofMillis(10)); + } + @PostMapping("/observable") Observable transformObservable(@RequestBody Observable persons) { return persons.map(person -> new Person(person.getName().toUpperCase())); From f20e76e2270383ae4177ac9fb5a03d0644511622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 27 Nov 2024 14:31:46 +0100 Subject: [PATCH 007/108] Upgrade to Undertow `2.3.18.Final` Closes gh-33976 --- framework-platform/framework-platform.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 844089b50f0e..7ed037722371 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -56,8 +56,8 @@ dependencies { api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE") api("io.reactivex.rxjava3:rxjava:3.1.9") api("io.smallrye.reactive:mutiny:1.10.0") - api("io.undertow:undertow-core:2.3.17.Final") - api("io.undertow:undertow-servlet:2.3.17.Final") + api("io.undertow:undertow-core:2.3.18.Final") + api("io.undertow:undertow-servlet:2.3.18.Final") api("io.undertow:undertow-websockets-jsr:2.3.17.Final") api("io.vavr:vavr:0.10.4") api("jakarta.activation:jakarta.activation-api:2.0.1") From 3635ff0c17a2e4163007b42c754ba461a3acdf8a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 4 Dec 2024 16:39:41 +0100 Subject: [PATCH 008/108] Consistent fallback to NoUpgradeStrategyWebSocketService Closes gh-33970 (cherry picked from commit 58c64cba2cd4eeced72b874f8bc0f384e41e24f1) --- .../reactive/config/WebFluxConfigurationSupport.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java index ac71dc84675d..ffba81e8bbe7 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java @@ -478,9 +478,9 @@ private WebSocketService initWebSocketService() { try { service = new HandshakeWebSocketService(); } - catch (IllegalStateException ex) { + catch (Throwable ex) { // Don't fail, test environment perhaps - service = new NoUpgradeStrategyWebSocketService(); + service = new NoUpgradeStrategyWebSocketService(ex); } } return service; @@ -578,9 +578,15 @@ public void validate(@Nullable Object target, Errors errors) { private static final class NoUpgradeStrategyWebSocketService implements WebSocketService { + private final Throwable ex; + + public NoUpgradeStrategyWebSocketService(Throwable ex) { + this.ex = ex; + } + @Override public Mono handleRequest(ServerWebExchange exchange, WebSocketHandler webSocketHandler) { - return Mono.error(new IllegalStateException("No suitable RequestUpgradeStrategy")); + return Mono.error(new IllegalStateException("No suitable RequestUpgradeStrategy", this.ex)); } } From aaf2e8fbe666e3e5f053d355856c0a4aa6cbae64 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 4 Dec 2024 16:41:07 +0100 Subject: [PATCH 009/108] Polishing (cherry picked from commit edf7f3cd43514cd27925c14c5425996b79627256) --- .../AbstractSchedulingTaskExecutorTests.java | 14 +++----------- .../jdbc/SqlScriptsTestExecutionListener.java | 6 +++--- .../java/org/springframework/http/MediaType.java | 12 ++++++------ .../function/DefaultAsyncServerResponse.java | 5 +++-- .../DefaultServerResponseBuilderTests.java | 2 +- spring-websocket/spring-websocket.gradle | 6 +++--- .../StandardToWebSocketExtensionAdapter.java | 2 -- .../WebSocketToStandardExtensionAdapter.java | 3 ++- .../web/socket/messaging/WebSocketStompClient.java | 12 ++++++------ .../sockjs/client/DefaultTransportRequest.java | 3 ++- 10 files changed, 29 insertions(+), 36 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java index 5d817ee9e166..7d2746f90f0e 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java @@ -209,18 +209,10 @@ void submitCompletableRunnableWithGetAfterShutdown() throws Exception { CompletableFuture future1 = executor.submitCompletable(new TestTask(this.testName, -1)); CompletableFuture future2 = executor.submitCompletable(new TestTask(this.testName, -1)); shutdownExecutor(); - - try { + assertThatExceptionOfType(TimeoutException.class).isThrownBy(() -> { future1.get(1000, TimeUnit.MILLISECONDS); - } - catch (Exception ex) { - // ignore - } - Awaitility.await() - .atMost(5, TimeUnit.SECONDS) - .pollInterval(10, TimeUnit.MILLISECONDS) - .untilAsserted(() -> assertThatExceptionOfType(TimeoutException.class) - .isThrownBy(() -> future2.get(1000, TimeUnit.MILLISECONDS))); + future2.get(1000, TimeUnit.MILLISECONDS); + }); } @Test diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java index f219bea1f744..d37b21ef5f92 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java @@ -182,10 +182,10 @@ public void afterTestMethod(TestContext testContext) { @Override public void processAheadOfTime(RuntimeHints runtimeHints, Class testClass, ClassLoader classLoader) { getSqlAnnotationsFor(testClass).forEach(sql -> - registerClasspathResources(getScripts(sql, testClass, null, true), runtimeHints, classLoader)); + registerClasspathResources(getScripts(sql, testClass, null, true), runtimeHints, classLoader)); getSqlMethods(testClass).forEach(testMethod -> - getSqlAnnotationsFor(testMethod).forEach(sql -> - registerClasspathResources(getScripts(sql, testClass, testMethod, false), runtimeHints, classLoader))); + getSqlAnnotationsFor(testMethod).forEach(sql -> + registerClasspathResources(getScripts(sql, testClass, testMethod, false), runtimeHints, classLoader))); } /** diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index f89063d8df00..844996abb7dd 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -853,7 +853,7 @@ public static String toString(Collection mediaTypes) { *
audio/basic == text/html
*
audio/basic == audio/wave
* @param mediaTypes the list of media types to be sorted - * @deprecated As of 6.0, in favor of {@link MimeTypeUtils#sortBySpecificity(List)} + * @deprecated as of 6.0, in favor of {@link MimeTypeUtils#sortBySpecificity(List)} */ @Deprecated(since = "6.0", forRemoval = true) public static void sortBySpecificity(List mediaTypes) { @@ -882,7 +882,7 @@ public static void sortBySpecificity(List mediaTypes) { * * @param mediaTypes the list of media types to be sorted * @see #getQualityValue() - * @deprecated As of 6.0, with no direct replacement + * @deprecated as of 6.0, with no direct replacement */ @Deprecated(since = "6.0", forRemoval = true) public static void sortByQualityValue(List mediaTypes) { @@ -895,9 +895,9 @@ public static void sortByQualityValue(List mediaTypes) { /** * Sorts the given list of {@code MediaType} objects by specificity as the * primary criteria and quality value the secondary. - * @deprecated As of 6.0, in favor of {@link MimeTypeUtils#sortBySpecificity(List)} + * @deprecated as of 6.0, in favor of {@link MimeTypeUtils#sortBySpecificity(List)} */ - @Deprecated(since = "6.0") + @Deprecated(since = "6.0", forRemoval = true) public static void sortBySpecificityAndQuality(List mediaTypes) { Assert.notNull(mediaTypes, "'mediaTypes' must not be null"); if (mediaTypes.size() > 1) { @@ -908,7 +908,7 @@ public static void sortBySpecificityAndQuality(List mediaTypes) { /** * Comparator used by {@link #sortByQualityValue(List)}. - * @deprecated As of 6.0, with no direct replacement + * @deprecated as of 6.0, with no direct replacement */ @Deprecated(since = "6.0", forRemoval = true) public static final Comparator QUALITY_VALUE_COMPARATOR = (mediaType1, mediaType2) -> { @@ -948,7 +948,7 @@ else if (!mediaType1.getSubtype().equals(mediaType2.getSubtype())) { // audio/b /** * Comparator used by {@link #sortBySpecificity(List)}. - * @deprecated As of 6.0, with no direct replacement + * @deprecated as of 6.0, with no direct replacement */ @Deprecated(since = "6.0", forRemoval = true) @SuppressWarnings("removal") diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index c026e40d81df..6db059f839ae 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -67,6 +67,7 @@ private DefaultAsyncServerResponse(CompletableFuture futureRespo this.timeout = timeout; } + @Override public ServerResponse block() { try { @@ -114,8 +115,8 @@ private R delegate(Function function) { } } - @Nullable @Override + @Nullable public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) throws ServletException, IOException { @@ -169,6 +170,7 @@ private DeferredResult createDeferredResult(HttpServletRequest r return result; } + @SuppressWarnings({"rawtypes", "unchecked"}) public static AsyncServerResponse create(Object obj, @Nullable Duration timeout) { Assert.notNull(obj, "Argument to async must not be null"); @@ -192,5 +194,4 @@ else if (reactiveStreamsPresent) { throw new IllegalArgumentException("Asynchronous type not supported: " + obj.getClass()); } - } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerResponseBuilderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerResponseBuilderTests.java index 965a8233d143..5832ee575b40 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerResponseBuilderTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerResponseBuilderTests.java @@ -54,6 +54,7 @@ class DefaultServerResponseBuilderTests { static final ServerResponse.Context EMPTY_CONTEXT = Collections::emptyList; + @Test @SuppressWarnings("removal") void status() { @@ -75,7 +76,6 @@ void from() { assertThat(result.cookies().getFirst("foo")).isEqualTo(cookie); } - @Test void ok() { ServerResponse response = ServerResponse.ok().build(); diff --git a/spring-websocket/spring-websocket.gradle b/spring-websocket/spring-websocket.gradle index 72df03b20dd0..95c9a3d0ffc1 100644 --- a/spring-websocket/spring-websocket.gradle +++ b/spring-websocket/spring-websocket.gradle @@ -16,13 +16,13 @@ dependencies { exclude group: "org.apache.tomcat", module: "tomcat-servlet-api" exclude group: "org.apache.tomcat", module: "tomcat-websocket-api" } - optional("org.eclipse.jetty.ee10:jetty-ee10-webapp") { - exclude group: "jakarta.servlet", module: "jakarta.servlet-api" - } optional("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server") optional("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server") { exclude group: "jakarta.servlet", module: "jakarta.servlet-api" } + optional("org.eclipse.jetty.ee10:jetty-ee10-webapp") { + exclude group: "jakarta.servlet", module: "jakarta.servlet-api" + } optional("org.glassfish.tyrus:tyrus-container-servlet") testImplementation(testFixtures(project(":spring-core"))) testImplementation(testFixtures(project(":spring-web"))) diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/standard/StandardToWebSocketExtensionAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/standard/StandardToWebSocketExtensionAdapter.java index af221926d62c..f61d8978e928 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/standard/StandardToWebSocketExtensionAdapter.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/standard/StandardToWebSocketExtensionAdapter.java @@ -34,12 +34,10 @@ */ public class StandardToWebSocketExtensionAdapter extends WebSocketExtension { - public StandardToWebSocketExtensionAdapter(Extension extension) { super(extension.getName(), initParameters(extension)); } - private static Map initParameters(Extension extension) { List parameters = extension.getParameters(); Map result = new LinkedCaseInsensitiveMap<>(parameters.size(), Locale.ROOT); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/standard/WebSocketToStandardExtensionAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/standard/WebSocketToStandardExtensionAdapter.java index f8334d3ff349..8455b8dd2c45 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/standard/WebSocketToStandardExtensionAdapter.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/standard/WebSocketToStandardExtensionAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 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. @@ -55,6 +55,7 @@ public String getValue() { } } + @Override public String getName() { return this.name; diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java index 973376af7076..9a519a0b8a89 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java @@ -234,7 +234,7 @@ public CompletableFuture connectAsync(String url, StompSessionHand /** * An overloaded version of - * {@link #connect(String, StompSessionHandler, Object...)} that also + * {@link #connectAsync(String, StompSessionHandler, Object...)} that also * accepts {@link WebSocketHttpHeaders} to use for the WebSocket handshake. * @param url the url to connect to * @param handshakeHeaders the headers for the WebSocket handshake @@ -254,7 +254,7 @@ public org.springframework.util.concurrent.ListenableFuture connec /** * An overloaded version of - * {@link #connect(String, StompSessionHandler, Object...)} that also + * {@link #connectAsync(String, StompSessionHandler, Object...)} that also * accepts {@link WebSocketHttpHeaders} to use for the WebSocket handshake. * @param url the url to connect to * @param handshakeHeaders the headers for the WebSocket handshake @@ -271,7 +271,7 @@ public CompletableFuture connectAsync(String url, @Nullable WebSoc /** * An overloaded version of - * {@link #connect(String, StompSessionHandler, Object...)} that also accepts + * {@link #connectAsync(String, StompSessionHandler, Object...)} that also accepts * {@link WebSocketHttpHeaders} to use for the WebSocket handshake and * {@link StompHeaders} for the STOMP CONNECT frame. * @param url the url to connect to @@ -293,7 +293,7 @@ public org.springframework.util.concurrent.ListenableFuture connec /** * An overloaded version of - * {@link #connect(String, StompSessionHandler, Object...)} that also accepts + * {@link #connectAsync(String, StompSessionHandler, Object...)} that also accepts * {@link WebSocketHttpHeaders} to use for the WebSocket handshake and * {@link StompHeaders} for the STOMP CONNECT frame. * @param url the url to connect to @@ -314,7 +314,7 @@ public CompletableFuture connectAsync(String url, @Nullable WebSoc /** * An overloaded version of - * {@link #connect(String, WebSocketHttpHeaders, StompSessionHandler, Object...)} + * {@link #connectAsync(String, WebSocketHttpHeaders, StompSessionHandler, Object...)} * that accepts a fully prepared {@link java.net.URI}. * @param url the url to connect to * @param handshakeHeaders the headers for the WebSocket handshake @@ -334,7 +334,7 @@ public org.springframework.util.concurrent.ListenableFuture connec /** * An overloaded version of - * {@link #connect(String, WebSocketHttpHeaders, StompSessionHandler, Object...)} + * {@link #connectAsync(String, WebSocketHttpHeaders, StompSessionHandler, Object...)} * that accepts a fully prepared {@link java.net.URI}. * @param url the url to connect to * @param handshakeHeaders the headers for the WebSocket handshake diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/DefaultTransportRequest.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/DefaultTransportRequest.java index b637acbb0152..d320ec3fd035 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/DefaultTransportRequest.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/DefaultTransportRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -278,6 +278,7 @@ private void handleFailure(@Nullable Throwable ex, boolean isTimeoutFailure) { } } + /** * Updates the given (global) future based success or failure to connect for * the entire SockJS request regardless of which transport actually managed From c48ca5151f972908caa6a193dac9af9c09cc314c Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 4 Dec 2024 17:36:11 +0100 Subject: [PATCH 010/108] Upgrade to Gradle 8.11.1 --- framework-platform/framework-platform.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 7ed037722371..ea2c7c2ef3c6 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -58,7 +58,7 @@ dependencies { api("io.smallrye.reactive:mutiny:1.10.0") api("io.undertow:undertow-core:2.3.18.Final") api("io.undertow:undertow-servlet:2.3.18.Final") - api("io.undertow:undertow-websockets-jsr:2.3.17.Final") + api("io.undertow:undertow-websockets-jsr:2.3.18.Final") api("io.vavr:vavr:0.10.4") api("jakarta.activation:jakarta.activation-api:2.0.1") api("jakarta.annotation:jakarta.annotation-api:2.0.0") diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 94113f200e61..e2847c820046 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 4fbca99f202bfbc932d6d0eaff025e750c4833c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 5 Dec 2024 15:44:09 +0100 Subject: [PATCH 011/108] Remove unnecessary HandshakeHandlerRuntimeHints Those hints are not needed anymore as of Spring Framework 6.1. Backport of gh-34032. Closes gh-34033 --- .../support/HandshakeHandlerRuntimeHints.java | 93 ------------------- .../resources/META-INF/spring/aot.factories | 2 - 2 files changed, 95 deletions(-) delete mode 100644 spring-websocket/src/main/java/org/springframework/web/socket/server/support/HandshakeHandlerRuntimeHints.java delete mode 100644 spring-websocket/src/main/resources/META-INF/spring/aot.factories diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/HandshakeHandlerRuntimeHints.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/HandshakeHandlerRuntimeHints.java deleted file mode 100644 index 46a513377f74..000000000000 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/HandshakeHandlerRuntimeHints.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2002-2024 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.web.socket.server.support; - -import org.springframework.aot.hint.MemberCategory; -import org.springframework.aot.hint.ReflectionHints; -import org.springframework.aot.hint.RuntimeHints; -import org.springframework.aot.hint.RuntimeHintsRegistrar; -import org.springframework.aot.hint.TypeReference; -import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; - -/** - * {@link RuntimeHintsRegistrar} implementation that registers reflection hints - * for {@link AbstractHandshakeHandler}. - * - * @author Sebastien Deleuze - * @since 6.0 - */ -class HandshakeHandlerRuntimeHints implements RuntimeHintsRegistrar { - - private static final boolean tomcatWsPresent; - - private static final boolean jettyWsPresent; - - private static final boolean undertowWsPresent; - - private static final boolean glassfishWsPresent; - - private static final boolean weblogicWsPresent; - - private static final boolean websphereWsPresent; - - static { - ClassLoader classLoader = AbstractHandshakeHandler.class.getClassLoader(); - tomcatWsPresent = ClassUtils.isPresent( - "org.apache.tomcat.websocket.server.WsHttpUpgradeHandler", classLoader); - jettyWsPresent = ClassUtils.isPresent( - "org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer", classLoader); - undertowWsPresent = ClassUtils.isPresent( - "io.undertow.websockets.jsr.ServerWebSocketContainer", classLoader); - glassfishWsPresent = ClassUtils.isPresent( - "org.glassfish.tyrus.servlet.TyrusHttpUpgradeHandler", classLoader); - weblogicWsPresent = ClassUtils.isPresent( - "weblogic.websocket.tyrus.TyrusServletWriter", classLoader); - websphereWsPresent = ClassUtils.isPresent( - "com.ibm.websphere.wsoc.WsWsocServerContainer", classLoader); - } - - - @Override - public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { - ReflectionHints reflectionHints = hints.reflection(); - if (tomcatWsPresent) { - registerType(reflectionHints, "org.springframework.web.socket.server.standard.TomcatRequestUpgradeStrategy"); - } - else if (jettyWsPresent) { - registerType(reflectionHints, "org.springframework.web.socket.server.jetty.JettyRequestUpgradeStrategy"); - } - else if (undertowWsPresent) { - registerType(reflectionHints, "org.springframework.web.socket.server.standard.UndertowRequestUpgradeStrategy"); - } - else if (glassfishWsPresent) { - registerType(reflectionHints, "org.springframework.web.socket.server.standard.GlassFishRequestUpgradeStrategy"); - } - else if (weblogicWsPresent) { - registerType(reflectionHints, "org.springframework.web.socket.server.standard.WebLogicRequestUpgradeStrategy"); - } - else if (websphereWsPresent) { - registerType(reflectionHints, "org.springframework.web.socket.server.standard.WebSphereRequestUpgradeStrategy"); - } - } - - private void registerType(ReflectionHints reflectionHints, String className) { - reflectionHints.registerType(TypeReference.of(className), - builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); - } - -} diff --git a/spring-websocket/src/main/resources/META-INF/spring/aot.factories b/spring-websocket/src/main/resources/META-INF/spring/aot.factories deleted file mode 100644 index e33a7e934f18..000000000000 --- a/spring-websocket/src/main/resources/META-INF/spring/aot.factories +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.aot.hint.RuntimeHintsRegistrar= \ -org.springframework.web.socket.server.support.HandshakeHandlerRuntimeHints \ No newline at end of file From 357195289dcddf75065158f408a26a786a72e427 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 5 Dec 2024 17:53:30 +0100 Subject: [PATCH 012/108] Unit test for match against unresolvable wildcard See gh-33982 --- .../springframework/core/ResolvableType.java | 14 +++++----- .../core/ResolvableTypeTests.java | 27 ++++++++++++------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index 46acaf778919..7facf3ade35b 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -309,12 +309,12 @@ private boolean isAssignableFrom(ResolvableType other, boolean strict, @Nullable // Deal with wildcard bounds WildcardBounds ourBounds = WildcardBounds.get(this); - WildcardBounds typeBounds = WildcardBounds.get(other); + WildcardBounds otherBounds = WildcardBounds.get(other); // In the form X is assignable to - if (typeBounds != null) { - return (ourBounds != null && ourBounds.isSameKind(typeBounds) && - ourBounds.isAssignableFrom(typeBounds.getBounds())); + if (otherBounds != null) { + return (ourBounds != null && ourBounds.isSameKind(otherBounds) && + ourBounds.isAssignableFrom(otherBounds.getBounds())); } // In the form is assignable to X... @@ -365,8 +365,8 @@ private boolean isAssignableFrom(ResolvableType other, boolean strict, @Nullable if (checkGenerics) { // Recursively check each generic ResolvableType[] ourGenerics = getGenerics(); - ResolvableType[] typeGenerics = other.as(ourResolved).getGenerics(); - if (ourGenerics.length != typeGenerics.length) { + ResolvableType[] otherGenerics = other.as(ourResolved).getGenerics(); + if (ourGenerics.length != otherGenerics.length) { return false; } if (ourGenerics.length > 0) { @@ -375,7 +375,7 @@ private boolean isAssignableFrom(ResolvableType other, boolean strict, @Nullable } matchedBefore.put(this.type, other.type); for (int i = 0; i < ourGenerics.length; i++) { - if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], true, matchedBefore)) { + if (!ourGenerics[i].isAssignableFrom(otherGenerics[i], true, matchedBefore)) { return false; } } diff --git a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java index 7880ca35d3bd..0d4a389b45fa 100644 --- a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -169,7 +169,7 @@ void forInstanceProvider() { @Test void forInstanceProviderNull() { - ResolvableType type = ResolvableType.forInstance(new MyGenericInterfaceType(null)); + ResolvableType type = ResolvableType.forInstance(new MyGenericInterfaceType<>(null)); assertThat(type.getType()).isEqualTo(MyGenericInterfaceType.class); assertThat(type.resolve()).isEqualTo(MyGenericInterfaceType.class); } @@ -1177,6 +1177,20 @@ void isAssignableFromForComplexWildcards() throws Exception { assertThatResolvableType(complex4).isNotAssignableFrom(complex3); } + @Test + void isAssignableFromForUnresolvedWildcards() { + ResolvableType wildcard = ResolvableType.forInstance(new Wildcard<>()); + ResolvableType wildcardFixed = ResolvableType.forInstance(new WildcardFixed()); + ResolvableType wildcardConcrete = ResolvableType.forClassWithGenerics(Wildcard.class, Number.class); + + assertThat(wildcard.isAssignableFrom(wildcardFixed)).isTrue(); + assertThat(wildcard.isAssignableFrom(wildcardConcrete)).isTrue(); + assertThat(wildcardFixed.isAssignableFrom(wildcard)).isFalse(); + assertThat(wildcardFixed.isAssignableFrom(wildcardConcrete)).isFalse(); + assertThat(wildcardConcrete.isAssignableFrom(wildcard)).isTrue(); + assertThat(wildcardConcrete.isAssignableFrom(wildcardFixed)).isFalse(); + } + @Test void identifyTypeVariable() throws Exception { Method method = ClassArguments.class.getMethod("typedArgumentFirst", Class.class, Class.class, Class.class); @@ -1574,7 +1588,6 @@ public ResolvableType getResolvableType() { } } - public class MySimpleInterfaceType implements MyInterfaceType { } @@ -1584,7 +1597,6 @@ public abstract class MySimpleInterfaceTypeWithImplementsRaw implements MyInterf public abstract class ExtendsMySimpleInterfaceTypeWithImplementsRaw extends MySimpleInterfaceTypeWithImplementsRaw { } - public class MyCollectionInterfaceType implements MyInterfaceType> { } @@ -1592,20 +1604,17 @@ public class MyCollectionInterfaceType implements MyInterfaceType { } - public class MySimpleSuperclassType extends MySuperclassType { } - public class MyCollectionSuperclassType extends MySuperclassType> { } - interface Wildcard extends List { + public class Wildcard { } - - interface RawExtendsWildcard extends Wildcard { + public class WildcardFixed extends Wildcard { } From 0e74ffa1448138a8bb74a062774ec8c517e2d4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sun, 8 Dec 2024 10:47:15 +0100 Subject: [PATCH 013/108] Start building against Reactor 2023.0.13 snapshots See gh-34049 --- 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 ea2c7c2ef3c6..61aeaed63b6e 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -11,7 +11,7 @@ dependencies { api(platform("io.micrometer:micrometer-bom:1.12.12")) api(platform("io.netty:netty-bom:4.1.115.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2023.0.12")) + api(platform("io.projectreactor:reactor-bom:2023.0.13-SNAPSHOT")) api(platform("io.rsocket:rsocket-bom:1.1.4")) api(platform("org.apache.groovy:groovy-bom:4.0.24")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) From 7ffa12f90f2dc4ec07a3fec800c9c0de1d3e0eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 10 Dec 2024 14:09:57 +0100 Subject: [PATCH 014/108] Upgrade to Reactor 2023.0.13 Closes gh-34049 --- 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 61aeaed63b6e..aeb920fd97db 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -11,7 +11,7 @@ dependencies { api(platform("io.micrometer:micrometer-bom:1.12.12")) api(platform("io.netty:netty-bom:4.1.115.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2023.0.13-SNAPSHOT")) + api(platform("io.projectreactor:reactor-bom:2023.0.13")) api(platform("io.rsocket:rsocket-bom:1.1.4")) api(platform("org.apache.groovy:groovy-bom:4.0.24")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) From a48897a24136524a58b84a6c36b19f0bbd6a433f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 10 Dec 2024 16:25:57 +0100 Subject: [PATCH 015/108] Log provider setup failure at info level without stacktrace Closes gh-33979 (cherry picked from commit 3e3ca7402012d4ad893958f7639579b4b3c69373) --- .../beanvalidation/OptionalValidatorFactoryBean.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/OptionalValidatorFactoryBean.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/OptionalValidatorFactoryBean.java index a2991e556333..b1097f213c8f 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/OptionalValidatorFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/OptionalValidatorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2024 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. @@ -17,6 +17,7 @@ package org.springframework.validation.beanvalidation; import jakarta.validation.ValidationException; +import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** @@ -39,7 +40,13 @@ public void afterPropertiesSet() { super.afterPropertiesSet(); } catch (ValidationException ex) { - LogFactory.getLog(getClass()).debug("Failed to set up a Bean Validation provider", ex); + Log logger = LogFactory.getLog(getClass()); + if (logger.isDebugEnabled()) { + logger.debug("Failed to set up a Bean Validation provider", ex); + } + else if (logger.isInfoEnabled()) { + logger.info("Failed to set up a Bean Validation provider: " + ex); + } } } From daf1b3d7a74124f1f8445192ffbdc6ff16b9f606 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 10 Dec 2024 22:33:01 +0100 Subject: [PATCH 016/108] Remove unnecessary downcast to DefaultListableBeanFactory See gh-33920 See gh-25952 --- .../annotation/ConfigurationClassBeanDefinitionReader.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index 55d1db9fab90..e0e1ca93547f 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -39,7 +39,6 @@ import org.springframework.beans.factory.support.BeanDefinitionReader; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase; @@ -318,7 +317,7 @@ protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String // At this point, it's a top-level override (probably XML), just having been parsed // before configuration class processing kicks in... - if (this.registry instanceof DefaultListableBeanFactory dlbf && !dlbf.isBeanDefinitionOverridable(beanName)) { + if (!this.registry.isBeanDefinitionOverridable(beanName)) { throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(), beanName, "@Bean definition illegally overridden by existing bean definition: " + existingBeanDef); } From faa000f330786357cc702758166b9b9a9a161242 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:03:52 +0100 Subject: [PATCH 017/108] Log alias removal in DefaultListableBeanFactory Prior to this commit, information was logged when a bean definition overrode an existing bean definition, but nothing was logged when the registration of a bean definition resulted in the removal of an alias. With this commit, an INFO message is now logged whenever an alias is removed in DefaultListableBeanFactory. Closes gh-34070 (cherry picked from commit 41d9f21ab97b8cda9c3b740fc10b677c4d27fb50) --- .../beans/factory/support/DefaultListableBeanFactory.java | 5 +++++ .../beans/factory/DefaultListableBeanFactoryTests.java | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index 67858633d774..d0afcf8674a0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -1055,6 +1055,11 @@ else if (!beanDefinition.equals(existingDefinition)) { } } else { + if (logger.isInfoEnabled()) { + logger.info("Removing alias '" + beanName + "' for bean '" + aliasedName + + "' due to registration of bean definition for bean '" + beanName + "': [" + + beanDefinition + "]"); + } removeAlias(beanName); } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java index 7687b17323b5..a0e86132cb5e 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java @@ -885,10 +885,15 @@ void aliasChaining() { @Test void beanDefinitionOverriding() { lbf.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class)); + // Override "test" bean definition. lbf.registerBeanDefinition("test", new RootBeanDefinition(NestedTestBean.class)); + // Temporary "test2" alias for nonexistent bean. lbf.registerAlias("otherTest", "test2"); + // Reassign "test2" alias to "test". lbf.registerAlias("test", "test2"); + // Assign "testX" alias to "test" as well. lbf.registerAlias("test", "testX"); + // Register new "testX" bean definition which also removes the "testX" alias for "test". lbf.registerBeanDefinition("testX", new RootBeanDefinition(TestBean.class)); assertThat(lbf.getBean("test")).isInstanceOf(NestedTestBean.class); From d512aafc36d0c194e755759f60550d0efe40e212 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 11 Dec 2024 16:58:37 +0100 Subject: [PATCH 018/108] Avoid javadoc references to deprecated types/methods (cherry picked from commit 68997d84162f9fdfccb17c23006f3c4f0620a993) --- .../AbstractAsyncReturnValueHandler.java | 6 ++---- .../AsyncHandlerMethodReturnValueHandler.java | 5 ++--- .../client/SimpleClientHttpRequestFactory.java | 18 +++--------------- .../http/client/support/HttpAccessor.java | 4 ++-- .../web/socket/sockjs/client/SockJsClient.java | 12 ++++++------ 5 files changed, 15 insertions(+), 30 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractAsyncReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractAsyncReturnValueHandler.java index 520d3b27bc44..38801e420d80 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractAsyncReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractAsyncReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 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. @@ -22,9 +22,7 @@ /** * Convenient base class for {@link AsyncHandlerMethodReturnValueHandler} - * implementations that support only asynchronous (Future-like) return values - * and merely serve as adapters of such types to Spring's - * {@link org.springframework.util.concurrent.ListenableFuture ListenableFuture}. + * implementations that support only asynchronous (Future-like) return values. * * @author Sebastien Deleuze * @since 4.2 diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AsyncHandlerMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AsyncHandlerMethodReturnValueHandler.java index 311969d82c6d..3dde07d15b4c 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AsyncHandlerMethodReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AsyncHandlerMethodReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -24,8 +24,6 @@ /** * An extension of {@link HandlerMethodReturnValueHandler} for handling async, * Future-like return value types that support success and error callbacks. - * Essentially anything that can be adapted to a - * {@link org.springframework.util.concurrent.ListenableFuture ListenableFuture}. * *

Implementations should consider extending the convenient base class * {@link AbstractAsyncReturnValueHandler}. @@ -71,6 +69,7 @@ public interface AsyncHandlerMethodReturnValueHandler extends HandlerMethodRetur @Nullable default org.springframework.util.concurrent.ListenableFuture toListenableFuture( Object returnValue, MethodParameter returnType) { + CompletableFuture result = toCompletableFuture(returnValue, returnType); return (result != null ? new org.springframework.util.concurrent.CompletableToListenableFutureAdapter<>(result) : diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java index ec2b075bd53f..9c5982f213b9 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -62,16 +62,9 @@ public void setProxy(Proxy proxy) { /** * Indicate whether this request factory should buffer the * {@linkplain ClientHttpRequest#getBody() request body} internally. - *

Default is {@code true}. When sending large amounts of data via POST or PUT, - * it is recommended to change this property to {@code false}, so as not to run - * out of memory. This will result in a {@link ClientHttpRequest} that either - * streams directly to the underlying {@link HttpURLConnection} (if the - * {@link org.springframework.http.HttpHeaders#getContentLength() Content-Length} - * is known in advance), or that will use "Chunked transfer encoding" - * (if the {@code Content-Length} is not known in advance). * @see #setChunkSize(int) - * @see HttpURLConnection#setFixedLengthStreamingMode(int) - * @deprecated since 6.1 requests are never buffered, as if this property is {@code false} + * @deprecated since 6.1 requests are never buffered, + * as if this property is {@code false} */ @Deprecated(since = "6.1", forRemoval = true) public void setBufferRequestBody(boolean bufferRequestBody) { @@ -80,11 +73,6 @@ public void setBufferRequestBody(boolean bufferRequestBody) { /** * Set the number of bytes to write in each chunk when not buffering request * bodies locally. - *

Note that this parameter is only used when - * {@link #setBufferRequestBody(boolean) bufferRequestBody} is set to {@code false}, - * and the {@link org.springframework.http.HttpHeaders#getContentLength() Content-Length} - * is not known in advance. - * @see #setBufferRequestBody(boolean) */ public void setChunkSize(int chunkSize) { this.chunkSize = chunkSize; diff --git a/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java b/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java index 1ac8b7969c8d..58d8cec61ae9 100644 --- a/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java +++ b/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -67,7 +67,7 @@ public abstract class HttpAccessor { * @see #createRequest(URI, HttpMethod) * @see SimpleClientHttpRequestFactory * @see org.springframework.http.client.HttpComponentsClientHttpRequestFactory - * @see org.springframework.http.client.OkHttp3ClientHttpRequestFactory + * @see org.springframework.http.client.JdkClientHttpRequestFactory */ public void setRequestFactory(ClientHttpRequestFactory requestFactory) { Assert.notNull(requestFactory, "ClientHttpRequestFactory must not be null"); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java index 559ac54ffd77..a7dfe0837af2 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -116,13 +116,13 @@ private static InfoReceiver initInfoReceiver(List transports) { /** - * The names of HTTP headers that should be copied from the handshake headers - * of each call to {@link SockJsClient#doHandshake(WebSocketHandler, WebSocketHttpHeaders, URI)} - * and also used with other HTTP requests issued as part of that SockJS - * connection, e.g. the initial info request, XHR send or receive requests. + * The names of HTTP headers that should be copied from the handshake headers of each + * call to {@link SockJsClient#execute(WebSocketHandler, WebSocketHttpHeaders, URI)} + * and also used with other HTTP requests issued as part of that SockJS connection, + * for example, the initial info request, XHR send or receive requests. *

By default if this property is not set, all handshake headers are also * used for other HTTP requests. Set it if you want only a subset of handshake - * headers (e.g. auth headers) to be used for other HTTP requests. + * headers (for example, auth headers) to be used for other HTTP requests. * @param httpHeaderNames the HTTP header names */ public void setHttpHeaderNames(@Nullable String... httpHeaderNames) { From 2c0ce790d86c3829c592549368974fc16c15626f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 11 Dec 2024 17:34:48 +0100 Subject: [PATCH 019/108] Avoid deprecated ListenableFuture name for internal class --- .../handler/invocation/AbstractMethodMessageHandler.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java index b6405818912c..d8c7a9816baf 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -572,7 +572,7 @@ protected void handleMatch(T mapping, HandlerMethod handlerMethod, String lookup if (returnValue != null && this.returnValueHandlers.isAsyncReturnValue(returnValue, returnType)) { CompletableFuture future = this.returnValueHandlers.toCompletableFuture(returnValue, returnType); if (future != null) { - future.whenComplete(new ReturnValueListenableFutureCallback(invocable, message)); + future.whenComplete(new ReturnValueCallback(invocable, message)); } } else { @@ -703,13 +703,13 @@ public int compare(Match match1, Match match2) { } - private class ReturnValueListenableFutureCallback implements BiConsumer { + private class ReturnValueCallback implements BiConsumer { private final InvocableHandlerMethod handlerMethod; private final Message message; - public ReturnValueListenableFutureCallback(InvocableHandlerMethod handlerMethod, Message message) { + public ReturnValueCallback(InvocableHandlerMethod handlerMethod, Message message) { this.handlerMethod = handlerMethod; this.message = message; } From 0976d1a2521b1a35b17300644b65b201f48272e8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 11 Dec 2024 17:49:18 +0100 Subject: [PATCH 020/108] Upgrade to RxJava 3.1.10 and Checkstyle 10.20.2 --- .../java/org/springframework/build/CheckstyleConventions.java | 2 +- framework-platform/framework-platform.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java index bd4bcba5686e..3fad7c143ce4 100644 --- a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java @@ -50,7 +50,7 @@ public void apply(Project project) { project.getPlugins().apply(CheckstylePlugin.class); project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g")); CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class); - checkstyle.setToolVersion("10.20.1"); + checkstyle.setToolVersion("10.20.2"); checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle")); String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion(); DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies(); diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index aeb920fd97db..805eb2b92c16 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -54,7 +54,7 @@ dependencies { api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE") api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE") api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE") - api("io.reactivex.rxjava3:rxjava:3.1.9") + api("io.reactivex.rxjava3:rxjava:3.1.10") api("io.smallrye.reactive:mutiny:1.10.0") api("io.undertow:undertow-core:2.3.18.Final") api("io.undertow:undertow-servlet:2.3.18.Final") From 890a8f4311e9d717e18a4109bfad72b02d596761 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 12 Dec 2024 10:04:11 +0100 Subject: [PATCH 021/108] Next development version (v6.1.17-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 531361dfa877..eccb3877bcdd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.1.16-SNAPSHOT +version=6.1.17-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From 68368621f3ad2b22ea9d3ce29215076a1d006ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 12 Dec 2024 17:35:51 +0100 Subject: [PATCH 022/108] Prevent execution of Antora jobs on forks Closes gh-34083 --- .github/workflows/deploy-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index ea6006f52fc5..cd1aac0d6895 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -15,7 +15,7 @@ permissions: jobs: build: name: Dispatch docs deployment - if: github.repository_owner == 'spring-projects' + if: ${{ github.repository == 'spring-projects/spring-framework' }} runs-on: ubuntu-latest steps: - name: Check out code From 95003e35128d0bf3f049145dee48a08ba440ccdb Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 12 Dec 2024 19:48:21 +0100 Subject: [PATCH 023/108] Avoid logger serialization behind shared EntityManager proxy See gh-34084 --- .../org/springframework/orm/jpa/SharedEntityManagerCreator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java index ef20dfc3efcd..b47fc5fe9ac3 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java @@ -185,7 +185,7 @@ public static EntityManager createSharedEntityManager(EntityManagerFactory emf, @SuppressWarnings("serial") private static class SharedEntityManagerInvocationHandler implements InvocationHandler, Serializable { - private final Log logger = LogFactory.getLog(getClass()); + private static final Log logger = LogFactory.getLog(SharedEntityManagerInvocationHandler.class); private final EntityManagerFactory targetFactory; From 4b45338ae628f95421911e9f2e18da42d0d5ba91 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 17 Dec 2024 10:13:56 +0100 Subject: [PATCH 024/108] Improve PathMatcher/PatternParser XML configuration Prior to this commit, the MVC namespace for the XML Spring configuration model would use the `PathMatcher` bean instance when provided like this: ``` ``` With this configuration, the handler mapping for annotated controller would use the given `AntPathMatcher` instance but the handler mapping for resources would still use the default, which is `PathPatternParser` since 6.0. This commit ensures that when a custom `path-matcher` is defined, it's consistently used for all MVC handler mappings as an alias to the well-known bean name. This allows to use `AntPathMatcher` consistently while working on a migration path to `PathPatternParser` This commit also adds a new XML attribute to the path matching configuration that makes it possible to use a custom `PathPatternParser` instance: ``` ``` Closes gh-34102 See gh-34064 --- .../AnnotationDrivenBeanDefinitionParser.java | 19 +++-- .../web/servlet/config/MvcNamespaceUtils.java | 77 ++++++++++++++++++- .../config/ResourcesBeanDefinitionParser.java | 7 +- .../ViewControllerBeanDefinitionParser.java | 4 +- .../web/servlet/config/spring-mvc.xsd | 9 ++- ...tationDrivenBeanDefinitionParserTests.java | 1 + .../web/servlet/config/MvcNamespaceTests.java | 46 +++++++++-- .../mvc-config-custom-pattern-parser.xml | 17 ++++ .../mvc-config-deprecated-path-matcher.xml | 17 ++++ 9 files changed, 176 insertions(+), 21 deletions(-) create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-custom-pattern-parser.xml create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-deprecated-path-matcher.xml diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index b48e06b26241..1ec70aa2947e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java @@ -401,16 +401,13 @@ private void configurePathMatchingProperties( RootBeanDefinition handlerMappingDef, Element element, ParserContext context) { Element pathMatchingElement = DomUtils.getChildElementByTagName(element, "path-matching"); + Object source = context.extractSource(element); if (pathMatchingElement != null) { - Object source = context.extractSource(element); - if (pathMatchingElement.hasAttribute("trailing-slash")) { boolean useTrailingSlashMatch = Boolean.parseBoolean(pathMatchingElement.getAttribute("trailing-slash")); handlerMappingDef.getPropertyValues().add("useTrailingSlashMatch", useTrailingSlashMatch); } - boolean preferPathMatcher = false; - if (pathMatchingElement.hasAttribute("suffix-pattern")) { boolean useSuffixPatternMatch = Boolean.parseBoolean(pathMatchingElement.getAttribute("suffix-pattern")); handlerMappingDef.getPropertyValues().add("useSuffixPatternMatch", useSuffixPatternMatch); @@ -435,12 +432,20 @@ private void configurePathMatchingProperties( pathMatcherRef = new RuntimeBeanReference(pathMatchingElement.getAttribute("path-matcher")); preferPathMatcher = true; } - pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(pathMatcherRef, context, source); - handlerMappingDef.getPropertyValues().add("pathMatcher", pathMatcherRef); - if (preferPathMatcher) { + pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(pathMatcherRef, context, source); + handlerMappingDef.getPropertyValues().add("pathMatcher", pathMatcherRef); handlerMappingDef.getPropertyValues().add("patternParser", null); } + else if (pathMatchingElement.hasAttribute("pattern-parser")) { + RuntimeBeanReference patternParserRef = new RuntimeBeanReference(pathMatchingElement.getAttribute("pattern-parser")); + patternParserRef = MvcNamespaceUtils.registerPatternParser(patternParserRef, context, source); + handlerMappingDef.getPropertyValues().add("patternParser", patternParserRef); + } + } + else { + RuntimeBeanReference pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(null, context, source); + handlerMappingDef.getPropertyValues().add("pathMatcher", pathMatcherRef); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java index 9e7d7d681aee..e68be743bd88 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -28,9 +28,11 @@ import org.springframework.beans.factory.xml.ParserContext; import org.springframework.lang.Nullable; import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; import org.springframework.util.PathMatcher; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.handler.AbstractHandlerMapping; import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; @@ -39,6 +41,7 @@ import org.springframework.web.servlet.support.SessionFlashMapManager; import org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator; import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.pattern.PathPatternParser; /** * Convenience methods for use in MVC namespace BeanDefinitionParsers. @@ -64,6 +67,8 @@ public abstract class MvcNamespaceUtils { private static final String PATH_MATCHER_BEAN_NAME = "mvcPathMatcher"; + private static final String PATTERN_PARSER_BEAN_NAME = "mvcPatternParser"; + private static final String CORS_CONFIGURATION_BEAN_NAME = "mvcCorsConfigurations"; private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector"; @@ -105,6 +110,18 @@ else if (!context.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME) && return new RuntimeBeanReference(URL_PATH_HELPER_BEAN_NAME); } + /** + * Return the {@link PathMatcher} bean definition if it has been registered + * in the context as an alias with its well-known name, or {@code null}. + */ + @Nullable + static RuntimeBeanReference getCustomPathMatcher(ParserContext context) { + if(context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME)) { + return new RuntimeBeanReference(PATH_MATCHER_BEAN_NAME); + } + return null; + } + /** * Adds an alias to an existing well-known name or registers a new instance of a {@link PathMatcher} * under that well-known name, unless already registered. @@ -117,6 +134,9 @@ public static RuntimeBeanReference registerPathMatcher(@Nullable RuntimeBeanRefe if (context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME)) { context.getRegistry().removeAlias(PATH_MATCHER_BEAN_NAME); } + if (context.getRegistry().containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) { + context.getRegistry().removeBeanDefinition(PATH_MATCHER_BEAN_NAME); + } context.getRegistry().registerAlias(pathMatcherRef.getBeanName(), PATH_MATCHER_BEAN_NAME); } else if (!context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME) && @@ -130,6 +150,60 @@ else if (!context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME) && return new RuntimeBeanReference(PATH_MATCHER_BEAN_NAME); } + /** + * Return the {@link PathPatternParser} bean definition if it has been registered + * in the context as an alias with its well-known name, or {@code null}. + */ + @Nullable + static RuntimeBeanReference getCustomPatternParser(ParserContext context) { + if (context.getRegistry().isAlias(PATTERN_PARSER_BEAN_NAME)) { + return new RuntimeBeanReference(PATTERN_PARSER_BEAN_NAME); + } + return null; + } + + /** + * Adds an alias to an existing well-known name or registers a new instance of a {@link PathPatternParser} + * under that well-known name, unless already registered. + * @return a RuntimeBeanReference to this {@link PathPatternParser} instance + */ + public static RuntimeBeanReference registerPatternParser(@Nullable RuntimeBeanReference patternParserRef, + ParserContext context, @Nullable Object source) { + if (patternParserRef != null) { + if (context.getRegistry().isAlias(PATTERN_PARSER_BEAN_NAME)) { + context.getRegistry().removeAlias(PATTERN_PARSER_BEAN_NAME); + } + context.getRegistry().registerAlias(patternParserRef.getBeanName(), PATTERN_PARSER_BEAN_NAME); + } + else if (!context.getRegistry().isAlias(PATTERN_PARSER_BEAN_NAME) && + !context.getRegistry().containsBeanDefinition(PATTERN_PARSER_BEAN_NAME)) { + RootBeanDefinition pathMatcherDef = new RootBeanDefinition(PathPatternParser.class); + pathMatcherDef.setSource(source); + pathMatcherDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + context.getRegistry().registerBeanDefinition(PATTERN_PARSER_BEAN_NAME, pathMatcherDef); + context.registerComponent(new BeanComponentDefinition(pathMatcherDef, PATTERN_PARSER_BEAN_NAME)); + } + return new RuntimeBeanReference(PATTERN_PARSER_BEAN_NAME); + } + + static void configurePathMatching(RootBeanDefinition handlerMappingDef, ParserContext context, @Nullable Object source) { + Assert.isTrue(AbstractHandlerMapping.class.isAssignableFrom(handlerMappingDef.getBeanClass()), + () -> "Handler mapping type [" + handlerMappingDef.getTargetType() + "] not supported"); + RuntimeBeanReference customPathMatcherRef = MvcNamespaceUtils.getCustomPathMatcher(context); + RuntimeBeanReference customPatternParserRef = MvcNamespaceUtils.getCustomPatternParser(context); + if (customPathMatcherRef != null) { + handlerMappingDef.getPropertyValues().add("pathMatcher", customPathMatcherRef) + .add("patternParser", null); + } + else if (customPatternParserRef != null) { + handlerMappingDef.getPropertyValues().add("patternParser", customPatternParserRef); + } + else { + handlerMappingDef.getPropertyValues().add("pathMatcher", + MvcNamespaceUtils.registerPathMatcher(null, context, source)); + } + } + /** * Registers an {@link HttpRequestHandlerAdapter} under a well-known * name unless already registered. @@ -142,6 +216,7 @@ private static void registerBeanNameUrlHandlerMapping(ParserContext context, @Nu mappingDef.getPropertyValues().add("order", 2); // consistent with WebMvcConfigurationSupport RuntimeBeanReference corsRef = MvcNamespaceUtils.registerCorsConfigurations(null, context, source); mappingDef.getPropertyValues().add("corsConfigurations", corsRef); + configurePathMatching(mappingDef, context, source); context.getRegistry().registerBeanDefinition(BEAN_NAME_URL_HANDLER_MAPPING_BEAN_NAME, mappingDef); context.registerComponent(new BeanComponentDefinition(mappingDef, BEAN_NAME_URL_HANDLER_MAPPING_BEAN_NAME)); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java index 8f85ba6efe49..62338d80520a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -92,7 +92,6 @@ public BeanDefinition parse(Element element, ParserContext context) { registerUrlProvider(context, source); - RuntimeBeanReference pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(null, context, source); RuntimeBeanReference pathHelperRef = MvcNamespaceUtils.registerUrlPathHelper(null, context, source); String resourceHandlerName = registerResourceHandler(context, element, pathHelperRef, source); @@ -111,8 +110,8 @@ public BeanDefinition parse(Element element, ParserContext context) { RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class); handlerMappingDef.setSource(source); handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - handlerMappingDef.getPropertyValues().add("urlMap", urlMap); - handlerMappingDef.getPropertyValues().add("pathMatcher", pathMatcherRef).add("urlPathHelper", pathHelperRef); + handlerMappingDef.getPropertyValues().add("urlMap", urlMap).add("urlPathHelper", pathHelperRef); + MvcNamespaceUtils.configurePathMatching(handlerMappingDef, context, source); String orderValue = element.getAttribute("order"); // Use a default of near-lowest precedence, still allowing for even lower precedence in other mappings diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java index 8a39ed1a2832..5964af08ad11 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -123,7 +123,7 @@ private BeanDefinition registerHandlerMapping(ParserContext context, @Nullable O beanDef.setSource(source); beanDef.getPropertyValues().add("order", "1"); - beanDef.getPropertyValues().add("pathMatcher", MvcNamespaceUtils.registerPathMatcher(null, context, source)); + MvcNamespaceUtils.configurePathMatching(beanDef, context, source); beanDef.getPropertyValues().add("urlPathHelper", MvcNamespaceUtils.registerUrlPathHelper(null, context, source)); RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, context, source); beanDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef); diff --git a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd index 6a674fcf1794..1131f9bc90bd 100644 --- a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd +++ b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd @@ -89,7 +89,14 @@ + + + + + diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java index 42de3720f386..4325cb39633c 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java @@ -89,6 +89,7 @@ public void testPathMatchingConfiguration() { assertThat(hm.useRegisteredSuffixPatternMatch()).isTrue(); assertThat(hm.getUrlPathHelper()).isInstanceOf(TestPathHelper.class); assertThat(hm.getPathMatcher()).isInstanceOf(TestPathMatcher.class); + assertThat(hm.getPatternParser()).isNull(); List fileExtensions = hm.getContentNegotiationManager().getAllFileExtensions(); assertThat(fileExtensions).containsExactly("xml"); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index eb9cba86ba5a..011b4808c96e 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -64,6 +64,7 @@ import org.springframework.lang.Nullable; import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor; import org.springframework.stereotype.Controller; +import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; @@ -145,6 +146,7 @@ import org.springframework.web.testfixture.servlet.MockRequestDispatcher; import org.springframework.web.testfixture.servlet.MockServletContext; import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.pattern.PathPatternParser; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -202,6 +204,8 @@ void testDefaultConfig() throws Exception { assertThat(mapping).isNotNull(); assertThat(mapping.getOrder()).isEqualTo(0); assertThat(mapping.getUrlPathHelper().shouldRemoveSemicolonContent()).isTrue(); + assertThat(mapping.getPathMatcher()).isEqualTo(appContext.getBean("mvcPathMatcher")); + assertThat(mapping.getPatternParser()).isNotNull(); mapping.setDefaultHandler(handlerMethod); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.json"); @@ -392,6 +396,8 @@ void testResources() throws Exception { SimpleUrlHandlerMapping resourceMapping = appContext.getBean(SimpleUrlHandlerMapping.class); assertThat(resourceMapping).isNotNull(); assertThat(resourceMapping.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE - 1); + assertThat(resourceMapping.getPathMatcher()).isNotNull(); + assertThat(resourceMapping.getPatternParser()).isNotNull(); BeanNameUrlHandlerMapping beanNameMapping = appContext.getBean(BeanNameUrlHandlerMapping.class); assertThat(beanNameMapping).isNotNull(); @@ -423,6 +429,31 @@ void testResources() throws Exception { .isInstanceOf(NoResourceFoundException.class); } + @Test + void testUseDeprecatedPathMatcher() throws Exception { + loadBeanDefinitions("mvc-config-deprecated-path-matcher.xml"); + Map handlerMappings = appContext.getBeansOfType(AbstractHandlerMapping.class); + AntPathMatcher mvcPathMatcher = appContext.getBean("pathMatcher", AntPathMatcher.class); + assertThat(handlerMappings).hasSize(4); + handlerMappings.forEach((name, hm) -> { + assertThat(hm.getPathMatcher()).as("path matcher for %s", name).isEqualTo(mvcPathMatcher); + assertThat(hm.getPatternParser()).as("pattern parser for %s", name).isNull(); + }); + } + + @Test + void testUsePathPatternParser() throws Exception { + loadBeanDefinitions("mvc-config-custom-pattern-parser.xml"); + + PathPatternParser patternParser = appContext.getBean("patternParser", PathPatternParser.class); + Map handlerMappings = appContext.getBeansOfType(AbstractHandlerMapping.class); + assertThat(handlerMappings).hasSize(4); + handlerMappings.forEach((name, hm) -> { + assertThat(hm.getPathMatcher()).as("path matcher for %s", name).isNotNull(); + assertThat(hm.getPatternParser()).as("pattern parser for %s", name).isEqualTo(patternParser); + }); + } + @Test void testResourcesWithOptionalAttributes() { loadBeanDefinitions("mvc-config-resources-optional-attrs.xml"); @@ -600,6 +631,9 @@ void testViewControllers() throws Exception { assertThat(beanNameMapping).isNotNull(); assertThat(beanNameMapping.getOrder()).isEqualTo(2); + assertThat(beanNameMapping.getPathMatcher()).isNotNull(); + assertThat(beanNameMapping.getPatternParser()).isNotNull(); + MockHttpServletRequest request = new MockHttpServletRequest(); request.setMethod("GET"); @@ -895,11 +929,11 @@ void testPathMatchingHandlerMappings() { assertThat(viewController.getUrlPathHelper().getClass()).isEqualTo(TestPathHelper.class); assertThat(viewController.getPathMatcher().getClass()).isEqualTo(TestPathMatcher.class); - for (SimpleUrlHandlerMapping handlerMapping : appContext.getBeansOfType(SimpleUrlHandlerMapping.class).values()) { + appContext.getBeansOfType(SimpleUrlHandlerMapping.class).forEach((name, handlerMapping) -> { assertThat(handlerMapping).isNotNull(); - assertThat(handlerMapping.getUrlPathHelper().getClass()).isEqualTo(TestPathHelper.class); - assertThat(handlerMapping.getPathMatcher().getClass()).isEqualTo(TestPathMatcher.class); - } + assertThat(handlerMapping.getUrlPathHelper().getClass()).as("path helper for %s", name).isEqualTo(TestPathHelper.class); + assertThat(handlerMapping.getPathMatcher().getClass()).as("path matcher for %s", name).isEqualTo(TestPathMatcher.class); + }); } @Test @@ -909,7 +943,7 @@ void testCorsMinimal() { String[] beanNames = appContext.getBeanNamesForType(AbstractHandlerMapping.class); assertThat(beanNames).hasSize(2); for (String beanName : beanNames) { - AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping)appContext.getBean(beanName); + AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping) appContext.getBean(beanName); assertThat(handlerMapping).isNotNull(); DirectFieldAccessor accessor = new DirectFieldAccessor(handlerMapping); Map configs = ((UrlBasedCorsConfigurationSource) accessor @@ -934,7 +968,7 @@ void testCors() { String[] beanNames = appContext.getBeanNamesForType(AbstractHandlerMapping.class); assertThat(beanNames).hasSize(2); for (String beanName : beanNames) { - AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping)appContext.getBean(beanName); + AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping) appContext.getBean(beanName); assertThat(handlerMapping).isNotNull(); DirectFieldAccessor accessor = new DirectFieldAccessor(handlerMapping); Map configs = ((UrlBasedCorsConfigurationSource) accessor diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-custom-pattern-parser.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-custom-pattern-parser.xml new file mode 100644 index 000000000000..4e3e5bec51d9 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-custom-pattern-parser.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-deprecated-path-matcher.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-deprecated-path-matcher.xml new file mode 100644 index 000000000000..78afbe8cb6f8 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-deprecated-path-matcher.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + From 9c934b5019852caa85ed321206276353a19b42c0 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:27:07 +0100 Subject: [PATCH 025/108] Support varargs-only MethodHandle as SpEL function Prior to this commit, if a MethodHandle was registered as a custom function in the Spring Expression Language (SpEL) for a static method that accepted only a variable argument list (for example, `static String func(String... args)`), attempting to invoke the registered function within a SpEL expression resulted in a ClassCastException because the varargs array was unnecessarily wrapped in an Object[]. This commit modifies the logic in FunctionReference's internal executeFunctionViaMethodHandle() method to address that. Closes gh-34109 --- .../spel/ast/FunctionReference.java | 5 ++-- .../expression/spel/TestScenarioCreator.java | 7 +++++- .../spel/VariableAndFunctionTests.java | 23 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java index 8ffed18ca766..91df4ce30a23 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java @@ -226,8 +226,9 @@ else if (spelParamCount != declaredParamCount) { ReflectionHelper.convertAllMethodHandleArguments(converter, functionArgs, methodHandle, varArgPosition); if (isSuspectedVarargs) { - if (declaredParamCount == 1) { - // We only repackage the varargs if it is the ONLY argument -- for example, + if (declaredParamCount == 1 && !methodHandle.isVarargsCollector()) { + // We only repackage the arguments if the MethodHandle accepts a single + // argument AND the MethodHandle is not a "varargs collector" -- for example, // when we are dealing with a bound MethodHandle. functionArgs = ReflectionHelper.setupArgumentsForVarargsInvocation( methodHandle.type().parameterArray(), functionArgs); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java b/spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java index 895952f62a45..d9280f5be4bf 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java @@ -108,11 +108,16 @@ private static void populateMethodHandles(StandardEvaluationContext testContext) "formatObjectVarargs", MethodType.methodType(String.class, String.class, Object[].class)); testContext.registerFunction("formatObjectVarargs", formatObjectVarargs); - // #formatObjectVarargs(format, args...) + // #formatPrimitiveVarargs(format, args...) MethodHandle formatPrimitiveVarargs = MethodHandles.lookup().findStatic(TestScenarioCreator.class, "formatPrimitiveVarargs", MethodType.methodType(String.class, String.class, int[].class)); testContext.registerFunction("formatPrimitiveVarargs", formatPrimitiveVarargs); + // #varargsFunctionHandle(args...) + MethodHandle varargsFunctionHandle = MethodHandles.lookup().findStatic(TestScenarioCreator.class, + "varargsFunction", MethodType.methodType(String.class, String[].class)); + testContext.registerFunction("varargsFunctionHandle", varargsFunctionHandle); + // #add(int, int) MethodHandle add = MethodHandles.lookup().findStatic(TestScenarioCreator.class, "add", MethodType.methodType(int.class, int.class, int.class)); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java index 38d7d047f210..88f8b0b8b39d 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java @@ -79,6 +79,8 @@ void functionInvocationWithStringArgument() { @Test void functionWithVarargs() { + // static String varargsFunction(String... strings) -> Arrays.toString(strings) + evaluate("#varargsFunction()", "[]", String.class); evaluate("#varargsFunction(new String[0])", "[]", String.class); evaluate("#varargsFunction('a')", "[a]", String.class); @@ -241,6 +243,27 @@ void functionFromMethodHandleWithListConvertedToVarargsArray() { evaluate("#formatObjectVarargs('x -> %s %s %s', {'a', 'b', 'c'})", expected, String.class); } + @Test // gh-34109 + void functionViaMethodHandleForStaticMethodThatAcceptsOnlyVarargs() { + // #varargsFunctionHandle: static String varargsFunction(String... strings) -> Arrays.toString(strings) + + evaluate("#varargsFunctionHandle()", "[]", String.class); + evaluate("#varargsFunctionHandle(new String[0])", "[]", String.class); + evaluate("#varargsFunctionHandle('a')", "[a]", String.class); + evaluate("#varargsFunctionHandle('a','b','c')", "[a, b, c]", String.class); + evaluate("#varargsFunctionHandle(new String[]{'a','b','c'})", "[a, b, c]", String.class); + // Conversion from int to String + evaluate("#varargsFunctionHandle(25)", "[25]", String.class); + evaluate("#varargsFunctionHandle('b',25)", "[b, 25]", String.class); + evaluate("#varargsFunctionHandle(new int[]{1, 2, 3})", "[1, 2, 3]", String.class); + // Strings that contain a comma + evaluate("#varargsFunctionHandle('a,b')", "[a,b]", String.class); + evaluate("#varargsFunctionHandle('a', 'x,y', 'd')", "[a, x,y, d]", String.class); + // null values + evaluate("#varargsFunctionHandle(null)", "[null]", String.class); + evaluate("#varargsFunctionHandle('a',null,'b')", "[a, null, b]", String.class); + } + @Test void functionMethodMustBeStatic() throws Exception { SpelExpressionParser parser = new SpelExpressionParser(); From 8ccaabe7782edf94dc614e9dd48dcca3bf7b14a8 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Wed, 18 Dec 2024 21:38:52 +0700 Subject: [PATCH 026/108] Fix broken links in the web reference documentation Backport of gh-34115. Closes gh-34139 --- framework-docs/modules/ROOT/pages/testing/webtestclient.adoc | 2 +- framework-docs/modules/ROOT/pages/web/webflux-cors.adoc | 3 ++- framework-docs/modules/ROOT/pages/web/webflux-functional.adoc | 1 + framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc | 1 + .../modules/ROOT/pages/web/webflux/new-framework.adoc | 2 +- framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc | 1 + framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc | 3 ++- framework-docs/modules/ROOT/pages/web/websocket.adoc | 1 + 8 files changed, 10 insertions(+), 4 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc b/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc index 7f0fa031ef80..1556384f0bb4 100644 --- a/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc +++ b/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc @@ -193,7 +193,7 @@ Kotlin:: [[webtestclient-fn-config]] === Bind to Router Function -This setup allows you to test <> via +This setup allows you to test xref:web/webflux-functional.adoc[functional endpoints] via mock request and response objects, without a running server. For WebFlux, use the following which delegates to `RouterFunctions.toWebHandler` to diff --git a/framework-docs/modules/ROOT/pages/web/webflux-cors.adoc b/framework-docs/modules/ROOT/pages/web/webflux-cors.adoc index 4de277efa7ea..353e299323d9 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-cors.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-cors.adoc @@ -1,5 +1,6 @@ [[webflux-cors]] = CORS + [.small]#xref:web/webmvc-cors.adoc[See equivalent in the Servlet stack]# Spring WebFlux lets you handle CORS (Cross-Origin Resource Sharing). This section @@ -364,7 +365,7 @@ Kotlin:: You can apply CORS support through the built-in {spring-framework-api}/web/cors/reactive/CorsWebFilter.html[`CorsWebFilter`], which is a -good fit with <>. +good fit with xref:web/webflux-functional.adoc[functional endpoints]. NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring Security has {docs-spring-security}/servlet/integrations/cors.html[built-in support] for diff --git a/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc b/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc index 5f03e5a131ea..3cd907e88fd8 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc @@ -1,5 +1,6 @@ [[webflux-fn]] = Functional Endpoints + [.small]#xref:web/webmvc-functional.adoc[See equivalent in the Servlet stack]# Spring WebFlux includes WebFlux.fn, a lightweight functional programming model in which functions diff --git a/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc b/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc index 204c5f771fe8..a2843c94953b 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc @@ -1,5 +1,6 @@ [[webflux-websocket]] = WebSockets + [.small]#xref:web/websocket.adoc[See equivalent in the Servlet stack]# This part of the reference documentation covers support for reactive-stack WebSocket diff --git a/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc b/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc index b03cefb04bbe..9b7022fb72a2 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc @@ -101,7 +101,7 @@ On that foundation, Spring WebFlux provides a choice of two programming models: from the `spring-web` module. Both Spring MVC and WebFlux controllers support reactive (Reactor and RxJava) return types, and, as a result, it is not easy to tell them apart. One notable difference is that WebFlux also supports reactive `@RequestBody` arguments. -* <>: Lambda-based, lightweight, and functional programming model. You can think of +* xref:web/webflux-functional.adoc[Functional Endpoints]: Lambda-based, lightweight, and functional programming model. You can think of this as a small library or a set of utilities that an application can use to route and handle requests. The big difference with annotated controllers is that the application is in charge of request handling from start to finish versus declaring intent through diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc index a8e7bb148b64..9b4e434759c1 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc @@ -1,5 +1,6 @@ [[mvc-cors]] = CORS + [.small]#xref:web/webflux-cors.adoc[See equivalent in the Reactive stack]# Spring MVC lets you handle CORS (Cross-Origin Resource Sharing). This section diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc index 276adcade006..59355b01a6df 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc @@ -1,6 +1,7 @@ [[webmvc-fn]] = Functional Endpoints -[.small]#<># + +[.small]#xref:web/webflux-functional.adoc[See equivalent in the Reactive stack]# Spring Web MVC includes WebMvc.fn, a lightweight functional programming model in which functions are used to route and handle requests and contracts are designed for immutability. diff --git a/framework-docs/modules/ROOT/pages/web/websocket.adoc b/framework-docs/modules/ROOT/pages/web/websocket.adoc index 726c9c2de3ed..f917e7e09390 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket.adoc @@ -1,6 +1,7 @@ [[websocket]] = WebSockets :page-section-summary-toc: 1 + [.small]#xref:web/webflux-websocket.adoc[See equivalent in the Reactive stack]# This part of the reference documentation covers support for Servlet stack, WebSocket From 898d3ec86aaaaa76f69f1e65adf9b52f6a26e7ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 23 Dec 2024 11:29:32 +0100 Subject: [PATCH 027/108] Backport tests for exact match resolution See gh-34124 --- .../util/PropertyPlaceholderHelperTests.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java b/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java index 429df4d0a449..5866bcc007bb 100644 --- a/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java +++ b/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -156,6 +156,23 @@ static Stream defaultValues() { ); } + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("exactMatchPlaceholders") + void placeholdersWithExactMatchAreConsidered(String text, String expected) { + Properties properties = new Properties(); + properties.setProperty("prefix://my-service", "example-service"); + properties.setProperty("px", "prefix"); + properties.setProperty("p1", "${prefix://my-service}"); + assertThat(this.helper.replacePlaceholders(text, properties)).isEqualTo(expected); + } + + static Stream exactMatchPlaceholders() { + return Stream.of( + Arguments.of("${prefix://my-service}", "example-service"), + Arguments.of("${p1}", "example-service") + ); + } + } PlaceholderResolver mockPlaceholderResolver(String... pairs) { From 875cc828cf874315bc3ea2b7b0ef0d7f1bf00166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 30 Dec 2024 12:14:52 +0100 Subject: [PATCH 028/108] Upgrade to Java 17.0.13 --- .sdkmanrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.sdkmanrc b/.sdkmanrc index 828308d277ec..eb2990e97224 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,3 +1,3 @@ # Enable auto-env through the sdkman_auto_env config # Add key=value pairs of SDKs to use below -java=17.0.12-librca +java=17.0.13-librca From 245341231f13be941343165a9fa9adae7e118d5c Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 2 Jan 2025 13:32:41 +0000 Subject: [PATCH 029/108] Polishing in WebAsyncManager See gh-34192 --- .../context/request/async/DeferredResult.java | 106 ++++++++++-------- .../request/async/WebAsyncManager.java | 76 ++++++------- .../request/async/DeferredResultTests.java | 8 +- .../annotation/ReactiveTypeHandler.java | 10 +- 4 files changed, 105 insertions(+), 95 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java index 3fbd694e8b76..d5e16c10666c 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java @@ -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"); * you may not use this file except in compliance with the License. @@ -288,65 +288,75 @@ public boolean setErrorResult(Object result) { } - final DeferredResultProcessingInterceptor getInterceptor() { - return new DeferredResultProcessingInterceptor() { - @Override - public boolean handleTimeout(NativeWebRequest request, DeferredResult deferredResult) { - boolean continueProcessing = true; - try { - if (timeoutCallback != null) { - timeoutCallback.run(); - } - } - finally { - Object value = timeoutResult.get(); - if (value != RESULT_NONE) { - continueProcessing = false; - try { - setResultInternal(value); - } - catch (Throwable ex) { - logger.debug("Failed to handle timeout result", ex); - } - } + final DeferredResultProcessingInterceptor getLifecycleInterceptor() { + return new LifecycleInterceptor(); + } + + + /** + * Handles a DeferredResult value when set. + */ + @FunctionalInterface + public interface DeferredResultHandler { + + void handleResult(@Nullable Object result); + } + + + /** + * Instance interceptor to receive Servlet container notifications. + */ + private class LifecycleInterceptor implements DeferredResultProcessingInterceptor { + + @Override + public boolean handleTimeout(NativeWebRequest request, DeferredResult result) { + boolean continueProcessing = true; + try { + if (timeoutCallback != null) { + timeoutCallback.run(); } - return continueProcessing; } - @Override - public boolean handleError(NativeWebRequest request, DeferredResult deferredResult, Throwable t) { - try { - if (errorCallback != null) { - errorCallback.accept(t); - } - } - finally { + finally { + Object value = timeoutResult.get(); + if (value != RESULT_NONE) { + continueProcessing = false; try { - setResultInternal(t); + setResultInternal(value); } catch (Throwable ex) { - logger.debug("Failed to handle error result", ex); + logger.debug("Failed to handle timeout result", ex); } } - return false; } - @Override - public void afterCompletion(NativeWebRequest request, DeferredResult deferredResult) { - expired = true; - if (completionCallback != null) { - completionCallback.run(); + return continueProcessing; + } + + @Override + public boolean handleError(NativeWebRequest request, DeferredResult result, Throwable t) { + try { + if (errorCallback != null) { + errorCallback.accept(t); } } - }; - } - + finally { + try { + setResultInternal(t); + } + catch (Throwable ex) { + logger.debug("Failed to handle error result", ex); + } + } + return false; + } - /** - * Handles a DeferredResult value when set. - */ - @FunctionalInterface - public interface DeferredResultHandler { + @Override + public void afterCompletion(NativeWebRequest request, DeferredResult result) { + expired = true; + if (completionCallback != null) { + completionCallback.run(); + } + } - void handleResult(@Nullable Object result); } } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java index 3fbbf15f1046..e42cdb17cb0c 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java @@ -382,36 +382,6 @@ public void startCallableProcessing(final WebAsyncTask webAsyncTask, Object.. } } - private void setConcurrentResultAndDispatch(@Nullable Object result) { - Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); - synchronized (WebAsyncManager.this) { - if (!this.state.compareAndSet(State.ASYNC_PROCESSING, State.RESULT_SET)) { - if (logger.isDebugEnabled()) { - logger.debug("Async result already set: [" + this.state.get() + - "], ignored result for " + formatUri(this.asyncWebRequest)); - } - return; - } - - this.concurrentResult = result; - if (logger.isDebugEnabled()) { - logger.debug("Async result set for " + formatUri(this.asyncWebRequest)); - } - - if (this.asyncWebRequest.isAsyncComplete()) { - if (logger.isDebugEnabled()) { - logger.debug("Async request already completed for " + formatUri(this.asyncWebRequest)); - } - return; - } - - if (logger.isDebugEnabled()) { - logger.debug("Performing async dispatch for " + formatUri(this.asyncWebRequest)); - } - this.asyncWebRequest.dispatch(); - } - } - /** * Start concurrent request processing and initialize the given * {@link DeferredResult} with a {@link DeferredResultHandler} that saves @@ -443,7 +413,7 @@ public void startDeferredResultProcessing( } List interceptors = new ArrayList<>(); - interceptors.add(deferredResult.getInterceptor()); + interceptors.add(deferredResult.getLifecycleInterceptor()); interceptors.addAll(this.deferredResultInterceptors.values()); interceptors.add(timeoutDeferredResultInterceptor); @@ -508,6 +478,36 @@ private void startAsyncProcessing(Object[] processingContext) { this.asyncWebRequest.startAsync(); } + private void setConcurrentResultAndDispatch(@Nullable Object result) { + Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); + synchronized (WebAsyncManager.this) { + if (!this.state.compareAndSet(State.ASYNC_PROCESSING, State.RESULT_SET)) { + if (logger.isDebugEnabled()) { + logger.debug("Async result already set: [" + this.state.get() + "], " + + "ignored result for " + formatUri(this.asyncWebRequest)); + } + return; + } + + this.concurrentResult = result; + if (logger.isDebugEnabled()) { + logger.debug("Async result set for " + formatUri(this.asyncWebRequest)); + } + + if (this.asyncWebRequest.isAsyncComplete()) { + if (logger.isDebugEnabled()) { + logger.debug("Async request already completed for " + formatUri(this.asyncWebRequest)); + } + return; + } + + if (logger.isDebugEnabled()) { + logger.debug("Performing async dispatch for " + formatUri(this.asyncWebRequest)); + } + this.asyncWebRequest.dispatch(); + } + } + private static String formatUri(AsyncWebRequest asyncWebRequest) { HttpServletRequest request = asyncWebRequest.getNativeRequest(HttpServletRequest.class); return (request != null ? "\"" + request.getRequestURI() + "\"" : "servlet container"); @@ -517,13 +517,13 @@ private static String formatUri(AsyncWebRequest asyncWebRequest) { /** * Represents a state for {@link WebAsyncManager} to be in. *

-	 *        NOT_STARTED <------+
-	 *             |             |
-	 *             v             |
-	 *      ASYNC_PROCESSING     |
-	 *             |             |
-	 *             v             |
-	 *         RESULT_SET -------+
+	 *     +------> NOT_STARTED <------+
+	 *     |             |             |
+	 *     |             v             |
+	 *     |      ASYNC_PROCESSING     |
+	 *     |             |             |
+	 *     |             v             |
+	 *     <-------+ RESULT_SET -------+
 	 * 
* @since 5.3.33 */ diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/DeferredResultTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/DeferredResultTests.java index c5f632d60f1d..24621bd75099 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/async/DeferredResultTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/DeferredResultTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -93,7 +93,7 @@ void onCompletion() throws Exception { DeferredResult result = new DeferredResult<>(); result.onCompletion(() -> sb.append("completion event")); - result.getInterceptor().afterCompletion(null, null); + result.getLifecycleInterceptor().afterCompletion(null, null); assertThat(result.isSetOrExpired()).isTrue(); assertThat(sb.toString()).isEqualTo("completion event"); @@ -109,7 +109,7 @@ void onTimeout() throws Exception { result.setResultHandler(handler); result.onTimeout(() -> sb.append("timeout event")); - result.getInterceptor().handleTimeout(null, null); + result.getLifecycleInterceptor().handleTimeout(null, null); assertThat(sb.toString()).isEqualTo("timeout event"); assertThat(result.setResult("hello")).as("Should not be able to set result a second time").isFalse(); @@ -127,7 +127,7 @@ void onError() throws Exception { Exception e = new Exception(); result.onError(t -> sb.append("error event")); - result.getInterceptor().handleError(null, null, e); + result.getLifecycleInterceptor().handleError(null, null, e); assertThat(sb.toString()).isEqualTo("error event"); assertThat(result.setResult("hello")).as("Should not be able to set result a second time").isFalse(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandler.java index 4d22dd957d95..6014c0a8169b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandler.java @@ -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"); * you may not use this file except in compliance with the License. @@ -333,8 +333,8 @@ public void run() { this.subscription.request(1); } catch (final Throwable ex) { - if (logger.isTraceEnabled()) { - logger.trace("Send for " + this.emitter + " failed: " + ex); + if (logger.isDebugEnabled()) { + logger.debug("Send for " + this.emitter + " failed: " + ex); } terminate(); this.emitter.completeWithError(ex); @@ -347,8 +347,8 @@ public void run() { Throwable ex = this.error; this.error = null; if (ex != null) { - if (logger.isTraceEnabled()) { - logger.trace("Publisher for " + this.emitter + " failed: " + ex); + if (logger.isDebugEnabled()) { + logger.debug("Publisher for " + this.emitter + " failed: " + ex); } this.emitter.completeWithError(ex); } From c50cb10964cc4cf98b4ceb5da8b7c181b67955e6 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 6 Jan 2025 10:55:52 +0000 Subject: [PATCH 030/108] Minor refactoring in WebAsyncManager There is no need to set the DeferredResult from WebAsyncManager in an onError notification because it is already done from the Lifecycle interceptor in DeferredResult. See gh-34192 --- .../web/context/request/async/WebAsyncManager.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java index e42cdb17cb0c..10e98d38d3b1 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java @@ -436,10 +436,7 @@ public void startDeferredResultProcessing( logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest)); } try { - if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) { - return; - } - deferredResult.setErrorResult(ex); + interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex); } catch (Throwable interceptorEx) { setConcurrentResultAndDispatch(interceptorEx); From 9eefdb80415768dc9fe41f410e320f1d8b30c187 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 3 Jan 2025 16:04:51 +0000 Subject: [PATCH 031/108] Synchronize in WebAsyncManager onError/onTimeout On connection loss, in a race between application thread and onError callback trying to set the DeferredResult and dispatch, the onError callback must not exit until dispatch completes. Currently, it may do so because the DeferredResult has checks to bypasses locking or even trying to dispatch if result is already set. Closes gh-34192 --- .../web/context/request/async/WebAsyncManager.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java index 10e98d38d3b1..721c81bf642b 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java @@ -425,6 +425,11 @@ public void startDeferredResultProcessing( } try { interceptorChain.triggerAfterTimeout(this.asyncWebRequest, deferredResult); + synchronized (WebAsyncManager.this) { + // If application thread set the DeferredResult first in a race, + // we must still not return until setConcurrentResultAndDispatch is done + return; + } } catch (Throwable ex) { setConcurrentResultAndDispatch(ex); @@ -437,6 +442,11 @@ public void startDeferredResultProcessing( } try { interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex); + synchronized (WebAsyncManager.this) { + // If application thread set the DeferredResult first in a race, + // we must still not return until setConcurrentResultAndDispatch is done + return; + } } catch (Throwable interceptorEx) { setConcurrentResultAndDispatch(interceptorEx); From e1b06ccfaa09076b1cf785a5fa5ecb6e5cba4a12 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 6 Jan 2025 12:13:29 +0000 Subject: [PATCH 032/108] Improve logging in ReactiveTypeHandler See gh-34188 --- .../annotation/ReactiveTypeHandler.java | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandler.java index 6014c0a8169b..540fa3ec2080 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandler.java @@ -337,7 +337,14 @@ public void run() { logger.debug("Send for " + this.emitter + " failed: " + ex); } terminate(); - this.emitter.completeWithError(ex); + try { + this.emitter.completeWithError(ex); + } + catch (Exception ex2) { + if (logger.isDebugEnabled()) { + logger.debug("Failure from emitter completeWithError: " + ex2); + } + } return; } } @@ -350,13 +357,27 @@ public void run() { if (logger.isDebugEnabled()) { logger.debug("Publisher for " + this.emitter + " failed: " + ex); } - this.emitter.completeWithError(ex); + try { + this.emitter.completeWithError(ex); + } + catch (Exception ex2) { + if (logger.isDebugEnabled()) { + logger.debug("Failure from emitter completeWithError: " + ex2); + } + } } else { if (logger.isTraceEnabled()) { logger.trace("Publisher for " + this.emitter + " completed"); } - this.emitter.complete(); + try { + this.emitter.complete(); + } + catch (Exception ex2) { + if (logger.isDebugEnabled()) { + logger.debug("Failure from emitter complete: " + ex2); + } + } } return; } From cf662368a581284e3e61daad038e51791f88ed14 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 13 Jan 2025 19:03:43 +0100 Subject: [PATCH 033/108] Fix Wrong parentId tracking in JFR application startup This commit fixes the tracking of the main event parentId for the Java Flight Recorder implementation variant. Fixes gh-34254 --- .../core/metrics/jfr/FlightRecorderApplicationStartup.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderApplicationStartup.java b/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderApplicationStartup.java index 60f739a0d8ff..b30a627bc67d 100644 --- a/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderApplicationStartup.java +++ b/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderApplicationStartup.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -51,10 +51,11 @@ public FlightRecorderApplicationStartup() { @Override public StartupStep start(String name) { + Long parentId = this.currentSteps.getFirst(); long sequenceId = this.currentSequenceId.incrementAndGet(); this.currentSteps.offerFirst(sequenceId); return new FlightRecorderStartupStep(sequenceId, name, - this.currentSteps.getFirst(), committedStep -> this.currentSteps.removeFirstOccurrence(sequenceId)); + parentId, committedStep -> this.currentSteps.removeFirstOccurrence(sequenceId)); } } From bb5be2119c332edf2ea7afc56771aab408508071 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 20 Jan 2025 15:01:16 +0000 Subject: [PATCH 034/108] Restore exception phrase in DisconnectedClientHelper Effectively revert 203fa7, and add implementation comment and tests. See gh-34264 --- .../web/util/DisconnectedClientHelper.java | 8 ++- .../util/DisconnectedClientHelperTests.java | 69 +++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 spring-web/src/test/java/org/springframework/web/util/DisconnectedClientHelperTests.java diff --git a/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java b/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java index b2b16f135adb..9d49f1b1a30c 100644 --- a/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java +++ b/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java @@ -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"); * you may not use this file except in compliance with the License. @@ -36,8 +36,11 @@ */ public class DisconnectedClientHelper { + // Look for server response connection issues (aborted), not onward connections + // to other servers (500 errors). + private static final Set EXCEPTION_PHRASES = - Set.of("broken pipe", "connection reset"); + Set.of("broken pipe", "connection reset by peer"); private static final Set EXCEPTION_TYPE_NAMES = Set.of("AbortedException", "ClientAbortException", @@ -79,7 +82,6 @@ else if (logger.isDebugEnabled()) { *
  • ClientAbortException or EOFException for Tomcat *
  • EofException for Jetty *
  • IOException "Broken pipe" or "connection reset by peer" - *
  • SocketException "Connection reset" * */ public static boolean isClientDisconnectedException(Throwable ex) { diff --git a/spring-web/src/test/java/org/springframework/web/util/DisconnectedClientHelperTests.java b/spring-web/src/test/java/org/springframework/web/util/DisconnectedClientHelperTests.java new file mode 100644 index 000000000000..02fbd9a5ab85 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/util/DisconnectedClientHelperTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2025 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.util; + +import java.io.EOFException; +import java.io.IOException; +import java.util.List; + +import org.apache.catalina.connector.ClientAbortException; +import org.eclipse.jetty.io.EofException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.netty.channel.AbortedException; + +import org.springframework.web.context.request.async.AsyncRequestNotUsableException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link DisconnectedClientHelper}. + * @author Rossen Stoyanchev + */ +public class DisconnectedClientHelperTests { + + @ParameterizedTest + @ValueSource(strings = {"broKen pipe", "connection reset By peer"}) + void exceptionPhrases(String phrase) { + Exception ex = new IOException(phrase); + assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isTrue(); + + ex = new IOException(ex); + assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isTrue(); + } + + @Test + void connectionResetExcluded() { + Exception ex = new IOException("connection reset"); + assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isFalse(); + } + + @ParameterizedTest + @MethodSource("disconnectedExceptions") + void name(Exception ex) { + assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isTrue(); + } + + static List disconnectedExceptions() { + return List.of( + new AbortedException(""), new ClientAbortException(""), + new EOFException(), new EofException(), new AsyncRequestNotUsableException("")); + } + +} From b0a8a3ec5fa262f43cd4cbd219a27d3c9b28fed5 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 20 Jan 2025 16:26:02 +0000 Subject: [PATCH 035/108] Enhance DisconnectedClientHelper exception type checks We now look for the target exception types in cause chain as well, but return false if we encounter a RestClient or WebClient exception in the chain. Closes gh-34264 --- .../web/util/DisconnectedClientHelper.java | 49 ++++++++++++++++--- .../util/DisconnectedClientHelperTests.java | 24 +++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java b/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java index 9d49f1b1a30c..a62f6312bbb5 100644 --- a/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java +++ b/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java @@ -16,6 +16,7 @@ package org.springframework.web.util; +import java.util.HashSet; import java.util.Locale; import java.util.Set; @@ -24,21 +25,20 @@ import org.springframework.core.NestedExceptionUtils; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** - * Utility methods to assist with identifying and logging exceptions that indicate - * the client has gone away. Such exceptions fill logs with unnecessary stack - * traces. The utility methods help to log a single line message at DEBUG level, - * and a full stacktrace at TRACE level. + * Utility methods to assist with identifying and logging exceptions that + * indicate the server response connection is lost, for example because the + * client has gone away. This class helps to identify such exceptions and + * minimize logging to a single line at DEBUG level, while making the full + * error stacktrace at TRACE level. * * @author Rossen Stoyanchev * @since 6.1 */ public class DisconnectedClientHelper { - // Look for server response connection issues (aborted), not onward connections - // to other servers (500 errors). - private static final Set EXCEPTION_PHRASES = Set.of("broken pipe", "connection reset by peer"); @@ -46,6 +46,22 @@ public class DisconnectedClientHelper { Set.of("AbortedException", "ClientAbortException", "EOFException", "EofException", "AsyncRequestNotUsableException"); + private static final Set> CLIENT_EXCEPTION_TYPES = new HashSet<>(2); + + static { + try { + ClassLoader classLoader = DisconnectedClientHelper.class.getClassLoader(); + CLIENT_EXCEPTION_TYPES.add(ClassUtils.forName( + "org.springframework.web.client.RestClientException", classLoader)); + CLIENT_EXCEPTION_TYPES.add(ClassUtils.forName( + "org.springframework.web.reactive.function.client.WebClientException", classLoader)); + } + catch (ClassNotFoundException ex) { + // ignore + } + } + + private final Log logger; @@ -85,6 +101,22 @@ else if (logger.isDebugEnabled()) { * */ public static boolean isClientDisconnectedException(Throwable ex) { + Throwable currentEx = ex; + Throwable lastEx = null; + while (currentEx != null && currentEx != lastEx) { + // Ignore onward connection issues to other servers (500 error) + for (Class exceptionType : CLIENT_EXCEPTION_TYPES) { + if (exceptionType.isInstance(currentEx)) { + return false; + } + } + if (EXCEPTION_TYPE_NAMES.contains(currentEx.getClass().getSimpleName())) { + return true; + } + lastEx = currentEx; + currentEx = currentEx.getCause(); + } + String message = NestedExceptionUtils.getMostSpecificCause(ex).getMessage(); if (message != null) { String text = message.toLowerCase(Locale.ROOT); @@ -94,7 +126,8 @@ public static boolean isClientDisconnectedException(Throwable ex) { } } } - return EXCEPTION_TYPE_NAMES.contains(ex.getClass().getSimpleName()); + + return false; } } diff --git a/spring-web/src/test/java/org/springframework/web/util/DisconnectedClientHelperTests.java b/spring-web/src/test/java/org/springframework/web/util/DisconnectedClientHelperTests.java index 02fbd9a5ab85..296a19209272 100644 --- a/spring-web/src/test/java/org/springframework/web/util/DisconnectedClientHelperTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/DisconnectedClientHelperTests.java @@ -28,7 +28,10 @@ import org.junit.jupiter.params.provider.ValueSource; import reactor.netty.channel.AbortedException; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.client.ResourceAccessException; import org.springframework.web.context.request.async.AsyncRequestNotUsableException; +import org.springframework.web.testfixture.http.MockHttpInputMessage; import static org.assertj.core.api.Assertions.assertThat; @@ -66,4 +69,25 @@ static List disconnectedExceptions() { new EOFException(), new EofException(), new AsyncRequestNotUsableException("")); } + @Test // gh-33064 + void nestedDisconnectedException() { + Exception ex = new HttpMessageNotReadableException( + "I/O error while reading input message", new ClientAbortException(), + new MockHttpInputMessage(new byte[0])); + + assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isTrue(); + } + + @Test // gh-34264 + void onwardClientDisconnectedExceptionPhrase() { + Exception ex = new ResourceAccessException("I/O error", new EOFException("Connection reset by peer")); + assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isFalse(); + } + + @Test + void onwardClientDisconnectedExceptionType() { + Exception ex = new ResourceAccessException("I/O error", new EOFException()); + assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isFalse(); + } + } From c333946b0b9eacc14e77b55caa1b0d6ea8994488 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:33:46 +0100 Subject: [PATCH 036/108] Restore property binding support for a Map that implements Iterable The changes in commit c20a2e4763 introduced a regression with regard to binding to a Map property when the Map also happens to implement Iterable. Although that is perhaps not a very common scenario, this commit reorders the if-blocks in AbstractNestablePropertyAccessor's getPropertyValue(PropertyTokenHolder) method so that a Map is considered before an Iterable, thereby allowing an Iterable-Map to be accessed as a Map. See gh-907 Closes gh-34332 (cherry picked from commit b9e43d05bd64ed25c2488ad1fd325671b7c2d42d) --- .../AbstractNestablePropertyAccessor.java | 18 +++++------ .../beans/AbstractPropertyAccessorTests.java | 29 ++++++++++++++++- .../testfixture/beans/IndexedTestBean.java | 31 ++++++++++++++++++- 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java index 04fc76399ad1..64e3a1e57c2d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java @@ -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"); * you may not use this file except in compliance with the License. @@ -658,6 +658,14 @@ else if (value instanceof List list) { growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1); value = list.get(index); } + else if (value instanceof Map map) { + Class mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0); + // IMPORTANT: Do not pass full property name in here - property editors + // must not kick in for map keys but rather only for map values. + TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); + Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor); + value = map.get(convertedMapKey); + } else if (value instanceof Iterable iterable) { // Apply index to Iterator in case of a Set/Collection/Iterable. int index = Integer.parseInt(key); @@ -685,14 +693,6 @@ else if (value instanceof Iterable iterable) { currIndex + ", accessed using property path '" + propertyName + "'"); } } - else if (value instanceof Map map) { - Class mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0); - // IMPORTANT: Do not pass full property name in here - property editors - // must not kick in for map keys but rather only for map values. - TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); - Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor); - value = map.get(convertedMapKey); - } else { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "Property referenced in indexed property path '" + propertyName + diff --git a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java index dd50e8c117c1..f15154edb598 100644 --- a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -139,6 +139,7 @@ void isReadableWritableForIndexedProperties() { assertThat(accessor.isReadableProperty("list")).isTrue(); assertThat(accessor.isReadableProperty("set")).isTrue(); assertThat(accessor.isReadableProperty("map")).isTrue(); + assertThat(accessor.isReadableProperty("iterableMap")).isTrue(); assertThat(accessor.isReadableProperty("myTestBeans")).isTrue(); assertThat(accessor.isReadableProperty("xxx")).isFalse(); @@ -146,6 +147,7 @@ void isReadableWritableForIndexedProperties() { assertThat(accessor.isWritableProperty("list")).isTrue(); assertThat(accessor.isWritableProperty("set")).isTrue(); assertThat(accessor.isWritableProperty("map")).isTrue(); + assertThat(accessor.isWritableProperty("iterableMap")).isTrue(); assertThat(accessor.isWritableProperty("myTestBeans")).isTrue(); assertThat(accessor.isWritableProperty("xxx")).isFalse(); @@ -161,6 +163,14 @@ void isReadableWritableForIndexedProperties() { assertThat(accessor.isReadableProperty("map[key4][0].name")).isTrue(); assertThat(accessor.isReadableProperty("map[key4][1]")).isTrue(); assertThat(accessor.isReadableProperty("map[key4][1].name")).isTrue(); + assertThat(accessor.isReadableProperty("map[key999]")).isTrue(); + assertThat(accessor.isReadableProperty("iterableMap[key1]")).isTrue(); + assertThat(accessor.isReadableProperty("iterableMap[key1].name")).isTrue(); + assertThat(accessor.isReadableProperty("iterableMap[key2][0]")).isTrue(); + assertThat(accessor.isReadableProperty("iterableMap[key2][0].name")).isTrue(); + assertThat(accessor.isReadableProperty("iterableMap[key2][1]")).isTrue(); + assertThat(accessor.isReadableProperty("iterableMap[key2][1].name")).isTrue(); + assertThat(accessor.isReadableProperty("iterableMap[key999]")).isTrue(); assertThat(accessor.isReadableProperty("myTestBeans[0]")).isTrue(); assertThat(accessor.isReadableProperty("myTestBeans[1]")).isFalse(); assertThat(accessor.isReadableProperty("array[key1]")).isFalse(); @@ -177,6 +187,14 @@ void isReadableWritableForIndexedProperties() { assertThat(accessor.isWritableProperty("map[key4][0].name")).isTrue(); assertThat(accessor.isWritableProperty("map[key4][1]")).isTrue(); assertThat(accessor.isWritableProperty("map[key4][1].name")).isTrue(); + assertThat(accessor.isWritableProperty("map[key999]")).isTrue(); + assertThat(accessor.isWritableProperty("iterableMap[key1]")).isTrue(); + assertThat(accessor.isWritableProperty("iterableMap[key1].name")).isTrue(); + assertThat(accessor.isWritableProperty("iterableMap[key2][0]")).isTrue(); + assertThat(accessor.isWritableProperty("iterableMap[key2][0].name")).isTrue(); + assertThat(accessor.isWritableProperty("iterableMap[key2][1]")).isTrue(); + assertThat(accessor.isWritableProperty("iterableMap[key2][1].name")).isTrue(); + assertThat(accessor.isWritableProperty("iterableMap[key999]")).isTrue(); assertThat(accessor.isReadableProperty("myTestBeans[0]")).isTrue(); assertThat(accessor.isReadableProperty("myTestBeans[1]")).isFalse(); assertThat(accessor.isWritableProperty("array[key1]")).isFalse(); @@ -1394,6 +1412,9 @@ void getAndSetIndexedProperties() { assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name8"); assertThat(accessor.getPropertyValue("map['key5[foo]'].name")).isEqualTo("name8"); assertThat(accessor.getPropertyValue("map[\"key5[foo]\"].name")).isEqualTo("name8"); + assertThat(accessor.getPropertyValue("iterableMap[key1].name")).isEqualTo("nameC"); + assertThat(accessor.getPropertyValue("iterableMap[key2][0].name")).isEqualTo("nameA"); + assertThat(accessor.getPropertyValue("iterableMap[key2][1].name")).isEqualTo("nameB"); assertThat(accessor.getPropertyValue("myTestBeans[0].name")).isEqualTo("nameZ"); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -1408,6 +1429,9 @@ void getAndSetIndexedProperties() { pvs.add("map[key4][0].name", "nameA"); pvs.add("map[key4][1].name", "nameB"); pvs.add("map[key5[foo]].name", "name10"); + pvs.add("iterableMap[key1].name", "newName1"); + pvs.add("iterableMap[key2][0].name", "newName2A"); + pvs.add("iterableMap[key2][1].name", "newName2B"); pvs.add("myTestBeans[0].name", "nameZZ"); accessor.setPropertyValues(pvs); assertThat(tb0.getName()).isEqualTo("name5"); @@ -1427,6 +1451,9 @@ void getAndSetIndexedProperties() { assertThat(accessor.getPropertyValue("map[key4][0].name")).isEqualTo("nameA"); assertThat(accessor.getPropertyValue("map[key4][1].name")).isEqualTo("nameB"); assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name10"); + assertThat(accessor.getPropertyValue("iterableMap[key1].name")).isEqualTo("newName1"); + assertThat(accessor.getPropertyValue("iterableMap[key2][0].name")).isEqualTo("newName2A"); + assertThat(accessor.getPropertyValue("iterableMap[key2][1].name")).isEqualTo("newName2B"); assertThat(accessor.getPropertyValue("myTestBeans[0].name")).isEqualTo("nameZZ"); } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java index 54d32af54535..39026aec6f95 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java @@ -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"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -49,6 +50,8 @@ public class IndexedTestBean { private SortedMap sortedMap; + private IterableMap iterableMap; + private MyTestBeans myTestBeans; @@ -73,6 +76,9 @@ public void populate() { TestBean tb6 = new TestBean("name6", 0); TestBean tb7 = new TestBean("name7", 0); TestBean tb8 = new TestBean("name8", 0); + TestBean tbA = new TestBean("nameA", 0); + TestBean tbB = new TestBean("nameB", 0); + TestBean tbC = new TestBean("nameC", 0); TestBean tbX = new TestBean("nameX", 0); TestBean tbY = new TestBean("nameY", 0); TestBean tbZ = new TestBean("nameZ", 0); @@ -88,6 +94,12 @@ public void populate() { this.map.put("key2", tb5); this.map.put("key.3", tb5); List list = new ArrayList(); + list.add(tbA); + list.add(tbB); + this.iterableMap = new IterableMap<>(); + this.iterableMap.put("key1", tbC); + this.iterableMap.put("key2", list); + list = new ArrayList(); list.add(tbX); list.add(tbY); this.map.put("key4", list); @@ -152,6 +164,14 @@ public void setSortedMap(SortedMap sortedMap) { this.sortedMap = sortedMap; } + public IterableMap getIterableMap() { + return this.iterableMap; + } + + public void setIterableMap(IterableMap iterableMap) { + this.iterableMap = iterableMap; + } + public MyTestBeans getMyTestBeans() { return myTestBeans; } @@ -161,6 +181,15 @@ public void setMyTestBeans(MyTestBeans myTestBeans) { } + @SuppressWarnings("serial") + public static class IterableMap extends LinkedHashMap implements Iterable { + + @Override + public Iterator iterator() { + return values().iterator(); + } + } + public static class MyTestBeans implements Iterable { private final Collection testBeans; From a4fc68b8e89a6cb1ef841b57353bc710d4b8d2e4 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 3 Feb 2025 15:23:51 +0100 Subject: [PATCH 037/108] Explicitly set custom ClassLoader on CGLIB Enhancer Closes gh-34274 (cherry picked from commit 1b18928bf057967509c03937c3832be3a9d462c7) --- .../context/annotation/ConfigurationClassEnhancer.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index 21392d5fc7cc..fb5b3ceb7684 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -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"); * you may not use this file except in compliance with the License. @@ -129,6 +129,9 @@ public Class enhance(Class configClass, @Nullable ClassLoader classLoader) */ private Enhancer newEnhancer(Class configSuperClass, @Nullable ClassLoader classLoader) { Enhancer enhancer = new Enhancer(); + if (classLoader != null) { + enhancer.setClassLoader(classLoader); + } enhancer.setSuperclass(configSuperClass); enhancer.setInterfaces(new Class[] {EnhancedConfiguration.class}); enhancer.setUseFactory(false); From ebd80bdd6c184fd29eeb36bdd227bed208e76bcc Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 29 Jan 2025 15:23:54 +0100 Subject: [PATCH 038/108] Delete failing Freemarker test This test was already ignored as of Java 21 because of a Java behavior change, and now it started failing as of 17.0.14. This commit removes the test entirely. --- .../result/view/freemarker/FreeMarkerMacroTests.java | 8 -------- .../web/reactive/result/view/freemarker/test-macro.ftl | 3 --- .../web/servlet/view/freemarker/FreeMarkerMacroTests.java | 8 -------- .../springframework/web/servlet/view/freemarker/test.ftl | 3 --- 4 files changed, 22 deletions(-) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerMacroTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerMacroTests.java index 8a647bdb1e13..10084f9a100d 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerMacroTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerMacroTests.java @@ -28,8 +28,6 @@ import freemarker.template.Configuration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledForJreRange; -import org.junit.jupiter.api.condition.JRE; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -125,12 +123,6 @@ private void testMacroOutput(String name, String... contents) throws Exception { } - @Test - @DisabledForJreRange(min = JRE.JAVA_21) - public void age() throws Exception { - testMacroOutput("AGE", "99"); - } - @Test void message() throws Exception { testMacroOutput("MESSAGE", "Howdy Mundo"); diff --git a/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/freemarker/test-macro.ftl b/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/freemarker/test-macro.ftl index 168c4c6aab37..4ce3dfdfec4d 100644 --- a/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/freemarker/test-macro.ftl +++ b/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/freemarker/test-macro.ftl @@ -6,9 +6,6 @@ test template for FreeMarker macro support NAME ${command.name} -AGE -${command.age} - MESSAGE <@spring.message "hello"/> <@spring.message "world"/> diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerMacroTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerMacroTests.java index 79bac00e11b9..5d61c927b3c0 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerMacroTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerMacroTests.java @@ -31,8 +31,6 @@ import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledForJreRange; -import org.junit.jupiter.api.condition.JRE; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.io.ClassPathResource; @@ -146,12 +144,6 @@ void testName() throws Exception { assertThat(getMacroOutput("NAME")).isEqualTo("Darren"); } - @Test - @DisabledForJreRange(min = JRE.JAVA_21) - public void testAge() throws Exception { - assertThat(getMacroOutput("AGE")).isEqualTo("99"); - } - @Test void testMessage() throws Exception { assertThat(getMacroOutput("MESSAGE")).isEqualTo("Howdy Mundo"); diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/freemarker/test.ftl b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/freemarker/test.ftl index b6fb4caf4ea6..ffce5d5d480e 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/freemarker/test.ftl +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/freemarker/test.ftl @@ -6,9 +6,6 @@ test template for FreeMarker macro test class NAME ${command.name} -AGE -${command.age} - MESSAGE <@spring.message "hello"/> <@spring.message "world"/> From 5b1a7c7f2164df85a9da2a153ecb3404c13e7f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 5 Feb 2025 11:50:35 +0100 Subject: [PATCH 039/108] Handle arbitrary JoinPoint argument index Closes gh-34369 --- .../aop/aspectj/AbstractAspectJAdvice.java | 22 +-- .../aspectj/AbstractAspectJAdviceTests.java | 135 ++++++++++++++++++ 2 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 spring-aop/src/test/java/org/springframework/aop/aspectj/AbstractAspectJAdviceTests.java diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java index bbe397880b10..f9e5e82896df 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java @@ -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"); * you may not use this file except in compliance with the License. @@ -276,14 +276,18 @@ public void setArgumentNamesFromStringArray(String... argumentNames) { } if (this.aspectJAdviceMethod.getParameterCount() == this.argumentNames.length + 1) { // May need to add implicit join point arg name... - Class firstArgType = this.aspectJAdviceMethod.getParameterTypes()[0]; - if (firstArgType == JoinPoint.class || - firstArgType == ProceedingJoinPoint.class || - firstArgType == JoinPoint.StaticPart.class) { - String[] oldNames = this.argumentNames; - this.argumentNames = new String[oldNames.length + 1]; - this.argumentNames[0] = "THIS_JOIN_POINT"; - System.arraycopy(oldNames, 0, this.argumentNames, 1, oldNames.length); + for (int i = 0; i < this.aspectJAdviceMethod.getParameterCount(); i++) { + Class argType = this.aspectJAdviceMethod.getParameterTypes()[i]; + if (argType == JoinPoint.class || + argType == ProceedingJoinPoint.class || + argType == JoinPoint.StaticPart.class) { + String[] oldNames = this.argumentNames; + this.argumentNames = new String[oldNames.length + 1]; + System.arraycopy(oldNames, 0, this.argumentNames, 0, i); + this.argumentNames[i] = "THIS_JOIN_POINT"; + System.arraycopy(oldNames, i, this.argumentNames, i + 1, oldNames.length - i); + break; + } } } } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AbstractAspectJAdviceTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AbstractAspectJAdviceTests.java new file mode 100644 index 000000000000..f6768b0e8440 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AbstractAspectJAdviceTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2025 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aop.aspectj; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.function.Consumer; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AbstractAspectJAdvice}. + * + * @author Joshua Chen + * @author Stephane Nicoll + */ +class AbstractAspectJAdviceTests { + + @Test + void setArgumentNamesFromStringArray_withoutJoinPointParameter() { + AbstractAspectJAdvice advice = getAspectJAdvice("methodWithNoJoinPoint"); + assertThat(advice).satisfies(hasArgumentNames("arg1", "arg2")); + } + + @Test + void setArgumentNamesFromStringArray_withJoinPointAsFirstParameter() { + AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsFirstParameter"); + assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2")); + } + + @Test + void setArgumentNamesFromStringArray_withJoinPointAsLastParameter() { + AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsLastParameter"); + assertThat(advice).satisfies(hasArgumentNames("arg1", "arg2", "THIS_JOIN_POINT")); + } + + @Test + void setArgumentNamesFromStringArray_withJoinPointAsMiddleParameter() { + AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsMiddleParameter"); + assertThat(advice).satisfies(hasArgumentNames("arg1", "THIS_JOIN_POINT", "arg2")); + } + + @Test + void setArgumentNamesFromStringArray_withProceedingJoinPoint() { + AbstractAspectJAdvice advice = getAspectJAdvice("methodWithProceedingJoinPoint"); + assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2")); + } + + @Test + void setArgumentNamesFromStringArray_withStaticPart() { + AbstractAspectJAdvice advice = getAspectJAdvice("methodWithStaticPart"); + assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2")); + } + + private Consumer hasArgumentNames(String... argumentNames) { + return advice -> assertThat(advice).extracting("argumentNames") + .asInstanceOf(InstanceOfAssertFactories.array(String[].class)) + .containsExactly(argumentNames); + } + + private AbstractAspectJAdvice getAspectJAdvice(final String methodName) { + AbstractAspectJAdvice advice = new TestAspectJAdvice(getMethod(methodName), + mock(AspectJExpressionPointcut.class), mock(AspectInstanceFactory.class)); + advice.setArgumentNamesFromStringArray("arg1", "arg2"); + return advice; + } + + private Method getMethod(final String methodName) { + return Arrays.stream(Sample.class.getDeclaredMethods()) + .filter(method -> method.getName().equals(methodName)).findFirst() + .orElseThrow(); + } + + @SuppressWarnings("serial") + public static class TestAspectJAdvice extends AbstractAspectJAdvice { + + public TestAspectJAdvice(Method aspectJAdviceMethod, AspectJExpressionPointcut pointcut, + AspectInstanceFactory aspectInstanceFactory) { + super(aspectJAdviceMethod, pointcut, aspectInstanceFactory); + } + + @Override + public boolean isBeforeAdvice() { + return false; + } + + @Override + public boolean isAfterAdvice() { + return false; + } + } + + @SuppressWarnings("unused") + static class Sample { + + void methodWithNoJoinPoint(String arg1, String arg2) { + } + + void methodWithJoinPointAsFirstParameter(JoinPoint joinPoint, String arg1, String arg2) { + } + + void methodWithJoinPointAsLastParameter(String arg1, String arg2, JoinPoint joinPoint) { + } + + void methodWithJoinPointAsMiddleParameter(String arg1, JoinPoint joinPoint, String arg2) { + } + + void methodWithProceedingJoinPoint(ProceedingJoinPoint joinPoint, String arg1, String arg2) { + } + + void methodWithStaticPart(JoinPoint.StaticPart staticPart, String arg1, String arg2) { + } + } + +} From b7a996a64b93e3a040f87e23dd7f749959706f07 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:39:29 +0100 Subject: [PATCH 040/108] =?UTF-8?q?Clarify=20component=20scanning=20of=20a?= =?UTF-8?q?bstract=20classes=20with=20@=E2=81=A0Lookup=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Due to changes in gh-19118, classes that contain @⁠Lookup methods are no longer required to be concrete classes for use with component scanning; however, the reference documentation still states that such classes must not be abstract. This commit therefore removes the outdated reference documentation and updates the corresponding Javadoc. See gh-19118 Closes gh-34367 (cherry picked from commit 819a7c86c194976622e0d035e1f5931aa9db4312) --- .../beans/dependencies/factory-method-injection.adoc | 7 ------- .../ClassPathScanningCandidateComponentProvider.java | 9 +++++---- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-method-injection.adoc b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-method-injection.adoc index 3738a55f8188..74d5ef1a902f 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-method-injection.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-method-injection.adoc @@ -120,8 +120,6 @@ dynamically generate a subclass that overrides the method. subclasses cannot be `final`, and the method to be overridden cannot be `final`, either. * Unit-testing a class that has an `abstract` method requires you to subclass the class yourself and to supply a stub implementation of the `abstract` method. -* Concrete methods are also necessary for component scanning, which requires concrete - classes to pick up. * A further key limitation is that lookup methods do not work with factory methods and in particular not with `@Bean` methods in configuration classes, since, in that case, the container is not in charge of creating the instance and therefore cannot create @@ -293,11 +291,6 @@ Kotlin:: ---- ====== -Note that you should typically declare such annotated lookup methods with a concrete -stub implementation, in order for them to be compatible with Spring's component -scanning rules where abstract classes get ignored by default. This limitation does not -apply to explicitly registered or explicitly imported bean classes. - [TIP] ==== Another way of accessing differently scoped target beans is an `ObjectFactory`/ diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java index daeb1cd833e1..cde1b3d8dbc2 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java @@ -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"); * you may not use this file except in compliance with the License. @@ -565,9 +565,10 @@ private boolean isConditionMatch(MetadataReader metadataReader) { } /** - * Determine whether the given bean definition qualifies as candidate. - *

    The default implementation checks whether the class is not an interface - * and not dependent on an enclosing class. + * Determine whether the given bean definition qualifies as a candidate component. + *

    The default implementation checks whether the class is not dependent on an + * enclosing class as well as whether the class is either concrete (and therefore + * not an interface) or has {@link Lookup @Lookup} methods. *

    Can be overridden in subclasses. * @param beanDefinition the bean definition to check * @return whether the bean definition qualifies as a candidate component From 65913c36552dc8cebf2d8862fab8a40d04c1a716 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 6 Feb 2025 18:27:07 +0100 Subject: [PATCH 041/108] Fix "Nth day of week" Quartz-style cron expressions Prior to this commit, `CronExpression` would support Quartz-style expressions with "Nth occurence of a dayOfWeek" semantics by using the `TemporalAdjusters.dayOfWeekInMonth` JDK support. This method will return the Nth occurence starting with the month of the given temporal, but in some cases will overflow to the next or previous month. This behavior is not expected for our cron expression support. This commit ensures that when an overflow happens (meaning, the resulting date is not in the same month as the input temporal), we should instead have another attempt at finding a valid month for this expression. Fixes gh-34377 --- .../scheduling/support/QuartzCronField.java | 14 +++++++++++--- .../scheduling/support/CronExpressionTests.java | 17 ++++++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java index 8c0873d15109..b49b49357c03 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java @@ -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"); * you may not use this file except in compliance with the License. @@ -317,8 +317,16 @@ private static TemporalAdjuster lastInMonth(DayOfWeek dayOfWeek) { private static TemporalAdjuster dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek) { TemporalAdjuster adjuster = TemporalAdjusters.dayOfWeekInMonth(ordinal, dayOfWeek); return temporal -> { - Temporal result = adjuster.adjustInto(temporal); - return rollbackToMidnight(temporal, result); + // TemporalAdjusters can overflow to a different month + // in this case, attempt the same adjustment with the next/previous month + for (int i = 0; i < 12; i++) { + Temporal result = adjuster.adjustInto(temporal); + if (result.get(ChronoField.MONTH_OF_YEAR) == temporal.get(ChronoField.MONTH_OF_YEAR)) { + return rollbackToMidnight(temporal, result); + } + temporal = result; + } + return null; }; } diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java index 57372204cb7f..3c74291dc23c 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** + * Tests for {@link CronExpression}. * @author Arjen Poutsma */ class CronExpressionTests { @@ -1092,6 +1093,20 @@ void quartz2ndFridayOfTheMonthDayName() { assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); } + @Test + void quartz5thMondayOfTheMonthDayName() { + CronExpression expression = CronExpression.parse("0 0 0 ? * MON#5"); + + LocalDateTime last = LocalDateTime.of(2025, 1, 1, 0, 0, 0); + + // first occurrence of 5 mondays in a month from last + LocalDateTime expected = LocalDateTime.of(2025, 3, 31, 0, 0, 0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(MONDAY); + } + @Test void quartzFifthWednesdayOfTheMonth() { CronExpression expression = CronExpression.parse("0 0 0 ? * 3#5"); From 78fe55f4d13c0ccd6626bf8eb8f80fa6b65c0061 Mon Sep 17 00:00:00 2001 From: Branden Clark Date: Tue, 28 Jan 2025 17:38:45 -0800 Subject: [PATCH 042/108] Check hasNext on sessionIds in UserDestinationResult Closes gh-34333 Signed-off-by: Branden Clark --- .../user/UserDestinationMessageHandler.java | 13 ++++------- .../simp/user/UserDestinationResult.java | 9 +++++--- .../UserDestinationMessageHandlerTests.java | 23 ++++++++++++++++++- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationMessageHandler.java index 20cd5e5e479b..86bf79c8e381 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationMessageHandler.java @@ -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"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; @@ -280,12 +279,10 @@ public MessageSendingOperations getMessagingTemplate() { return this.messagingTemplate; } - public void send(UserDestinationResult destinationResult, Message message) throws MessagingException { - Set sessionIds = destinationResult.getSessionIds(); - Iterator itr = (sessionIds != null ? sessionIds.iterator() : null); - - for (String target : destinationResult.getTargetDestinations()) { - String sessionId = (itr != null ? itr.next() : null); + public void send(UserDestinationResult result, Message message) throws MessagingException { + Iterator itr = result.getSessionIds().iterator(); + for (String target : result.getTargetDestinations()) { + String sessionId = (itr.hasNext() ? itr.next() : null); getTemplateToUse(sessionId).send(target, message); } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationResult.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationResult.java index 04da7a13f654..d665b27c9661 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationResult.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationResult.java @@ -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"); * you may not use this file except in compliance with the License. @@ -44,7 +44,11 @@ public class UserDestinationResult { private final Set sessionIds; - public UserDestinationResult(String sourceDestination, Set targetDestinations, + /** + * Main constructor. + */ + public UserDestinationResult( + String sourceDestination, Set targetDestinations, String subscribeDestination, @Nullable String user) { this(sourceDestination, targetDestinations, subscribeDestination, user, null); @@ -114,7 +118,6 @@ public String getUser() { /** * Return the session id for the targetDestination. */ - @Nullable public Set getSessionIds() { return this.sessionIds; } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/user/UserDestinationMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/user/UserDestinationMessageHandlerTests.java index e1ec61dd8905..cfcfc1d08d71 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/user/UserDestinationMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/user/UserDestinationMessageHandlerTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.messaging.simp.user; import java.nio.charset.StandardCharsets; +import java.util.Set; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -98,6 +99,26 @@ void handleMessage() { assertThat(accessor.getFirstNativeHeader(ORIGINAL_DESTINATION)).isEqualTo("/user/queue/foo"); } + @Test + @SuppressWarnings("rawtypes") + void handleMessageWithoutSessionIds() { + UserDestinationResolver resolver = mock(); + Message message = createWith(SimpMessageType.MESSAGE, "joe", null, "/user/joe/queue/foo"); + UserDestinationResult result = new UserDestinationResult("/queue/foo-user123", Set.of("/queue/foo-user123"), "/user/queue/foo", "joe"); + given(resolver.resolveDestination(message)).willReturn(result); + + given(this.brokerChannel.send(Mockito.any(Message.class))).willReturn(true); + UserDestinationMessageHandler handler = new UserDestinationMessageHandler(new StubMessageChannel(), this.brokerChannel, resolver); + handler.handleMessage(message); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Message.class); + Mockito.verify(this.brokerChannel).send(captor.capture()); + + SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.wrap(captor.getValue()); + assertThat(accessor.getDestination()).isEqualTo("/queue/foo-user123"); + assertThat(accessor.getFirstNativeHeader(ORIGINAL_DESTINATION)).isEqualTo("/user/queue/foo"); + } + @Test @SuppressWarnings("rawtypes") void handleMessageWithoutActiveSession() { From a5b9e667e32cb23d279397dcd265305f0fc4207c Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:17:40 +0100 Subject: [PATCH 043/108] Polishing (cherry picked from commit 2fcae65853fc1620e92493079668c789b433b066) --- .../annotation/AnnotationBeanNameGeneratorTests.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java index 884bc07d7869..cfbaf501c4bc 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java @@ -210,7 +210,7 @@ static class ComponentWithMultipleConflictingNames { @Retention(RetentionPolicy.RUNTIME) @Component @interface ConventionBasedComponent1 { - // This intentionally convention-based. Please do not add @AliasFor. + // This is intentionally convention-based. Please do not add @AliasFor. // See gh-31093. String value() default ""; } @@ -218,7 +218,7 @@ static class ComponentWithMultipleConflictingNames { @Retention(RetentionPolicy.RUNTIME) @Component @interface ConventionBasedComponent2 { - // This intentionally convention-based. Please do not add @AliasFor. + // This is intentionally convention-based. Please do not add @AliasFor. // See gh-31093. String value() default ""; } @@ -260,7 +260,7 @@ private static class ComponentFromNonStringMeta { @Target(ElementType.TYPE) @Controller @interface TestRestController { - // This intentionally convention-based. Please do not add @AliasFor. + // This is intentionally convention-based. Please do not add @AliasFor. // See gh-31093. String value() default ""; } From a68b7289f09c11ece02d665a9874427c5b0d6dc8 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 8 Feb 2025 15:31:40 +0100 Subject: [PATCH 044/108] =?UTF-8?q?Improve=20warning=20for=20unexpected=20?= =?UTF-8?q?use=20of=20value=20attribute=20as=20@=E2=81=A0Component=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this commit, if a String 'value' attribute of an annotation was annotated with @⁠AliasFor and explicitly configured to alias an attribute other than @⁠Component.value, the value was still used as the @⁠Component name, but the warning message that was logged stated that the 'value' attribute should be annotated with @⁠AliasFor(annotation=Component.class). However, it is not possible to annotate an annotation attribute twice with @⁠AliasFor. To address that, this commit revises the logic in AnnotationBeanNameGenerator so that it issues a log message similar to the following in such scenarios. WARN o.s.c.a.AnnotationBeanNameGenerator - Although the 'value' attribute in @⁠example.MyStereotype declares @⁠AliasFor for an attribute other than @⁠Component's 'value' attribute, the value is still used as the @⁠Component name based on convention. As of Spring Framework 7.0, such a 'value' attribute will no longer be used as the @⁠Component name. See gh-34346 Closes gh-34317 (cherry picked from commit 17a94fb110659b5986e3fae69479bcad0a47dd71) --- .../AnnotationBeanNameGenerator.java | 41 +++++++--- .../AnnotationBeanNameGeneratorTests.java | 74 ++++++++++++++++++- 2 files changed, 104 insertions(+), 11 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java index 4f0f8a7e62b5..c3f786384fd8 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java @@ -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"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.context.annotation; import java.lang.annotation.Annotation; +import java.lang.reflect.Method; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; @@ -33,6 +34,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation.Adapt; @@ -41,6 +43,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** @@ -147,16 +150,26 @@ protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotat Set metaAnnotationTypes = this.metaAnnotationTypesCache.computeIfAbsent(annotationType, key -> getMetaAnnotationTypes(mergedAnnotation)); if (isStereotypeWithNameValue(annotationType, metaAnnotationTypes, attributes)) { - Object value = attributes.get("value"); + Object value = attributes.get(MergedAnnotation.VALUE); if (value instanceof String currentName && !currentName.isBlank()) { if (conventionBasedStereotypeCheckCache.add(annotationType) && metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) && logger.isWarnEnabled()) { - logger.warn(""" - Support for convention-based stereotype names is deprecated and will \ - be removed in a future version of the framework. Please annotate the \ - 'value' attribute in @%s with @AliasFor(annotation=Component.class) \ - to declare an explicit alias for @Component's 'value' attribute.""" - .formatted(annotationType)); + if (hasExplicitlyAliasedValueAttribute(mergedAnnotation.getType())) { + logger.warn(""" + Although the 'value' attribute in @%s declares @AliasFor for an attribute \ + other than @Component's 'value' attribute, the value is still used as the \ + @Component name based on convention. As of Spring Framework 7.0, such a \ + 'value' attribute will no longer be used as the @Component name.""" + .formatted(annotationType)); + } + else { + logger.warn(""" + Support for convention-based @Component names is deprecated and will \ + be removed in a future version of the framework. Please annotate the \ + 'value' attribute in @%s with @AliasFor(annotation=Component.class) \ + to declare an explicit alias for @Component's 'value' attribute.""" + .formatted(annotationType)); + } } if (beanName != null && !currentName.equals(beanName)) { throw new IllegalStateException("Stereotype annotations suggest inconsistent " + @@ -224,7 +237,7 @@ protected boolean isStereotypeWithNameValue(String annotationType, annotationType.equals("jakarta.inject.Named") || annotationType.equals("javax.inject.Named"); - return (isStereotype && attributes.containsKey("value")); + return (isStereotype && attributes.containsKey(MergedAnnotation.VALUE)); } /** @@ -255,4 +268,14 @@ protected String buildDefaultBeanName(BeanDefinition definition) { return StringUtils.uncapitalizeAsProperty(shortClassName); } + /** + * Determine if the supplied annotation type declares a {@code value()} attribute + * with an explicit alias configured via {@link AliasFor @AliasFor}. + * @since 6.2.3 + */ + private static boolean hasExplicitlyAliasedValueAttribute(Class annotationType) { + Method valueAttribute = ReflectionUtils.findMethod(annotationType, MergedAnnotation.VALUE); + return (valueAttribute != null && valueAttribute.isAnnotationPresent(AliasFor.class)); + } + } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java index cfbaf501c4bc..574038a83b3d 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -168,6 +168,25 @@ void generateBeanNameFromSubStereotypeAnnotationWithStringArrayValueAndExplicitC assertGeneratedName(RestControllerAdviceClass.class, "myRestControllerAdvice"); } + @Test // gh-34317 + void generateBeanNameFromStereotypeAnnotationWithStringValueAsExplicitAliasForMetaAnnotationOtherThanComponent() { + // As of Spring Framework 6.2, "enigma" is incorrectly used as the @Component name. + // As of Spring Framework 7.0, the generated name will be "annotationBeanNameGeneratorTests.StereotypeWithoutExplicitName". + assertGeneratedName(StereotypeWithoutExplicitName.class, "enigma"); + } + + @Test // gh-34317 + void generateBeanNameFromStereotypeAnnotationWithStringValueAndExplicitAliasForComponentNameWithBlankName() { + // As of Spring Framework 6.2, "enigma" is incorrectly used as the @Component name. + // As of Spring Framework 7.0, the generated name will be "annotationBeanNameGeneratorTests.StereotypeWithGeneratedName". + assertGeneratedName(StereotypeWithGeneratedName.class, "enigma"); + } + + @Test // gh-34317 + void generateBeanNameFromStereotypeAnnotationWithStringValueAndExplicitAliasForComponentName() { + assertGeneratedName(StereotypeWithExplicitName.class, "explicitName"); + } + private void assertGeneratedName(Class clazz, String expectedName) { BeanDefinition bd = annotatedBeanDef(clazz); @@ -319,7 +338,6 @@ static class ComposedControllerAnnotationWithStringValue { String[] basePackages() default {}; } - @TestControllerAdvice(basePackages = "com.example", name = "myControllerAdvice") static class ControllerAdviceClass { } @@ -328,4 +346,56 @@ static class ControllerAdviceClass { static class RestControllerAdviceClass { } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.ANNOTATION_TYPE) + @interface MetaAnnotationWithStringAttribute { + + String attribute() default ""; + } + + /** + * Custom stereotype annotation which has a {@code String value} attribute that + * is explicitly declared as an alias for an attribute in a meta-annotation + * other than {@link Component @Component}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Component + @MetaAnnotationWithStringAttribute + @interface MyStereotype { + + @AliasFor(annotation = MetaAnnotationWithStringAttribute.class, attribute = "attribute") + String value() default ""; + } + + @MyStereotype("enigma") + static class StereotypeWithoutExplicitName { + } + + /** + * Custom stereotype annotation which is identical to {@link MyStereotype @MyStereotype} + * except that it has a {@link #name} attribute that is an explicit alias for + * {@link Component#value}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Component + @MetaAnnotationWithStringAttribute + @interface MyNamedStereotype { + + @AliasFor(annotation = MetaAnnotationWithStringAttribute.class, attribute = "attribute") + String value() default ""; + + @AliasFor(annotation = Component.class, attribute = "value") + String name() default ""; + } + + @MyNamedStereotype(value = "enigma", name ="explicitName") + static class StereotypeWithExplicitName { + } + + @MyNamedStereotype(value = "enigma") + static class StereotypeWithGeneratedName { + } + } From c10596a1fd9347e985189572c0ba6112e5835491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 11 Feb 2025 11:43:04 +0100 Subject: [PATCH 045/108] Upgrade to RSocket 1.1.5 Closes gh-34405 --- 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 805eb2b92c16..00cf7ac1b492 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -12,7 +12,7 @@ dependencies { api(platform("io.netty:netty-bom:4.1.115.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) api(platform("io.projectreactor:reactor-bom:2023.0.13")) - api(platform("io.rsocket:rsocket-bom:1.1.4")) + api(platform("io.rsocket:rsocket-bom:1.1.5")) api(platform("org.apache.groovy:groovy-bom:4.0.24")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) api(platform("org.assertj:assertj-bom:3.26.3")) From 5e52baee866a4ed744349fcabc7eed9d201a86c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 11 Feb 2025 11:44:35 +0100 Subject: [PATCH 046/108] Upgrade to Reactor 2023.0.15 Closes gh-34406 --- 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 00cf7ac1b492..4f89672a83dd 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -11,7 +11,7 @@ dependencies { api(platform("io.micrometer:micrometer-bom:1.12.12")) api(platform("io.netty:netty-bom:4.1.115.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2023.0.13")) + api(platform("io.projectreactor:reactor-bom:2023.0.15")) api(platform("io.rsocket:rsocket-bom:1.1.5")) api(platform("org.apache.groovy:groovy-bom:4.0.24")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) From 5b928f47a8f6fbfbb600d2ed746b9ef05648e5b0 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:33:23 +0100 Subject: [PATCH 047/108] Finish incomplete sentences (cherry picked from commit 9d3374b28df667f6e1e0af51f4f74f9448bed4e5) --- .../test/context/TestContextManager.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java index 6754b2210339..762c698b6a81 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java @@ -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"); * you may not use this file except in compliance with the License. @@ -389,8 +389,8 @@ public void beforeTestExecution(Object testInstance, Method testMethod) throws E * have executed, the first caught exception will be rethrown with any * subsequent exceptions {@linkplain Throwable#addSuppressed suppressed} in * the first exception. - *

    Note that registered listeners will be executed in the opposite - * order in which they were registered. + *

    Note that listeners will be executed in the opposite order in which they + * were registered. * @param testInstance the current test instance * @param testMethod the test method which has just been executed on the * test instance @@ -459,7 +459,8 @@ public void afterTestExecution(Object testInstance, Method testMethod, @Nullable * have executed, the first caught exception will be rethrown with any * subsequent exceptions {@linkplain Throwable#addSuppressed suppressed} in * the first exception. - *

    Note that registered listeners will be executed in the opposite + *

    Note that listeners will be executed in the opposite order in which they + * were registered. * @param testInstance the current test instance * @param testMethod the test method which has just been executed on the * test instance @@ -517,7 +518,8 @@ public void afterTestMethod(Object testInstance, Method testMethod, @Nullable Th * have executed, the first caught exception will be rethrown with any * subsequent exceptions {@linkplain Throwable#addSuppressed suppressed} in * the first exception. - *

    Note that registered listeners will be executed in the opposite + *

    Note that listeners will be executed in the opposite order in which they + * were registered. * @throws Exception if a registered TestExecutionListener throws an exception * @since 3.0 * @see #getTestExecutionListeners() From 24fd0940bd0cfe701aad2f7ff6d8a97744ac9017 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 11 Feb 2025 22:10:02 +0100 Subject: [PATCH 048/108] Align with SmartClassLoader handling for AOP proxy classes Closes gh-34274 (cherry picked from commit f53da047175e21cb7619475b885ba1b4967e8d05) --- .../ConfigurationClassEnhancer.java | 27 +++- .../ConfigurationClassEnhancerTests.java | 153 +++++++++++++++++- 2 files changed, 169 insertions(+), 11 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index fb5b3ceb7684..590a685f5386 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -109,8 +109,16 @@ public Class enhance(Class configClass, @Nullable ClassLoader classLoader) } return configClass; } + try { - Class enhancedClass = createClass(newEnhancer(configClass, classLoader)); + // Use original ClassLoader if config class not locally loaded in overriding class loader + if (classLoader instanceof SmartClassLoader smartClassLoader && + classLoader != configClass.getClassLoader()) { + classLoader = smartClassLoader.getOriginalClassLoader(); + } + Enhancer enhancer = newEnhancer(configClass, classLoader); + boolean classLoaderMismatch = (classLoader != null && classLoader != configClass.getClassLoader()); + Class enhancedClass = createClass(enhancer, classLoaderMismatch); if (logger.isTraceEnabled()) { logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s", configClass.getName(), enhancedClass.getName())); @@ -155,8 +163,21 @@ private boolean isClassReloadable(Class configSuperClass, @Nullable ClassLoad * Uses enhancer to generate a subclass of superclass, * ensuring that callbacks are registered for the new subclass. */ - private Class createClass(Enhancer enhancer) { - Class subclass = enhancer.createClass(); + private Class createClass(Enhancer enhancer, boolean fallback) { + Class subclass; + try { + subclass = enhancer.createClass(); + } + catch (CodeGenerationException ex) { + if (!fallback) { + throw ex; + } + // Possibly a package-visible @Bean method declaration not accessible + // in the given ClassLoader -> retry with original ClassLoader + enhancer.setClassLoader(null); + subclass = enhancer.createClass(); + } + // Registering callbacks statically (as opposed to thread-local) // is critical for usage in an OSGi environment (SPR-5932)... Enhancer.registerStaticCallbacks(subclass, CALLBACKS); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java index 96051f161729..052f27f43f22 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -18,11 +18,16 @@ import java.io.IOException; import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.ProtectionDomain; import java.security.SecureClassLoader; import org.junit.jupiter.api.Test; +import org.springframework.core.OverridingClassLoader; import org.springframework.core.SmartClassLoader; +import org.springframework.lang.Nullable; import org.springframework.util.StreamUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -36,19 +41,108 @@ class ConfigurationClassEnhancerTests { @Test void enhanceReloadedClass() throws Exception { ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer(); + ClassLoader parentClassLoader = getClass().getClassLoader(); - CustomClassLoader classLoader = new CustomClassLoader(parentClassLoader); + ClassLoader classLoader = new CustomSmartClassLoader(parentClassLoader); Class myClass = parentClassLoader.loadClass(MyConfig.class.getName()); - configurationClassEnhancer.enhance(myClass, parentClassLoader); - Class myReloadedClass = classLoader.loadClass(MyConfig.class.getName()); - Class enhancedReloadedClass = configurationClassEnhancer.enhance(myReloadedClass, classLoader); - assertThat(enhancedReloadedClass.getClassLoader()).isEqualTo(classLoader); + Class enhancedClass = configurationClassEnhancer.enhance(myClass, parentClassLoader); + assertThat(myClass).isAssignableFrom(enhancedClass); + + myClass = classLoader.loadClass(MyConfig.class.getName()); + enhancedClass = configurationClassEnhancer.enhance(myClass, classLoader); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); + assertThat(myClass).isAssignableFrom(enhancedClass); + } + + @Test + void withPublicClass() { + ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer(); + + ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader()); + Class enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader); + assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); + + classLoader = new OverridingClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader); + assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new CustomSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader); + assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader); + assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + } + + @Test + void withNonPublicClass() { + ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer(); + + ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader()); + Class enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader); + assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new OverridingClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader); + assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new CustomSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader); + assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader); + assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + } + + @Test + void withNonPublicMethod() { + ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer(); + + ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader()); + Class enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); + assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); + + classLoader = new OverridingClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); + assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new CustomSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); + assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); + assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); } @Configuration static class MyConfig { + @Bean + String myBean() { + return "bean"; + } + } + + + @Configuration + public static class MyConfigWithPublicClass { + @Bean public String myBean() { return "bean"; @@ -56,9 +150,29 @@ public String myBean() { } - static class CustomClassLoader extends SecureClassLoader implements SmartClassLoader { + @Configuration + static class MyConfigWithNonPublicClass { - CustomClassLoader(ClassLoader parent) { + @Bean + public String myBean() { + return "bean"; + } + } + + + @Configuration + public static class MyConfigWithNonPublicMethod { + + @Bean + String myBean() { + return "bean"; + } + } + + + static class CustomSmartClassLoader extends SecureClassLoader implements SmartClassLoader { + + CustomSmartClassLoader(ClassLoader parent) { super(parent); } @@ -82,6 +196,29 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE public boolean isClassReloadable(Class clazz) { return clazz.getName().contains("MyConfig"); } + + @Override + public ClassLoader getOriginalClassLoader() { + return getParent(); + } + + @Override + public Class publicDefineClass(String name, byte[] b, @Nullable ProtectionDomain protectionDomain) { + return defineClass(name, b, 0, b.length, protectionDomain); + } + } + + + static class BasicSmartClassLoader extends SecureClassLoader implements SmartClassLoader { + + BasicSmartClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + public Class publicDefineClass(String name, byte[] b, @Nullable ProtectionDomain protectionDomain) { + return defineClass(name, b, 0, b.length, protectionDomain); + } } } From 9d6d67188ae07394daf06c51cd21ad928b86a4d0 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 11 Feb 2025 22:47:24 +0100 Subject: [PATCH 049/108] Upgrade to Jetty 12.0.16, Netty 4.1.118, XStream 1.4.21, AssertJ 3.27.3, Checkstyle 10.21.2 --- .../springframework/build/CheckstyleConventions.java | 4 ++-- framework-platform/framework-platform.gradle | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java index 3fad7c143ce4..f955f18ce6fa 100644 --- a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java @@ -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"); * you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ public void apply(Project project) { project.getPlugins().apply(CheckstylePlugin.class); project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g")); CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class); - checkstyle.setToolVersion("10.20.2"); + checkstyle.setToolVersion("10.21.2"); checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle")); String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion(); DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies(); diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 4f89672a83dd..cf6c3b588388 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -9,15 +9,15 @@ javaPlatform { dependencies { api(platform("com.fasterxml.jackson:jackson-bom:2.15.4")) api(platform("io.micrometer:micrometer-bom:1.12.12")) - api(platform("io.netty:netty-bom:4.1.115.Final")) + api(platform("io.netty:netty-bom:4.1.118.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) api(platform("io.projectreactor:reactor-bom:2023.0.15")) api(platform("io.rsocket:rsocket-bom:1.1.5")) api(platform("org.apache.groovy:groovy-bom:4.0.24")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) - api(platform("org.assertj:assertj-bom:3.26.3")) - api(platform("org.eclipse.jetty:jetty-bom:12.0.15")) - api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.15")) + api(platform("org.assertj:assertj-bom:3.27.3")) + api(platform("org.eclipse.jetty:jetty-bom:12.0.16")) + api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.16")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3")) api(platform("org.junit:junit-bom:5.10.5")) @@ -44,7 +44,7 @@ dependencies { api("com.sun.xml.bind:jaxb-impl:3.0.2") api("com.sun.xml.bind:jaxb-xjc:3.0.2") api("com.thoughtworks.qdox:qdox:2.1.0") - api("com.thoughtworks.xstream:xstream:1.4.20") + api("com.thoughtworks.xstream:xstream:1.4.21") api("commons-io:commons-io:2.15.0") api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2") api("io.micrometer:context-propagation:1.1.1") From ecd943c7ec125949ab4321ae9ed665aba1118b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 13 Feb 2025 13:23:32 +0100 Subject: [PATCH 050/108] Next development version (v6.1.18-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index eccb3877bcdd..ea132c588a31 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.1.17-SNAPSHOT +version=6.1.18-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From 3cf1fbf2227a81e262a660edf621baef36dc0ef4 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 18 Feb 2025 13:13:34 +0100 Subject: [PATCH 051/108] Mark XML-configured executor/scheduler as infrastructure bean Closes gh-34015 (cherry picked from commit d0ceefedc6588fd57c0df1f2729e7fbffac8a3c1) --- .../scheduling/config/ExecutorBeanDefinitionParser.java | 4 +++- .../scheduling/config/SchedulerBeanDefinitionParser.java | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java index 1987aba7b9d4..3516eff33b47 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java @@ -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"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import org.w3c.dom.Element; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; @@ -53,6 +54,7 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit if (StringUtils.hasText(poolSize)) { builder.addPropertyValue("poolSize", poolSize); } + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); } private void configureRejectionPolicy(Element element, BeanDefinitionBuilder builder) { diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParser.java index a9429e4901aa..4eb9e1c1431e 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2025 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. @@ -18,6 +18,7 @@ import org.w3c.dom.Element; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.util.StringUtils; @@ -41,6 +42,7 @@ protected void doParse(Element element, BeanDefinitionBuilder builder) { if (StringUtils.hasText(poolSize)) { builder.addPropertyValue("poolSize", poolSize); } + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); } } From 48628194dc1cdeff8393536739a17026ba6d7ae6 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 18 Feb 2025 15:16:25 +0100 Subject: [PATCH 052/108] Apply fallback in case of any exception coming out of createClass Closes gh-34423 (cherry picked from commit 93134fd4d1b2ac39071098509794c386164d47ff) --- .../context/annotation/ConfigurationClassEnhancer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index 590a685f5386..2a2ec2ad7b21 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -168,9 +168,9 @@ private Class createClass(Enhancer enhancer, boolean fallback) { try { subclass = enhancer.createClass(); } - catch (CodeGenerationException ex) { + catch (Throwable ex) { if (!fallback) { - throw ex; + throw (ex instanceof CodeGenerationException cgex ? cgex : new CodeGenerationException(ex)); } // Possibly a package-visible @Bean method declaration not accessible // in the given ClassLoader -> retry with original ClassLoader From b48d72cbd0386617c89e4d62ed6b6e4c1204e62e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 18 Feb 2025 20:40:13 +0100 Subject: [PATCH 053/108] Apply fallback in case of initial SmartClassLoader mismatch as well See gh-34423 (cherry picked from commit 6786e1c3e591e820282d9febe198dee363a292cf) --- .../context/annotation/ConfigurationClassEnhancer.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index 2a2ec2ad7b21..a05ee31cb724 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -112,12 +112,11 @@ public Class enhance(Class configClass, @Nullable ClassLoader classLoader) try { // Use original ClassLoader if config class not locally loaded in overriding class loader - if (classLoader instanceof SmartClassLoader smartClassLoader && - classLoader != configClass.getClassLoader()) { + boolean classLoaderMismatch = (classLoader != null && classLoader != configClass.getClassLoader()); + if (classLoaderMismatch && classLoader instanceof SmartClassLoader smartClassLoader) { classLoader = smartClassLoader.getOriginalClassLoader(); } Enhancer enhancer = newEnhancer(configClass, classLoader); - boolean classLoaderMismatch = (classLoader != null && classLoader != configClass.getClassLoader()); Class enhancedClass = createClass(enhancer, classLoaderMismatch); if (logger.isTraceEnabled()) { logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s", @@ -188,8 +187,7 @@ private Class createClass(Enhancer enhancer, boolean fallback) { /** * Marker interface to be implemented by all @Configuration CGLIB subclasses. * Facilitates idempotent behavior for {@link ConfigurationClassEnhancer#enhance} - * through checking to see if candidate classes are already assignable to it, e.g. - * have already been enhanced. + * through checking to see if candidate classes are already assignable to it. *

    Also extends {@link BeanFactoryAware}, as all enhanced {@code @Configuration} * classes require access to the {@link BeanFactory} that created them. *

    Note that this interface is intended for framework-internal use only, however From 01ae0a7c26e164cc09452410880651fee189ab31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= <141109+sdeleuze@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:45:08 +0100 Subject: [PATCH 054/108] Fix broken antora task See https://github.com/spring-io/antora-extensions/pull/43 Closes gh-34455 --- framework-docs/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/framework-docs/package.json b/framework-docs/package.json index c3570e2f8a62..90bd21400fd5 100644 --- a/framework-docs/package.json +++ b/framework-docs/package.json @@ -5,6 +5,7 @@ "@antora/collector-extension": "1.0.0-alpha.3", "@asciidoctor/tabs": "1.0.0-beta.6", "@springio/antora-extensions": "1.11.1", + "fast-xml-parser": "4.5.2", "@springio/asciidoctor-extensions": "1.0.0-alpha.10" } } From b5c89c91a9aa3a8f6e2e58869da126c6c8e045bf Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 21 Feb 2025 14:23:12 +0100 Subject: [PATCH 055/108] Handle null values in MockHttpServletResponse#setHeader Prior to this commit, `MockHttpServletResponse#setHeader` would not remove the header entry when given a `null` value, as documented in the Servlet API. This commit ensures that this behavior is enforced. Fixes gh-34466 --- .../mock/web/MockHttpServletResponse.java | 7 ++++++- .../mock/web/MockHttpServletResponseTests.java | 12 ++++++++++++ .../testfixture/servlet/MockHttpServletResponse.java | 7 ++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java index 19e8753e4fe8..03b54d6a01e0 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java @@ -671,7 +671,12 @@ private DateFormat newDateFormat() { @Override public void setHeader(String name, @Nullable String value) { - setHeaderValue(name, value); + if (value == null) { + this.headers.remove(name); + } + else { + setHeaderValue(name, value); + } } @Override diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index 6d5c92007d13..e3177c8f0b86 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -87,6 +87,18 @@ void setHeaderWithNullValue(String headerName) { assertThat(response.containsHeader(headerName)).isFalse(); } + @ParameterizedTest + @ValueSource(strings = { + CONTENT_TYPE, + CONTENT_LANGUAGE, + "X-Test-Header" + }) + void removeHeaderIfNullValue(String headerName) { + response.addHeader(headerName, "test"); + response.setHeader(headerName, null); + assertThat(response.containsHeader(headerName)).isFalse(); + } + @Test // gh-26493 void setLocaleWithNullValue() { assertThat(response.getLocale()).isEqualTo(Locale.getDefault()); diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java index 9ced4d59b1ad..9e29ad3a603f 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java @@ -671,7 +671,12 @@ private DateFormat newDateFormat() { @Override public void setHeader(String name, @Nullable String value) { - setHeaderValue(name, value); + if (value == null) { + this.headers.remove(name); + } + else { + setHeaderValue(name, value); + } } @Override From ccf4b028d2900b19690220ff7e4724ef61d823c0 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 21 Feb 2025 14:33:03 +0100 Subject: [PATCH 056/108] Fix null value support in ContentCachingResponseWrapper Prior to this commit, calling `setHeader` on the response wrapper would have a separate code path for the "Content-Length" header. This did not support calls with `null` values and would result in an exception. This commit ensures that the cached content length value is reset in this case and that the call is forwarded properly to the superclass. Fixes gh-34465 --- .../web/util/ContentCachingResponseWrapper.java | 8 +++++++- .../filter/ContentCachingResponseWrapperTests.java | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java b/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java index 41fc196d6781..f3596993daa7 100644 --- a/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java +++ b/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java @@ -164,7 +164,13 @@ public boolean containsHeader(String name) { @Override public void setHeader(String name, String value) { if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { - this.contentLength = toContentLengthInt(Long.parseLong(value)); + if (value != null) { + this.contentLength = toContentLengthInt(Long.parseLong(value)); + } + else { + this.contentLength = null; + super.setHeader(name, null); + } } else { super.setHeader(name, value); diff --git a/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java b/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java index 25ddffbc3b63..5fd7a300c9f9 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java @@ -270,6 +270,17 @@ void setContentLengthAbove2GbViaSetHeader() { .withMessageContaining(overflow); } + @Test + void setContentLengthNull() { + MockHttpServletResponse response = new MockHttpServletResponse(); + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); + responseWrapper.setContentLength(1024); + responseWrapper.setHeader(CONTENT_LENGTH, null); + + assertThat(response.getHeaderNames()).doesNotContain(CONTENT_LENGTH); + assertThat(responseWrapper.getHeader(CONTENT_LENGTH)).isNull(); + } + private void assertHeader(HttpServletResponse response, String header, int value) { assertHeader(response, header, Integer.toString(value)); From 7460be617b6fc5486cea7a8274426cf18d8c83a8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 23 Feb 2025 15:15:25 +0100 Subject: [PATCH 057/108] Consistent default ClassLoader fallback in hint classes Closes gh-34470 --- .../aot/hint/ResourceHints.java | 19 +++++++------ .../aot/hint/RuntimeHintsRegistrar.java | 9 +++--- .../FilePatternResourceHintsRegistrar.java | 28 ++++++++++--------- .../SpringFactoriesLoaderRuntimeHints.java | 11 ++++---- 4 files changed, 36 insertions(+), 31 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ResourceHints.java b/spring-core/src/main/java/org/springframework/aot/hint/ResourceHints.java index 490dd257f497..ebe6ab916957 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ResourceHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ResourceHints.java @@ -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"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; /** * Gather the need for resources available at runtime. @@ -50,14 +51,14 @@ public ResourceHints() { this.resourceBundleHints = new LinkedHashSet<>(); } + /** * Return the resources that should be made available at runtime. * @return a stream of {@link ResourcePatternHints} */ public Stream resourcePatternHints() { Stream patterns = this.resourcePatternHints.stream(); - return (this.types.isEmpty() ? patterns - : Stream.concat(Stream.of(typesPatternResourceHint()), patterns)); + return (this.types.isEmpty() ? patterns : Stream.concat(Stream.of(typesPatternResourceHint()), patterns)); } /** @@ -70,18 +71,18 @@ public Stream resourceBundleHints() { /** * Register a pattern if the given {@code location} is available on the - * classpath. This delegates to {@link ClassLoader#getResource(String)} - * which validates directories as well. The location is not included in - * the hint. - * @param classLoader the classloader to use + * classpath. This delegates to {@link ClassLoader#getResource(String)} which + * validates directories as well. The location is not included in the hint. + * @param classLoader the ClassLoader to use, or {@code null} for the default * @param location a '/'-separated path name that should exist * @param resourceHint a builder to customize the resource pattern * @return {@code this}, to facilitate method chaining */ public ResourceHints registerPatternIfPresent(@Nullable ClassLoader classLoader, String location, Consumer resourceHint) { - ClassLoader classLoaderToUse = (classLoader != null ? classLoader : getClass().getClassLoader()); - if (classLoaderToUse.getResource(location) != null) { + + ClassLoader classLoaderToUse = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); + if (classLoaderToUse != null && classLoaderToUse.getResource(location) != null) { registerPattern(resourceHint); } return this; diff --git a/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHintsRegistrar.java b/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHintsRegistrar.java index c08aa06186a9..83db8b9fbff0 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHintsRegistrar.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHintsRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -25,8 +25,9 @@ * *

    Implementations of this interface can be registered dynamically by using * {@link org.springframework.context.annotation.ImportRuntimeHints @ImportRuntimeHints} - * or statically in {@code META-INF/spring/aot.factories} by using the FQN of this - * interface as the key. A standard no-arg constructor is required for implementations. + * or statically in {@code META-INF/spring/aot.factories} by using the fully-qualified + * class name of this interface as the key. A standard no-arg constructor is required + * for implementations. * * @author Brian Clozel * @author Stephane Nicoll @@ -38,7 +39,7 @@ public interface RuntimeHintsRegistrar { /** * Contribute hints to the given {@link RuntimeHints} instance. * @param hints the hints contributed so far for the deployment unit - * @param classLoader the classloader, or {@code null} if even the system ClassLoader isn't accessible + * @param classLoader the ClassLoader to use, or {@code null} for the default */ void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader); diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrar.java b/spring-core/src/main/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrar.java index f5e3525a1e80..a41d5d31f28c 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrar.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrar.java @@ -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"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.springframework.aot.hint.ResourceHints; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.ResourceUtils; /** @@ -66,19 +67,21 @@ public FilePatternResourceHintsRegistrar(List filePrefixes, List @Deprecated(since = "6.0.12", forRemoval = true) public void registerHints(ResourceHints hints, @Nullable ClassLoader classLoader) { - ClassLoader classLoaderToUse = (classLoader != null ? classLoader : getClass().getClassLoader()); - List includes = new ArrayList<>(); - for (String location : this.classpathLocations) { - if (classLoaderToUse.getResource(location) != null) { - for (String filePrefix : this.filePrefixes) { - for (String fileExtension : this.fileExtensions) { - includes.add(location + filePrefix + "*" + fileExtension); + ClassLoader classLoaderToUse = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); + if (classLoaderToUse != null) { + List includes = new ArrayList<>(); + for (String location : this.classpathLocations) { + if (classLoaderToUse.getResource(location) != null) { + for (String filePrefix : this.filePrefixes) { + for (String fileExtension : this.fileExtensions) { + includes.add(location + filePrefix + "*" + fileExtension); + } } } } - } - if (!includes.isEmpty()) { - hints.registerPattern(hint -> hint.includes(includes.toArray(String[]::new))); + if (!includes.isEmpty()) { + hints.registerPattern(hint -> hint.includes(includes.toArray(String[]::new))); + } } } @@ -246,8 +249,7 @@ private FilePatternResourceHintsRegistrar build() { * classpath location that resolves against the {@code ClassLoader}, files * with the configured file prefixes and extensions are registered. * @param hints the hints contributed so far for the deployment unit - * @param classLoader the classloader, or {@code null} if even the system - * ClassLoader isn't accessible + * @param classLoader the ClassLoader to use, or {@code null} for the default */ public void registerHints(ResourceHints hints, @Nullable ClassLoader classLoader) { build().registerHints(hints, classLoader); diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHints.java b/spring-core/src/main/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHints.java index 8081223c5215..819361ffff1b 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHints.java @@ -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"); * you may not use this file except in compliance with the License. @@ -48,10 +48,11 @@ class SpringFactoriesLoaderRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { - ClassLoader classLoaderToUse = (classLoader != null ? classLoader : - SpringFactoriesLoaderRuntimeHints.class.getClassLoader()); - for (String resourceLocation : RESOURCE_LOCATIONS) { - registerHints(hints, classLoaderToUse, resourceLocation); + ClassLoader classLoaderToUse = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); + if (classLoaderToUse != null) { + for (String resourceLocation : RESOURCE_LOCATIONS) { + registerHints(hints, classLoaderToUse, resourceLocation); + } } } From c02d07fdef827890212ad9780a50898cc38e6deb Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 25 Feb 2025 17:11:37 +0100 Subject: [PATCH 058/108] Fix multiple Content-Language values in MockHttpServletResponse Prior to this commit, `MockHttpServletResponse` would only support adding a `Content-Language` once. Adding multiple header values would always replace the content-language property in the response and the entire header value. This commit ensures that this behavior is supported. Fixes gh-34491 --- .../mock/web/MockHttpServletResponse.java | 21 +++++++++++-------- .../web/MockHttpServletResponseTests.java | 10 ++++++++- .../servlet/MockHttpServletResponse.java | 21 +++++++++++-------- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java index 03b54d6a01e0..743b1fd019e4 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java @@ -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"); * you may not use this file except in compliance with the License. @@ -728,14 +728,17 @@ else if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { } else if (HttpHeaders.CONTENT_LANGUAGE.equalsIgnoreCase(name)) { String contentLanguages = value.toString(); - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_LANGUAGE, contentLanguages); - Locale language = headers.getContentLanguage(); - setLocale(language != null ? language : Locale.getDefault()); - // Since setLocale() sets the Content-Language header to the given - // single Locale, we have to explicitly set the Content-Language header - // to the user-provided value. - doAddHeaderValue(HttpHeaders.CONTENT_LANGUAGE, contentLanguages, true); + // only set the locale if we replace the header or if there was none before + if (replaceHeader || !this.headers.containsKey(HttpHeaders.CONTENT_LANGUAGE)) { + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_LANGUAGE, contentLanguages); + Locale language = headers.getContentLanguage(); + this.locale = language != null ? language : Locale.getDefault(); + doAddHeaderValue(HttpHeaders.CONTENT_LANGUAGE, contentLanguages, replaceHeader); + } + else { + doAddHeaderValue(HttpHeaders.CONTENT_LANGUAGE, contentLanguages, false); + } return true; } else if (HttpHeaders.SET_COOKIE.equalsIgnoreCase(name)) { diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index e3177c8f0b86..9a137d1c4f8b 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -631,4 +631,12 @@ void resetResetsCharset() { assertThat(contentTypeHeader).isEqualTo("text/plain"); } + @Test // gh-34488 + void shouldAddMultipleContentLanguage() { + response.addHeader("Content-Language", "en"); + response.addHeader("Content-Language", "fr"); + assertThat(response.getHeaders("Content-Language")).contains("en", "fr"); + assertThat(response.getLocale()).isEqualTo(Locale.ENGLISH); + } + } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java index 9e29ad3a603f..3993548280da 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java @@ -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"); * you may not use this file except in compliance with the License. @@ -728,14 +728,17 @@ else if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { } else if (HttpHeaders.CONTENT_LANGUAGE.equalsIgnoreCase(name)) { String contentLanguages = value.toString(); - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_LANGUAGE, contentLanguages); - Locale language = headers.getContentLanguage(); - setLocale(language != null ? language : Locale.getDefault()); - // Since setLocale() sets the Content-Language header to the given - // single Locale, we have to explicitly set the Content-Language header - // to the user-provided value. - doAddHeaderValue(HttpHeaders.CONTENT_LANGUAGE, contentLanguages, true); + // only set the locale if we replace the header or if there was none before + if (replaceHeader || !this.headers.containsKey(HttpHeaders.CONTENT_LANGUAGE)) { + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_LANGUAGE, contentLanguages); + Locale language = headers.getContentLanguage(); + this.locale = language != null ? language : Locale.getDefault(); + doAddHeaderValue(HttpHeaders.CONTENT_LANGUAGE, contentLanguages, replaceHeader); + } + else { + doAddHeaderValue(HttpHeaders.CONTENT_LANGUAGE, contentLanguages, false); + } return true; } else if (HttpHeaders.SET_COOKIE.equalsIgnoreCase(name)) { From ea419d2dcd5f1aa0e91744fd3305890e245032d1 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 25 Feb 2025 16:20:12 +0100 Subject: [PATCH 059/108] Avoid unnecessary CGLIB processing on configuration classes Closes gh-34486 (cherry picked from commit aff9ac72ec5e7ea426b963f9d2d2a613b67ef880) --- .../annotation/ConfigurationClass.java | 13 +++- .../annotation/ConfigurationClassParser.java | 75 +++++++++++-------- .../ConfigurationClassPostProcessorTests.java | 24 +++++- ...alidConfigurationClassDefinitionTests.java | 12 +-- 4 files changed, 84 insertions(+), 40 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java index f952bf666ba1..57375fcb9e80 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -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"); * you may not use this file except in compliance with the License. @@ -191,6 +191,15 @@ Set getBeanMethods() { return this.beanMethods; } + boolean hasNonStaticBeanMethods() { + for (BeanMethod beanMethod : this.beanMethods) { + if (!beanMethod.getMetadata().isStatic()) { + return true; + } + } + return false; + } + void addImportedResource(String importedResource, Class readerClass) { this.importedResources.put(importedResource, readerClass); } @@ -212,7 +221,7 @@ void validate(ProblemReporter problemReporter) { // A configuration class may not be final (CGLIB limitation) unless it declares proxyBeanMethods=false if (attributes != null && (Boolean) attributes.get("proxyBeanMethods")) { - if (this.metadata.isFinal()) { + if (hasNonStaticBeanMethods() && this.metadata.isFinal()) { problemReporter.error(new FinalConfigurationProblem()); } for (BeanMethod beanMethod : this.beanMethods) { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 9a7917011b7a..be2188021713 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -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"); * you may not use this file except in compliance with the License. @@ -81,10 +81,9 @@ * any number of ConfigurationClass objects because one Configuration class may import * another using the {@link Import} annotation). * - *

    This class helps separate the concern of parsing the structure of a Configuration - * class from the concern of registering BeanDefinition objects based on the content of - * that model (with the exception of {@code @ComponentScan} annotations which need to be - * registered immediately). + *

    This class helps separate the concern of parsing the structure of a Configuration class + * from the concern of registering BeanDefinition objects based on the content of that model + * (except {@code @ComponentScan} annotations which need to be registered immediately). * *

    This ASM-based implementation avoids reflection and eager class loading in order to * interoperate effectively with lazy class loading in a Spring ApplicationContext. @@ -161,14 +160,22 @@ public void parse(Set configCandidates) { for (BeanDefinitionHolder holder : configCandidates) { BeanDefinition bd = holder.getBeanDefinition(); try { + ConfigurationClass configClass; if (bd instanceof AnnotatedBeanDefinition annotatedBeanDef) { - parse(annotatedBeanDef.getMetadata(), holder.getBeanName()); + configClass = parse(annotatedBeanDef.getMetadata(), holder.getBeanName()); } else if (bd instanceof AbstractBeanDefinition abstractBeanDef && abstractBeanDef.hasBeanClass()) { - parse(abstractBeanDef.getBeanClass(), holder.getBeanName()); + configClass = parse(abstractBeanDef.getBeanClass(), holder.getBeanName()); } else { - parse(bd.getBeanClassName(), holder.getBeanName()); + configClass = parse(bd.getBeanClassName(), holder.getBeanName()); + } + + // Downgrade to lite (no enhancement) in case of no instance-level @Bean methods. + if (!configClass.hasNonStaticBeanMethods() && ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals( + bd.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE))) { + bd.setAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE, + ConfigurationClassUtils.CONFIGURATION_CLASS_LITE); } } catch (BeanDefinitionStoreException ex) { @@ -183,31 +190,37 @@ else if (bd instanceof AbstractBeanDefinition abstractBeanDef && abstractBeanDef this.deferredImportSelectorHandler.process(); } - protected final void parse(@Nullable String className, String beanName) throws IOException { - Assert.notNull(className, "No bean class name for configuration class bean definition"); - MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className); - processConfigurationClass(new ConfigurationClass(reader, beanName), DEFAULT_EXCLUSION_FILTER); + final ConfigurationClass parse(AnnotationMetadata metadata, String beanName) { + ConfigurationClass configClass = new ConfigurationClass(metadata, beanName); + processConfigurationClass(configClass, DEFAULT_EXCLUSION_FILTER); + return configClass; } - protected final void parse(Class clazz, String beanName) throws IOException { - processConfigurationClass(new ConfigurationClass(clazz, beanName), DEFAULT_EXCLUSION_FILTER); + final ConfigurationClass parse(Class clazz, String beanName) { + ConfigurationClass configClass = new ConfigurationClass(clazz, beanName); + processConfigurationClass(configClass, DEFAULT_EXCLUSION_FILTER); + return configClass; } - protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException { - processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER); + final ConfigurationClass parse(@Nullable String className, String beanName) throws IOException { + Assert.notNull(className, "No bean class name for configuration class bean definition"); + MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className); + ConfigurationClass configClass = new ConfigurationClass(reader, beanName); + processConfigurationClass(configClass, DEFAULT_EXCLUSION_FILTER); + return configClass; } /** * Validate each {@link ConfigurationClass} object. * @see ConfigurationClass#validate */ - public void validate() { + void validate() { for (ConfigurationClass configClass : this.configurationClasses.keySet()) { configClass.validate(this.problemReporter); } } - public Set getConfigurationClasses() { + Set getConfigurationClasses() { return this.configurationClasses.keySet(); } @@ -216,7 +229,7 @@ List getPropertySourceDescriptors() { Collections.emptyList()); } - protected void processConfigurationClass(ConfigurationClass configClass, Predicate filter) throws IOException { + protected void processConfigurationClass(ConfigurationClass configClass, Predicate filter) { if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) { return; } @@ -448,7 +461,7 @@ private Set retrieveBeanMethodMetadata(SourceClass sourceClass) /** - * Returns {@code @Import} class, considering all meta-annotations. + * Returns {@code @Import} classes, considering all meta-annotations. */ private Set getImports(SourceClass sourceClass) throws IOException { Set imports = new LinkedHashSet<>(); @@ -636,7 +649,7 @@ private static class ImportStack extends ArrayDeque implemen private final MultiValueMap imports = new LinkedMultiValueMap<>(); - public void registerImport(AnnotationMetadata importingClass, String importedClass) { + void registerImport(AnnotationMetadata importingClass, String importedClass) { this.imports.add(importedClass, importingClass); } @@ -691,7 +704,7 @@ private class DeferredImportSelectorHandler { * @param configClass the source configuration class * @param importSelector the selector to handle */ - public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) { + void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) { DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(configClass, importSelector); if (this.deferredImportSelectors == null) { DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler(); @@ -703,7 +716,7 @@ public void handle(ConfigurationClass configClass, DeferredImportSelector import } } - public void process() { + void process() { List deferredImports = this.deferredImportSelectors; this.deferredImportSelectors = null; try { @@ -727,7 +740,7 @@ private class DeferredImportSelectorGroupingHandler { private final Map configurationClasses = new HashMap<>(); - public void register(DeferredImportSelectorHolder deferredImport) { + void register(DeferredImportSelectorHolder deferredImport) { Class group = deferredImport.getImportSelector().getImportGroup(); DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent( (group != null ? group : deferredImport), @@ -737,7 +750,7 @@ public void register(DeferredImportSelectorHolder deferredImport) { deferredImport.getConfigurationClass()); } - public void processGroupImports() { + void processGroupImports() { for (DeferredImportSelectorGrouping grouping : this.groupings.values()) { Predicate exclusionFilter = grouping.getCandidateFilter(); grouping.getImports().forEach(entry -> { @@ -775,16 +788,16 @@ private static class DeferredImportSelectorHolder { private final DeferredImportSelector importSelector; - public DeferredImportSelectorHolder(ConfigurationClass configClass, DeferredImportSelector selector) { + DeferredImportSelectorHolder(ConfigurationClass configClass, DeferredImportSelector selector) { this.configurationClass = configClass; this.importSelector = selector; } - public ConfigurationClass getConfigurationClass() { + ConfigurationClass getConfigurationClass() { return this.configurationClass; } - public DeferredImportSelector getImportSelector() { + DeferredImportSelector getImportSelector() { return this.importSelector; } } @@ -800,7 +813,7 @@ private static class DeferredImportSelectorGrouping { this.group = group; } - public void add(DeferredImportSelectorHolder deferredImport) { + void add(DeferredImportSelectorHolder deferredImport) { this.deferredImports.add(deferredImport); } @@ -808,7 +821,7 @@ public void add(DeferredImportSelectorHolder deferredImport) { * Return the imports defined by the group. * @return each import with its associated configuration class */ - public Iterable getImports() { + Iterable getImports() { for (DeferredImportSelectorHolder deferredImport : this.deferredImports) { this.group.process(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getImportSelector()); @@ -816,7 +829,7 @@ public Iterable getImports() { return this.group.selectImports(); } - public Predicate getCandidateFilter() { + Predicate getCandidateFilter() { Predicate mergedFilter = DEFAULT_EXCLUSION_FILTER; for (DeferredImportSelectorHolder deferredImport : this.deferredImports) { Predicate selectorFilter = deferredImport.getImportSelector().getExclusionFilter(); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java index c205ac781ab9..806887a5f5ee 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -67,6 +67,7 @@ import org.springframework.core.task.SyncTaskExecutor; import org.springframework.stereotype.Component; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -104,6 +105,7 @@ void enhancementIsPresentBecauseSingletonSemanticsAreRespected() { ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).contains(ClassUtils.CGLIB_CLASS_SEPARATOR); Foo foo = beanFactory.getBean("foo", Foo.class); Bar bar = beanFactory.getBean("bar", Bar.class); assertThat(bar.foo).isSameAs(foo); @@ -118,6 +120,7 @@ void enhancementIsPresentBecauseSingletonSemanticsAreRespectedUsingAsm() { ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).contains(ClassUtils.CGLIB_CLASS_SEPARATOR); Foo foo = beanFactory.getBean("foo", Foo.class); Bar bar = beanFactory.getBean("bar", Bar.class); assertThat(bar.foo).isSameAs(foo); @@ -132,6 +135,7 @@ void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalse() { ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).doesNotContain(ClassUtils.CGLIB_CLASS_SEPARATOR); Foo foo = beanFactory.getBean("foo", Foo.class); Bar bar = beanFactory.getBean("bar", Bar.class); assertThat(bar.foo).isNotSameAs(foo); @@ -143,6 +147,7 @@ void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalseUsingAsm() { ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).doesNotContain(ClassUtils.CGLIB_CLASS_SEPARATOR); Foo foo = beanFactory.getBean("foo", Foo.class); Bar bar = beanFactory.getBean("bar", Bar.class); assertThat(bar.foo).isNotSameAs(foo); @@ -154,6 +159,7 @@ void enhancementIsNotPresentForStaticMethods() { ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).doesNotContain(ClassUtils.CGLIB_CLASS_SEPARATOR); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("foo")).hasBeanClass()).isTrue(); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("bar")).hasBeanClass()).isTrue(); Foo foo = beanFactory.getBean("foo", Foo.class); @@ -167,6 +173,7 @@ void enhancementIsNotPresentForStaticMethodsUsingAsm() { ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).doesNotContain(ClassUtils.CGLIB_CLASS_SEPARATOR); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("foo")).hasBeanClass()).isTrue(); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("bar")).hasBeanClass()).isTrue(); Foo foo = beanFactory.getBean("foo", Foo.class); @@ -174,6 +181,15 @@ void enhancementIsNotPresentForStaticMethodsUsingAsm() { assertThat(bar.foo).isNotSameAs(foo); } + @Test + void enhancementIsNotPresentWithEmptyConfig() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(EmptyConfig.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).doesNotContain(ClassUtils.CGLIB_CLASS_SEPARATOR); + } + @Test void configurationIntrospectionOfInnerClassesWorksWithDotNameSyntax() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(getClass().getName() + ".SingletonBeanConfig")); @@ -1158,7 +1174,7 @@ static class NonEnhancedSingletonBeanConfig { } @Configuration - static class StaticSingletonBeanConfig { + static final class StaticSingletonBeanConfig { @Bean public static Foo foo() { @@ -1171,6 +1187,10 @@ public static Bar bar() { } } + @Configuration + static final class EmptyConfig { + } + @Configuration @Order(2) static class OverridingSingletonBeanConfig { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/InvalidConfigurationClassDefinitionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/InvalidConfigurationClassDefinitionTests.java index bd2bb4a41022..4c1c8a8cf75b 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/InvalidConfigurationClassDefinitionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/InvalidConfigurationClassDefinitionTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -37,16 +37,18 @@ class InvalidConfigurationClassDefinitionTests { @Test void configurationClassesMayNotBeFinal() { @Configuration - final class Config { } + final class Config { + @Bean String dummy() { return "dummy"; } + } BeanDefinition configBeanDef = rootBeanDefinition(Config.class).getBeanDefinition(); DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); beanFactory.registerBeanDefinition("config", configBeanDef); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); - assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> - pp.postProcessBeanFactory(beanFactory)) - .withMessageContaining("Remove the final modifier"); + assertThatExceptionOfType(BeanDefinitionParsingException.class) + .isThrownBy(() -> pp.postProcessBeanFactory(beanFactory)) + .withMessageContaining("Remove the final modifier"); } } From a018ae6e028a58e3588a52ad4587b8bda9c7d435 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 26 Feb 2025 10:24:24 +0100 Subject: [PATCH 060/108] Lenient fallback when cached WeakReference returns null Closes gh-34423 --- .../cglib/core/AbstractClassGenerator.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/cglib/core/AbstractClassGenerator.java b/spring-core/src/main/java/org/springframework/cglib/core/AbstractClassGenerator.java index af66af94c9c7..5148de415a60 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/AbstractClassGenerator.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/AbstractClassGenerator.java @@ -123,13 +123,17 @@ public Predicate getUniqueNamePredicate() { } public Object get(AbstractClassGenerator gen, boolean useCache) { - if (!useCache) { - return gen.generate(ClassLoaderData.this); - } - else { + // SPRING PATCH BEGIN + Object value = null; + if (useCache) { Object cachedValue = generatedClasses.get(gen); - return gen.unwrapCachedValue(cachedValue); + value = gen.unwrapCachedValue(cachedValue); } + if (value == null) { // fallback when cached WeakReference returns null + value = gen.generate(ClassLoaderData.this); + } + return value; + // SPRING PATCH END } } From 0d60f266ad67cd413f402facd7a5b40745f19aec Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 28 Feb 2025 14:11:57 +0100 Subject: [PATCH 061/108] Defensively call isShutdown method for executor description Closes gh-34514 (cherry picked from commit 559ea6c4809376e14a8365c8b66c229070703d16) --- .../core/task/TaskRejectedException.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/task/TaskRejectedException.java b/spring-core/src/main/java/org/springframework/core/task/TaskRejectedException.java index 6d70d26f4bef..68b31633304c 100644 --- a/spring-core/src/main/java/org/springframework/core/task/TaskRejectedException.java +++ b/spring-core/src/main/java/org/springframework/core/task/TaskRejectedException.java @@ -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"); * you may not use this file except in compliance with the License. @@ -69,7 +69,13 @@ public TaskRejectedException(Executor executor, Object task, RejectedExecutionEx private static String executorDescription(Executor executor) { if (executor instanceof ExecutorService executorService) { - return "ExecutorService in " + (executorService.isShutdown() ? "shutdown" : "active") + " state"; + try { + return "ExecutorService in " + (executorService.isShutdown() ? "shutdown" : "active") + " state"; + } + catch (Exception ex) { + // UnsupportedOperationException/IllegalStateException from ManagedExecutorService.isShutdown() + // Falling back to toString() below. + } } return executor.toString(); } From 1e31cd685ebf373498f562d9f92f5d31842251ec Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 28 Feb 2025 14:12:51 +0100 Subject: [PATCH 062/108] Avoid getTargetConnection call on transaction-aware Connection close Closes gh-34484 (cherry picked from commit c64dae36237e9ab41737c4b72977db879f0356ef) --- .../jdbc/datasource/DataSourceUtils.java | 8 ++- .../TransactionAwareDataSourceProxy.java | 16 ++++-- .../DataSourceTransactionManagerTests.java | 49 ++++++++++++++++--- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java index 2787b30d902f..e897ae5eb717 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -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"); * you may not use this file except in compliance with the License. @@ -439,7 +439,11 @@ private static boolean connectionEquals(ConnectionHolder conHolder, Connection p public static Connection getTargetConnection(Connection con) { Connection conToUse = con; while (conToUse instanceof ConnectionProxy connectionProxy) { - conToUse = connectionProxy.getTargetConnection(); + Connection targetCon = connectionProxy.getTargetConnection(); + if (targetCon == conToUse) { + break; + } + conToUse = targetCon; } return conToUse; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java index 7f9a013c6210..0176c67c3244 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java @@ -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"); * you may not use this file except in compliance with the License. @@ -210,13 +210,23 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl sb.append('[').append(this.target).append(']'); } else { - sb.append(" from DataSource [").append(this.targetDataSource).append(']'); + sb.append("from DataSource [").append(this.targetDataSource).append(']'); } return sb.toString(); } case "close" -> { // Handle close method: only close if not within a transaction. - DataSourceUtils.doReleaseConnection(this.target, this.targetDataSource); + if (this.target != null) { + ConnectionHolder conHolder = (ConnectionHolder) + TransactionSynchronizationManager.getResource(this.targetDataSource); + if (conHolder != null && conHolder.hasConnection() && conHolder.getConnection() == this.target) { + // It's the transactional Connection: Don't close it. + conHolder.released(); + } + else { + DataSourceUtils.doCloseConnection(this.target, this.targetDataSource); + } + } this.closed = true; return null; } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java index 02f87c4d6901..7955bae143b6 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -72,7 +72,7 @@ public class DataSourceTransactionManagerTests { protected DataSource ds = mock(); - protected Connection con = mock(); + protected ConnectionProxy con = mock(); protected DataSourceTransactionManager tm; @@ -81,6 +81,7 @@ public class DataSourceTransactionManagerTests { void setup() throws Exception { tm = createTransactionManager(ds); given(ds.getConnection()).willReturn(con); + given(con.getTargetConnection()).willThrow(new UnsupportedOperationException()); } protected DataSourceTransactionManager createTransactionManager(DataSource ds) { @@ -1074,9 +1075,9 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { Connection tCon = dsProxy.getConnection(); tCon.getWarnings(); tCon.clearWarnings(); - assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + assertThat(((ConnectionProxy) tCon).getTargetConnection()).isEqualTo(con); // should be ignored - dsProxy.getConnection().close(); + tCon.close(); } catch (SQLException ex) { throw new UncategorizedSQLException("", "", ex); @@ -1110,9 +1111,9 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { Connection tCon = dsProxy.getConnection(); assertThatExceptionOfType(SQLException.class).isThrownBy(tCon::getWarnings); tCon.clearWarnings(); - assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + assertThat(((ConnectionProxy) tCon).getTargetConnection()).isEqualTo(con); // should be ignored - dsProxy.getConnection().close(); + tCon.close(); } catch (SQLException ex) { throw new UncategorizedSQLException("", "", ex); @@ -1128,6 +1129,42 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { verify(con).close(); } + @Test + void testTransactionAwareDataSourceProxyWithEarlyConnection() throws Exception { + given(ds.getConnection()).willReturn(mock(Connection.class), con); + given(con.getAutoCommit()).willReturn(true); + given(con.getWarnings()).willThrow(new SQLException()); + + TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); + dsProxy.setLazyTransactionalConnections(false); + Connection tCon = dsProxy.getConnection(); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); + try { + // should close the early Connection obtained before the transaction + tCon.close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); + + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + verify(con).close(); + } + @Test void testTransactionAwareDataSourceProxyWithSuspension() throws Exception { given(con.getAutoCommit()).willReturn(true); From 9ea049ad6f68ccf4a3743bb068924dc8ed70b3ff Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 12 Mar 2025 13:28:03 +0100 Subject: [PATCH 063/108] Upgrade to Reactor 2023.0.16 Includes Netty 4.1.119 and Jetty 12.0.17 Closes gh-34579 --- framework-platform/framework-platform.gradle | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index cf6c3b588388..87ec00e5075e 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -9,15 +9,15 @@ javaPlatform { dependencies { api(platform("com.fasterxml.jackson:jackson-bom:2.15.4")) api(platform("io.micrometer:micrometer-bom:1.12.12")) - api(platform("io.netty:netty-bom:4.1.118.Final")) + api(platform("io.netty:netty-bom:4.1.119.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2023.0.15")) + api(platform("io.projectreactor:reactor-bom:2023.0.16")) api(platform("io.rsocket:rsocket-bom:1.1.5")) api(platform("org.apache.groovy:groovy-bom:4.0.24")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) api(platform("org.assertj:assertj-bom:3.27.3")) - api(platform("org.eclipse.jetty:jetty-bom:12.0.16")) - api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.16")) + api(platform("org.eclipse.jetty:jetty-bom:12.0.17")) + api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.17")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3")) api(platform("org.junit:junit-bom:5.10.5")) @@ -101,8 +101,8 @@ dependencies { api("org.apache.derby:derby:10.16.1.1") api("org.apache.derby:derbyclient:10.16.1.1") api("org.apache.derby:derbytools:10.16.1.1") - api("org.apache.httpcomponents.client5:httpclient5:5.4.1") - api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.1") + api("org.apache.httpcomponents.client5:httpclient5:5.4.2") + api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.3") api("org.apache.poi:poi-ooxml:5.2.5") api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28") api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.28") @@ -116,7 +116,7 @@ dependencies { api("org.codehaus.jettison:jettison:1.5.4") api("org.crac:crac:1.4.0") api("org.dom4j:dom4j:2.1.4") - api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.8") + api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.9") api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4") api("org.eclipse:yasson:2.0.4") api("org.ehcache:ehcache:3.10.8") From 252058a65ab4084b291718a83d4c74b8b879f40e Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 13 Mar 2025 09:00:08 +0100 Subject: [PATCH 064/108] Next development version (v6.1.19-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ea132c588a31..e5b336dea287 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.1.18-SNAPSHOT +version=6.1.19-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From 62d7396769354236877d95ce182a4ee8f31b2595 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 17 Mar 2025 19:22:56 +0100 Subject: [PATCH 065/108] Suggest compilation with -parameters in case of ambiguity Closes gh-34609 (cherry picked from commit 86b2617c7f78802621f45c95ffc04cf017c37409) --- .../AspectJAdviceParameterNameDiscoverer.java | 19 +++--- .../AbstractAspectJAdvisorFactoryTests.java | 63 +++++++++---------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java index ec9b634ff89f..e581f6814dbe 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java @@ -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"); * you may not use this file except in compliance with the License. @@ -241,7 +241,7 @@ public String[] getParameterNames(Method method) { try { int algorithmicStep = STEP_JOIN_POINT_BINDING; - while ((this.numberOfRemainingUnboundArguments > 0) && algorithmicStep < STEP_FINISHED) { + while (this.numberOfRemainingUnboundArguments > 0 && algorithmicStep < STEP_FINISHED) { switch (algorithmicStep++) { case STEP_JOIN_POINT_BINDING -> { if (!maybeBindThisJoinPoint()) { @@ -373,7 +373,8 @@ private void maybeBindReturningVariable() { if (this.returningName != null) { if (this.numberOfRemainingUnboundArguments > 1) { throw new AmbiguousBindingException("Binding of returning parameter '" + this.returningName + - "' is ambiguous: there are " + this.numberOfRemainingUnboundArguments + " candidates."); + "' is ambiguous: there are " + this.numberOfRemainingUnboundArguments + " candidates. " + + "Consider compiling with -parameters in order to make declared parameter names available."); } // We're all set... find the unbound parameter, and bind it. @@ -485,8 +486,8 @@ private void maybeExtractVariableNamesFromArgs(@Nullable String argsSpec, List 1) { - throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments - + " unbound args at this()/target()/args() binding stage, with no way to determine between them"); + throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments + + " unbound args at this()/target()/args() binding stage, with no way to determine between them"); } List varNames = new ArrayList<>(); @@ -535,8 +536,8 @@ else if (varNames.size() == 1) { private void maybeBindReferencePointcutParameter() { if (this.numberOfRemainingUnboundArguments > 1) { - throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments - + " unbound args at reference pointcut binding stage, with no way to determine between them"); + throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments + + " unbound args at reference pointcut binding stage, with no way to determine between them"); } List varNames = new ArrayList<>(); @@ -741,7 +742,9 @@ private void findAndBind(Class argumentType, String varName) { * Simple record to hold the extracted text from a pointcut body, together * with the number of tokens consumed in extracting it. */ - private record PointcutBody(int numTokensConsumed, @Nullable String text) {} + private record PointcutBody(int numTokensConsumed, @Nullable String text) { + } + /** * Thrown in response to an ambiguous binding being detected when diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java index 02d968212d53..03cc27f239f0 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -203,7 +203,6 @@ void perThisAspect() throws Exception { itb.getSpouse(); assertThat(maaif.isMaterialized()).isTrue(); - assertThat(imapa.getDeclaredPointcut().getMethodMatcher().matches(TestBean.class.getMethod("getAge"), null)).isTrue(); assertThat(itb.getAge()).as("Around advice must apply").isEqualTo(0); @@ -301,7 +300,7 @@ void bindingWithSingleArg() { void bindingWithMultipleArgsDifferentlyOrdered() { ManyValuedArgs target = new ManyValuedArgs(); ManyValuedArgs mva = createProxy(target, ManyValuedArgs.class, - getAdvisorFactory().getAdvisors(aspectInstanceFactory(new ManyValuedArgs(), "someBean"))); + getAdvisorFactory().getAdvisors(aspectInstanceFactory(new ManyValuedArgs(), "someBean"))); String a = "a"; int b = 12; @@ -320,7 +319,7 @@ void introductionOnTargetNotImplementingInterface() { NotLockable notLockableTarget = new NotLockable(); assertThat(notLockableTarget).isNotInstanceOf(Lockable.class); NotLockable notLockable1 = createProxy(notLockableTarget, NotLockable.class, - getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean"))); + getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean"))); assertThat(notLockable1).isInstanceOf(Lockable.class); Lockable lockable = (Lockable) notLockable1; assertThat(lockable.locked()).isFalse(); @@ -329,7 +328,7 @@ void introductionOnTargetNotImplementingInterface() { NotLockable notLockable2Target = new NotLockable(); NotLockable notLockable2 = createProxy(notLockable2Target, NotLockable.class, - getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean"))); + getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean"))); assertThat(notLockable2).isInstanceOf(Lockable.class); Lockable lockable2 = (Lockable) notLockable2; assertThat(lockable2.locked()).isFalse(); @@ -343,20 +342,19 @@ void introductionOnTargetNotImplementingInterface() { void introductionAdvisorExcludedFromTargetImplementingInterface() { assertThat(AopUtils.findAdvisorsThatCanApply( getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new MakeLockable(), "someBean")), + aspectInstanceFactory(new MakeLockable(), "someBean")), CannotBeUnlocked.class)).isEmpty(); assertThat(AopUtils.findAdvisorsThatCanApply(getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new MakeLockable(),"someBean")), NotLockable.class)).hasSize(2); + aspectInstanceFactory(new MakeLockable(),"someBean")), NotLockable.class)).hasSize(2); } @Test void introductionOnTargetImplementingInterface() { CannotBeUnlocked target = new CannotBeUnlocked(); Lockable proxy = createProxy(target, CannotBeUnlocked.class, - // Ensure that we exclude AopUtils.findAdvisorsThatCanApply( - getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean")), - CannotBeUnlocked.class)); + getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean")), + CannotBeUnlocked.class)); assertThat(proxy).isInstanceOf(Lockable.class); Lockable lockable = proxy; assertThat(lockable.locked()).as("Already locked").isTrue(); @@ -370,8 +368,8 @@ void introductionOnTargetExcludedByTypePattern() { ArrayList target = new ArrayList<>(); List proxy = createProxy(target, List.class, AopUtils.findAdvisorsThatCanApply( - getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean")), - List.class)); + getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean")), + List.class)); assertThat(proxy).as("Type pattern must have excluded mixin").isNotInstanceOf(Lockable.class); } @@ -379,7 +377,7 @@ void introductionOnTargetExcludedByTypePattern() { void introductionBasedOnAnnotationMatch() { // gh-9980 AnnotatedTarget target = new AnnotatedTargetImpl(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new MakeAnnotatedTypeModifiable(), "someBean")); + aspectInstanceFactory(new MakeAnnotatedTypeModifiable(), "someBean")); Object proxy = createProxy(target, AnnotatedTarget.class, advisors); assertThat(proxy).isInstanceOf(Lockable.class); Lockable lockable = (Lockable) proxy; @@ -393,9 +391,9 @@ void introductionWithArgumentBinding() { TestBean target = new TestBean(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new MakeITestBeanModifiable(), "someBean")); + aspectInstanceFactory(new MakeITestBeanModifiable(), "someBean")); advisors.addAll(getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new MakeLockable(), "someBean"))); + aspectInstanceFactory(new MakeLockable(), "someBean"))); Modifiable modifiable = (Modifiable) createProxy(target, ITestBean.class, advisors); assertThat(modifiable).isInstanceOf(Modifiable.class); @@ -426,7 +424,7 @@ void aspectMethodThrowsExceptionLegalOnSignature() { TestBean target = new TestBean(); UnsupportedOperationException expectedException = new UnsupportedOperationException(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new ExceptionThrowingAspect(expectedException), "someBean")); + aspectInstanceFactory(new ExceptionThrowingAspect(expectedException), "someBean")); assertThat(advisors).as("One advice method was found").hasSize(1); ITestBean itb = createProxy(target, ITestBean.class, advisors); assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(itb::getAge); @@ -439,12 +437,12 @@ void aspectMethodThrowsExceptionIllegalOnSignature() { TestBean target = new TestBean(); RemoteException expectedException = new RemoteException(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new ExceptionThrowingAspect(expectedException), "someBean")); + aspectInstanceFactory(new ExceptionThrowingAspect(expectedException), "someBean")); assertThat(advisors).as("One advice method was found").hasSize(1); ITestBean itb = createProxy(target, ITestBean.class, advisors); assertThatExceptionOfType(UndeclaredThrowableException.class) - .isThrownBy(itb::getAge) - .withCause(expectedException); + .isThrownBy(itb::getAge) + .withCause(expectedException); } @Test @@ -452,7 +450,7 @@ void twoAdvicesOnOneAspect() { TestBean target = new TestBean(); TwoAdviceAspect twoAdviceAspect = new TwoAdviceAspect(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(twoAdviceAspect, "someBean")); + aspectInstanceFactory(twoAdviceAspect, "someBean")); assertThat(advisors).as("Two advice methods found").hasSize(2); ITestBean itb = createProxy(target, ITestBean.class, advisors); itb.setName(""); @@ -466,7 +464,7 @@ void twoAdvicesOnOneAspect() { void afterAdviceTypes() throws Exception { InvocationTrackingAspect aspect = new InvocationTrackingAspect(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(aspect, "exceptionHandlingAspect")); + aspectInstanceFactory(aspect, "exceptionHandlingAspect")); Echo echo = createProxy(new Echo(), Echo.class, advisors); assertThat(aspect.invocations).isEmpty(); @@ -475,7 +473,7 @@ void afterAdviceTypes() throws Exception { aspect.invocations.clear(); assertThatExceptionOfType(FileNotFoundException.class) - .isThrownBy(() -> echo.echo(new FileNotFoundException())); + .isThrownBy(() -> echo.echo(new FileNotFoundException())); assertThat(aspect.invocations).containsExactly("around - start", "before", "after throwing", "after", "around - end"); } @@ -487,7 +485,6 @@ void nonAbstractParentAspect() { assertThat(Modifier.isAbstract(aspect.getClass().getSuperclass().getModifiers())).isFalse(); List advisors = getAdvisorFactory().getAdvisors(aspectInstanceFactory(aspect, "incrementingAspect")); - ITestBean proxy = createProxy(new TestBean("Jane", 42), ITestBean.class, advisors); assertThat(proxy.getAge()).isEqualTo(86); // (42 + 1) * 2 } @@ -812,19 +809,19 @@ void before() { invocations.add("before"); } - @AfterReturning("echo()") - void afterReturning() { - invocations.add("after returning"); + @After("echo()") + void after() { + invocations.add("after"); } - @AfterThrowing("echo()") - void afterThrowing() { - invocations.add("after throwing"); + @AfterReturning(pointcut = "this(target) && execution(* echo(*))", returning = "returnValue") + void afterReturning(JoinPoint joinPoint, Echo target, Object returnValue) { + invocations.add("after returning"); } - @After("echo()") - void after() { - invocations.add("after"); + @AfterThrowing(pointcut = "this(target) && execution(* echo(*))", throwing = "exception") + void afterThrowing(JoinPoint joinPoint, Echo target, Throwable exception) { + invocations.add("after throwing"); } } @@ -967,7 +964,7 @@ private Method getGetterFromSetter(Method setter) { class MakeITestBeanModifiable extends AbstractMakeModifiable { @DeclareParents(value = "org.springframework.beans.testfixture.beans.ITestBean+", - defaultImpl=ModifiableImpl.class) + defaultImpl = ModifiableImpl.class) static MutableModifiable mixin; } From 8c494e5165e47be2e3c085a4abcdf69be3de6514 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 19 Mar 2025 10:59:46 +0100 Subject: [PATCH 066/108] Defer triggerAfterCompletion invocation in doRollbackOnCommitException Closes gh-34595 (cherry picked from commit cc986cd2e84e550288e763b255aae7703b8976b1) --- .../R2dbcTransactionManagerTests.java | 27 +++++++++++++++++++ .../AbstractReactiveTransactionManager.java | 20 ++++++-------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/R2dbcTransactionManagerTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/R2dbcTransactionManagerTests.java index 040c4359cbeb..44f62963771f 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/R2dbcTransactionManagerTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/R2dbcTransactionManagerTests.java @@ -23,6 +23,7 @@ import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.IsolationLevel; import io.r2dbc.spi.R2dbcBadGrammarException; +import io.r2dbc.spi.R2dbcTransientResourceException; import io.r2dbc.spi.Statement; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,6 +33,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import org.springframework.dao.TransientDataAccessResourceException; import org.springframework.r2dbc.BadSqlGrammarException; import org.springframework.transaction.CannotCreateTransactionException; import org.springframework.transaction.IllegalTransactionStateException; @@ -315,6 +317,31 @@ void testConnectionReleasedWhenRollbackFails() { verify(connectionMock).close(); } + @Test + void testCommitAndRollbackFails() { + when(connectionMock.isAutoCommit()).thenReturn(false); + when(connectionMock.commitTransaction()).thenReturn(Mono.defer(() -> + Mono.error(new R2dbcBadGrammarException("Commit should fail")))); + when(connectionMock.rollbackTransaction()).thenReturn(Mono.defer(() -> + Mono.error(new R2dbcTransientResourceException("Rollback should also fail")))); + + TransactionalOperator operator = TransactionalOperator.create(tm); + + ConnectionFactoryUtils.getConnection(connectionFactoryMock) + .doOnNext(connection -> connection.createStatement("foo")).then() + .as(operator::transactional) + .as(StepVerifier::create) + .verifyError(TransientDataAccessResourceException.class); + + verify(connectionMock).isAutoCommit(); + verify(connectionMock).beginTransaction(any(io.r2dbc.spi.TransactionDefinition.class)); + verify(connectionMock).createStatement("foo"); + verify(connectionMock).commitTransaction(); + verify(connectionMock).rollbackTransaction(); + verify(connectionMock).close(); + verifyNoMoreInteractions(connectionMock); + } + @Test void testTransactionSetRollbackOnly() { when(connectionMock.isAutoCommit()).thenReturn(false); diff --git a/spring-tx/src/main/java/org/springframework/transaction/reactive/AbstractReactiveTransactionManager.java b/spring-tx/src/main/java/org/springframework/transaction/reactive/AbstractReactiveTransactionManager.java index 6efe444555c2..39f652e8788e 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/reactive/AbstractReactiveTransactionManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/reactive/AbstractReactiveTransactionManager.java @@ -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"); * you may not use this file except in compliance with the License. @@ -494,21 +494,17 @@ else if (ErrorPredicates.TRANSACTION_EXCEPTION.test(ex)) { })); } else if (ErrorPredicates.RUNTIME_OR_ERROR.test(ex)) { - Mono mono; + Mono mono = Mono.empty(); if (!beforeCompletionInvoked.get()) { mono = triggerBeforeCompletion(synchronizationManager, status); } - else { - mono = Mono.empty(); - } result = mono.then(doRollbackOnCommitException(synchronizationManager, status, ex)) .then(propagateException); } - return result; }) .then(Mono.defer(() -> triggerAfterCommit(synchronizationManager, status).onErrorResume(ex -> - triggerAfterCompletion(synchronizationManager, status, TransactionSynchronization.STATUS_COMMITTED).then(Mono.error(ex))) + triggerAfterCompletion(synchronizationManager, status, TransactionSynchronization.STATUS_COMMITTED).then(Mono.error(ex))) .then(triggerAfterCompletion(synchronizationManager, status, TransactionSynchronization.STATUS_COMMITTED)) .then(Mono.defer(() -> { if (status.isNewTransaction()) { @@ -518,8 +514,8 @@ else if (ErrorPredicates.RUNTIME_OR_ERROR.test(ex)) { })))); return commit - .onErrorResume(ex -> cleanupAfterCompletion(synchronizationManager, status) - .then(Mono.error(ex))).then(cleanupAfterCompletion(synchronizationManager, status)); + .onErrorResume(ex -> cleanupAfterCompletion(synchronizationManager, status).then(Mono.error(ex))) + .then(cleanupAfterCompletion(synchronizationManager, status)); } /** @@ -571,8 +567,8 @@ private Mono processRollback(TransactionSynchronizationManager synchroniza } return beforeCompletion; } - })).onErrorResume(ErrorPredicates.RUNTIME_OR_ERROR, ex -> triggerAfterCompletion( - synchronizationManager, status, TransactionSynchronization.STATUS_UNKNOWN) + })).onErrorResume(ErrorPredicates.RUNTIME_OR_ERROR, ex -> + triggerAfterCompletion(synchronizationManager, status, TransactionSynchronization.STATUS_UNKNOWN) .then(Mono.defer(() -> { if (status.isNewTransaction()) { this.transactionExecutionListeners.forEach(listener -> listener.afterRollback(status, ex)); @@ -623,7 +619,7 @@ else if (status.hasTransaction()) { return Mono.empty(); })) .then(Mono.error(rbex)); - }).then(triggerAfterCompletion(synchronizationManager, status, TransactionSynchronization.STATUS_ROLLED_BACK)) + }).then(Mono.defer(() -> triggerAfterCompletion(synchronizationManager, status, TransactionSynchronization.STATUS_ROLLED_BACK))) .then(Mono.defer(() -> { this.transactionExecutionListeners.forEach(listener -> listener.afterRollback(status, null)); return Mono.empty(); From 618e003b91ae904975c4ae3b3c0588f1778fe003 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 19 Mar 2025 12:33:01 +0000 Subject: [PATCH 067/108] Fix dated Javadoc in MvcUriComponentsBuilder related to forwarded headers Closes gh-34615 --- .../web/cors/CorsConfiguration.java | 15 ++++----------- .../annotation/MvcUriComponentsBuilder.java | 15 +++++++-------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index 4e471cfc233d..f3bca0f5ee9d 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -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"); * you may not use this file except in compliance with the License. @@ -296,14 +296,7 @@ private static void parseCommaDelimitedOrigin(String rawValue, Consumer * allowCredentials} is set to {@code true}, that combination is handled * by copying the method specified in the CORS preflight request. *

    If not set, only {@code "GET"} and {@code "HEAD"} are allowed. - *

    By default this is not set. - *

    Note: CORS checks use values from "Forwarded" - * (RFC 7239), - * "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" headers, - * if present, in order to reflect the client-originated address. - * Consider using the {@code ForwardedHeaderFilter} in order to choose from a - * central place whether to extract and use, or to discard such headers. - * See the Spring Framework reference for more on this filter. + *

    By default, this is not set. */ public void setAllowedMethods(@Nullable List allowedMethods) { this.allowedMethods = (allowedMethods != null ? new ArrayList<>(allowedMethods) : null); @@ -455,7 +448,7 @@ public void addExposedHeader(String exposedHeader) { * level of trust with the configured domains and also increases the surface * attack of the web application by exposing sensitive user-specific * information such as cookies and CSRF tokens. - *

    By default this is not set (i.e. user credentials are not supported). + *

    By default, this is not set (i.e. user credentials are not supported). */ public void setAllowCredentials(@Nullable Boolean allowCredentials) { this.allowCredentials = allowCredentials; @@ -479,7 +472,7 @@ public Boolean getAllowCredentials() { *

    Setting this property has an impact on how {@link #setAllowedOrigins(List) * origins} and {@link #setAllowedOriginPatterns(List) originPatterns} are processed, * see related API documentation for more details. - *

    By default this is not set (i.e. private network access is not supported). + *

    By default, this is not set (i.e. private network access is not supported). * @since 5.3.32 * @see Private network access specifications */ diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java index c66bb0652ed4..64ad6f1db126 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java @@ -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"); * you may not use this file except in compliance with the License. @@ -84,13 +84,12 @@ * {@link #relativeTo(org.springframework.web.util.UriComponentsBuilder)}. * * - *

    Note: This class uses values from "Forwarded" - * (RFC 7239), - * "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" headers, - * if present, in order to reflect the client-originated protocol and address. - * Consider using the {@code ForwardedHeaderFilter} in order to choose from a - * central place whether to extract and use, or to discard such headers. - * See the Spring Framework reference for more on this filter. + *

    Note: As of 5.1, methods in this class do not extract + * {@code "Forwarded"} and {@code "X-Forwarded-*"} headers that specify the + * client-originated address. Please, use + * {@link org.springframework.web.filter.ForwardedHeaderFilter + * ForwardedHeaderFilter}, or similar from the underlying server, to extract + * and use such headers, or to discard them. * * @author Oliver Gierke * @author Rossen Stoyanchev From f06167ea0194bce9da565e5f86f978d156a6dc68 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 21 Mar 2025 10:58:40 +0100 Subject: [PATCH 068/108] Polishing --- .../beans/AbstractNestablePropertyAccessor.java | 5 ++--- .../jdbc/core/SingleColumnRowMapper.java | 5 +++-- .../service/DestinationVariableArgumentResolver.java | 6 +++--- .../springframework/web/client/DefaultRestClient.java | 8 ++------ .../tags/form/AbstractMultiCheckedElementTag.java | 11 +++++------ 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java index 64e3a1e57c2d..efca60ebcb95 100644 --- a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java @@ -291,7 +291,7 @@ private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) String lastKey = tokens.keys[tokens.keys.length - 1]; if (propValue.getClass().isArray()) { - Class requiredType = propValue.getClass().componentType(); + Class componentType = propValue.getClass().componentType(); int arrayIndex = Integer.parseInt(lastKey); Object oldValue = null; try { @@ -299,10 +299,9 @@ private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) oldValue = Array.get(propValue, arrayIndex); } Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), - requiredType, ph.nested(tokens.keys.length)); + componentType, ph.nested(tokens.keys.length)); int length = Array.getLength(propValue); if (arrayIndex >= length && arrayIndex < this.autoGrowCollectionLimit) { - Class componentType = propValue.getClass().componentType(); Object newArray = Array.newInstance(componentType, arrayIndex + 1); System.arraycopy(propValue, 0, newArray, 0, length); int lastKeyIndex = tokens.canonicalName.lastIndexOf('['); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java index 43dffc066eba..ccf632ef0576 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java @@ -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"); * you may not use this file except in compliance with the License. @@ -85,12 +85,13 @@ public void setRequiredType(Class requiredType) { * Set a {@link ConversionService} for converting a fetched value. *

    Default is the {@link DefaultConversionService}. * @since 5.0.4 - * @see DefaultConversionService#getSharedInstance + * @see DefaultConversionService#getSharedInstance() */ public void setConversionService(@Nullable ConversionService conversionService) { this.conversionService = conversionService; } + /** * Extract a value for the single column in the current row. *

    Validates that there is only one column selected, diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/DestinationVariableArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/DestinationVariableArgumentResolver.java index 99b00160da2a..844a8d9e0020 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/DestinationVariableArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/DestinationVariableArgumentResolver.java @@ -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"); * you may not use this file except in compliance with the License. @@ -48,8 +48,8 @@ public boolean resolve( collection.forEach(requestValues::addRouteVariable); return true; } - else if (argument.getClass().isArray()) { - for (Object variable : (Object[]) argument) { + else if (argument instanceof Object[] arguments) { + for (Object variable : arguments) { requestValues.addRouteVariable(variable); } return true; diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java index dcfb734e7da9..dd883d771161 100644 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java @@ -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"); * you may not use this file except in compliance with the License. @@ -139,6 +139,7 @@ final class DefaultRestClient implements RestClient { this.builder = builder; } + @Override public RequestHeadersUriSpec get() { return methodInternal(HttpMethod.GET); @@ -272,8 +273,6 @@ private static Class bodyClass(Type type) { } - - private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec { private final HttpMethod httpMethod; @@ -452,7 +451,6 @@ private void logBody(Object body, @Nullable MediaType mediaType, HttpMessageConv } } - @Override public ResponseSpec retrieve() { ResponseSpec responseSpec = exchangeInternal(DefaultResponseSpec::new, false); @@ -784,8 +782,6 @@ public void close() { this.observationScope.close(); this.observation.stop(); } - } - } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java index 3547f810cf87..e81fad4e564b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java @@ -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"); * you may not use this file except in compliance with the License. @@ -217,11 +217,10 @@ protected int writeTagContent(TagWriter tagWriter) throws JspException { throw new IllegalArgumentException("Attribute 'items' is required and must be a Collection, an Array or a Map"); } - if (itemsObject.getClass().isArray()) { - Object[] itemsArray = (Object[]) itemsObject; - for (int i = 0; i < itemsArray.length; i++) { - Object item = itemsArray[i]; - writeObjectEntry(tagWriter, valueProperty, labelProperty, item, i); + if (itemsObject instanceof Object[] itemsArray) { + for (int itemIndex = 0; itemIndex < itemsArray.length; itemIndex++) { + Object item = itemsArray[itemIndex]; + writeObjectEntry(tagWriter, valueProperty, labelProperty, item, itemIndex); } } else if (itemsObject instanceof Collection optionCollection) { From 19257f4fdb508d26badbb2b406933b6c9049fca9 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 21 Mar 2025 15:52:42 +0100 Subject: [PATCH 069/108] Add javadoc notes on potential exception suppression in getBeansOfType Closes gh-34629 (cherry picked from commit dc41ff569eb952261e781293eeeb066a3398d53e) --- .../beans/factory/ListableBeanFactory.java | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java index 4ce86eceef84..fcce79fb1cdb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java @@ -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"); * you may not use this file except in compliance with the License. @@ -145,8 +145,6 @@ public interface ListableBeanFactory extends BeanFactory { *

    Does not consider any hierarchy this factory may participate in. * Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors} * to include beans in ancestor factories too. - *

    Note: Does not ignore singleton beans that have been registered - * by other means than bean definitions. *

    This version of {@code getBeanNamesForType} matches all kinds of beans, * be it singletons, prototypes, or FactoryBeans. In most implementations, the * result will be the same as for {@code getBeanNamesForType(type, true, true)}. @@ -176,8 +174,6 @@ public interface ListableBeanFactory extends BeanFactory { *

    Does not consider any hierarchy this factory may participate in. * Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors} * to include beans in ancestor factories too. - *

    Note: Does not ignore singleton beans that have been registered - * by other means than bean definitions. *

    Bean names returned by this method should always return bean names in the * order of definition in the backend configuration, as far as possible. * @param type the generically typed class or interface to match @@ -210,8 +206,6 @@ public interface ListableBeanFactory extends BeanFactory { *

    Does not consider any hierarchy this factory may participate in. * Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors} * to include beans in ancestor factories too. - *

    Note: Does not ignore singleton beans that have been registered - * by other means than bean definitions. *

    This version of {@code getBeanNamesForType} matches all kinds of beans, * be it singletons, prototypes, or FactoryBeans. In most implementations, the * result will be the same as for {@code getBeanNamesForType(type, true, true)}. @@ -239,8 +233,6 @@ public interface ListableBeanFactory extends BeanFactory { *

    Does not consider any hierarchy this factory may participate in. * Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors} * to include beans in ancestor factories too. - *

    Note: Does not ignore singleton beans that have been registered - * by other means than bean definitions. *

    Bean names returned by this method should always return bean names in the * order of definition in the backend configuration, as far as possible. * @param type the class or interface to match, or {@code null} for all bean names @@ -265,21 +257,24 @@ public interface ListableBeanFactory extends BeanFactory { * subclasses), judging from either bean definitions or the value of * {@code getObjectType} in the case of FactoryBeans. *

    NOTE: This method introspects top-level beans only. It does not - * check nested beans which might match the specified type as well. + * check nested beans which might match the specified type as well. Also, it + * suppresses exceptions for beans that are currently in creation in a circular + * reference scenario: typically, references back to the caller of this method. *

    Does consider objects created by FactoryBeans, which means that FactoryBeans * will get initialized. If the object created by the FactoryBean doesn't match, * the raw FactoryBean itself will be matched against the type. *

    Does not consider any hierarchy this factory may participate in. * Use BeanFactoryUtils' {@code beansOfTypeIncludingAncestors} * to include beans in ancestor factories too. - *

    Note: Does not ignore singleton beans that have been registered - * by other means than bean definitions. *

    This version of getBeansOfType matches all kinds of beans, be it * singletons, prototypes, or FactoryBeans. In most implementations, the * result will be the same as for {@code getBeansOfType(type, true, true)}. *

    The Map returned by this method should always return bean names and * corresponding bean instances in the order of definition in the * backend configuration, as far as possible. + *

    Consider {@link #getBeanNamesForType(Class)} with selective {@link #getBean} + * calls for specific bean names in preference to this Map-based retrieval method. + * Aside from lazy instantiation benefits, this also avoids any exception suppression. * @param type the class or interface to match, or {@code null} for all concrete beans * @return a Map with the matching beans, containing the bean names as * keys and the corresponding bean instances as values @@ -295,7 +290,9 @@ public interface ListableBeanFactory extends BeanFactory { * subclasses), judging from either bean definitions or the value of * {@code getObjectType} in the case of FactoryBeans. *

    NOTE: This method introspects top-level beans only. It does not - * check nested beans which might match the specified type as well. + * check nested beans which might match the specified type as well. Also, it + * suppresses exceptions for beans that are currently in creation in a circular + * reference scenario: typically, references back to the caller of this method. *

    Does consider objects created by FactoryBeans if the "allowEagerInit" flag is set, * which means that FactoryBeans will get initialized. If the object created by the * FactoryBean doesn't match, the raw FactoryBean itself will be matched against the @@ -304,11 +301,12 @@ public interface ListableBeanFactory extends BeanFactory { *

    Does not consider any hierarchy this factory may participate in. * Use BeanFactoryUtils' {@code beansOfTypeIncludingAncestors} * to include beans in ancestor factories too. - *

    Note: Does not ignore singleton beans that have been registered - * by other means than bean definitions. *

    The Map returned by this method should always return bean names and * corresponding bean instances in the order of definition in the * backend configuration, as far as possible. + *

    Consider {@link #getBeanNamesForType(Class)} with selective {@link #getBean} + * calls for specific bean names in preference to this Map-based retrieval method. + * Aside from lazy instantiation benefits, this also avoids any exception suppression. * @param type the class or interface to match, or {@code null} for all concrete beans * @param includeNonSingletons whether to include prototype or scoped beans too * or just singletons (also applies to FactoryBeans) From 7a1dfe79a0fa9f632097cc01c3fe1e7ce3af41cc Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 21 Mar 2025 16:08:02 +0100 Subject: [PATCH 070/108] Polishing (aligned with 6.2.x) --- .../orm/jpa/DefaultJpaDialectTests.java | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/DefaultJpaDialectTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/DefaultJpaDialectTests.java index 57030f7b7031..14afaa64a832 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/DefaultJpaDialectTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/DefaultJpaDialectTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.EntityTransaction; import jakarta.persistence.OptimisticLockException; +import jakarta.persistence.PersistenceException; import org.junit.jupiter.api.Test; import org.springframework.transaction.TransactionDefinition; @@ -33,33 +34,37 @@ /** * @author Costin Leau * @author Phillip Webb + * @author Juergen Hoeller */ class DefaultJpaDialectTests { - private JpaDialect dialect = new DefaultJpaDialect(); + private final JpaDialect dialect = new DefaultJpaDialect(); - @Test - void testDefaultTransactionDefinition() { - DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); - definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ); - assertThatExceptionOfType(TransactionException.class).isThrownBy(() -> - dialect.beginTransaction(null, definition)); - } @Test void testDefaultBeginTransaction() throws Exception { TransactionDefinition definition = new DefaultTransactionDefinition(); EntityManager entityManager = mock(); EntityTransaction entityTx = mock(); - given(entityManager.getTransaction()).willReturn(entityTx); dialect.beginTransaction(entityManager, definition); } + @Test + void testCustomIsolationLevel() { + DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); + definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ); + + assertThatExceptionOfType(TransactionException.class).isThrownBy(() -> + dialect.beginTransaction(null, definition)); + } + @Test void testTranslateException() { - OptimisticLockException ex = new OptimisticLockException(); - assertThat(dialect.translateExceptionIfPossible(ex).getCause()).isEqualTo(EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(ex).getCause()); + PersistenceException ex = new OptimisticLockException(); + assertThat(dialect.translateExceptionIfPossible(ex)) + .isInstanceOf(JpaOptimisticLockingFailureException.class).hasCause(ex); } + } From 564d3f6f5ed9ecedfb10b72b51557804c169bbed Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 25 Mar 2025 00:24:23 +0100 Subject: [PATCH 071/108] Enable qualifier annotation tests including backported precedence tests See gh-34644 --- ...ierAnnotationAutowireBeanFactoryTests.java | 86 +++++++++++++++---- 1 file changed, 67 insertions(+), 19 deletions(-) diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java index f9ecb7e6c7d7..12d0fbdbf7e3 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -21,12 +21,13 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.util.ClassUtils; @@ -43,14 +44,17 @@ class QualifierAnnotationAutowireBeanFactoryTests { private static final String MARK = "mark"; + private final DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + + @Test void testAutowireCandidateDefaultWithIrrelevantDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition rbd = new RootBeanDefinition(Person.class, cavs, null); lbf.registerBeanDefinition(JUERGEN, rbd); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, new DependencyDescriptor(Person.class.getDeclaredField("name"), false))).isTrue(); @@ -60,12 +64,12 @@ void testAutowireCandidateDefaultWithIrrelevantDescriptor() throws Exception { @Test void testAutowireCandidateExplicitlyFalseWithIrrelevantDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition rbd = new RootBeanDefinition(Person.class, cavs, null); rbd.setAutowireCandidate(false); lbf.registerBeanDefinition(JUERGEN, rbd); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isFalse(); assertThat(lbf.isAutowireCandidate(JUERGEN, new DependencyDescriptor(Person.class.getDeclaredField("name"), false))).isFalse(); @@ -73,44 +77,46 @@ void testAutowireCandidateExplicitlyFalseWithIrrelevantDescriptor() throws Excep new DependencyDescriptor(Person.class.getDeclaredField("name"), true))).isFalse(); } - @Disabled @Test void testAutowireCandidateWithFieldDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(JUERGEN, person1); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); lbf.registerBeanDefinition(MARK, person2); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("qualified"), false); DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("nonqualified"), false); - assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); + assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); - assertThat(lbf.isAutowireCandidate(MARK, null)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, nonqualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isFalse(); } @Test void testAutowireCandidateExplicitlyFalseWithFieldDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); person.setAutowireCandidate(false); person.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(JUERGEN, person); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("qualified"), false); DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("nonqualified"), false); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isFalse(); assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isFalse(); assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isFalse(); @@ -118,56 +124,61 @@ void testAutowireCandidateExplicitlyFalseWithFieldDescriptor() throws Exception @Test void testAutowireCandidateWithShortClassName() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); person.addQualifier(new AutowireCandidateQualifier(ClassUtils.getShortName(TestQualifier.class))); lbf.registerBeanDefinition(JUERGEN, person); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("qualified"), false); DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("nonqualified"), false); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); } - @Disabled @Test void testAutowireCandidateWithConstructorDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(JUERGEN, person1); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); lbf.registerBeanDefinition(MARK, person2); + MethodParameter param = new MethodParameter(QualifiedTestBean.class.getDeclaredConstructor(Person.class), 0); DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor(param, false); param.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + assertThat(param.getParameterName()).isEqualTo("tpb"); - assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isFalse(); } - @Disabled @Test void testAutowireCandidateWithMethodDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(JUERGEN, person1); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); lbf.registerBeanDefinition(MARK, person2); + MethodParameter qualifiedParam = new MethodParameter(QualifiedTestBean.class.getDeclaredMethod("autowireQualified", Person.class), 0); MethodParameter nonqualifiedParam = @@ -175,37 +186,70 @@ void testAutowireCandidateWithMethodDescriptor() throws Exception { DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor(qualifiedParam, false); DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor(nonqualifiedParam, false); qualifiedParam.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); - assertThat(qualifiedParam.getParameterName()).isEqualTo("tpb"); nonqualifiedParam.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + + assertThat(qualifiedParam.getParameterName()).isEqualTo("tpb"); assertThat(nonqualifiedParam.getParameterName()).isEqualTo("tpb"); - assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); - assertThat(lbf.isAutowireCandidate(MARK, null)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, nonqualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isFalse(); } @Test void testAutowireCandidateWithMultipleCandidatesDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(JUERGEN, person1); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); person2.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(MARK, person2); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor( new MethodParameter(QualifiedTestBean.class.getDeclaredConstructor(Person.class), 0), false); + assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isTrue(); } + @Test + void autowireBeanByTypeWithQualifierPrecedence() throws Exception { + lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("testBean", bd); + lbf.registerBeanDefinition("spouse", bd2); + lbf.registerAlias("test", "testBean"); + + assertThat(lbf.resolveDependency(new DependencyDescriptor(getClass().getDeclaredField("testBean"), true), null)) + .isSameAs(lbf.getBean("spouse")); + } + + @Test + void autowireBeanByTypeWithQualifierPrecedenceInAncestor() throws Exception { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + parent.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + parent.registerBeanDefinition("test", bd); + parent.registerBeanDefinition("spouse", bd2); + parent.registerAlias("test", "testBean"); + + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(parent); + lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + + assertThat(lbf.resolveDependency(new DependencyDescriptor(getClass().getDeclaredField("testBean"), true), null)) + .isSameAs(lbf.getBean("spouse")); + } + @SuppressWarnings("unused") private static class QualifiedTestBean { @@ -247,4 +291,8 @@ public String getName() { private @interface TestQualifier { } + + @Qualifier("spouse") + private TestBean testBean; + } From 4645ce60c85f0dff28b8aff4a84d14514213a312 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 25 Mar 2025 19:27:37 +0100 Subject: [PATCH 072/108] Polishing --- .../objenesis/SpringObjenesis.java | 4 ++-- .../AbstractJmsListenerContainerFactory.java | 3 ++- .../AbstractMessageListenerContainer.java | 18 +++++++----------- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java b/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java index 031ef027f03c..84f5bf9cc667 100644 --- a/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java +++ b/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 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. @@ -69,7 +69,7 @@ public SpringObjenesis(InstantiatorStrategy strategy) { this.strategy = (strategy != null ? strategy : new StdInstantiatorStrategy()); // Evaluate the "spring.objenesis.ignore" property upfront... - if (SpringProperties.getFlag(SpringObjenesis.IGNORE_OBJENESIS_PROPERTY_NAME)) { + if (SpringProperties.getFlag(IGNORE_OBJENESIS_PROPERTY_NAME)) { this.worthTrying = Boolean.FALSE; } } diff --git a/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java b/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java index 32816c009275..b40101595a17 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java @@ -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"); * you may not use this file except in compliance with the License. @@ -209,6 +209,7 @@ public void setObservationRegistry(ObservationRegistry observationRegistry) { this.observationRegistry = observationRegistry; } + @Override public C createListenerContainer(JmsListenerEndpoint endpoint) { C instance = createContainerInstance(); diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractMessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractMessageListenerContainer.java index 035805fa8213..047449a9a40c 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractMessageListenerContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractMessageListenerContainer.java @@ -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"); * you may not use this file except in compliance with the License. @@ -102,7 +102,7 @@ * (i.e. after your business logic executed but before the JMS part got committed), * so duplicate message detection is just there to cover a corner case. *

  • Or wrap your entire processing with an XA transaction, covering the - * reception of the JMS message as well as the execution of the business logic in + * receipt of the JMS message as well as the execution of the business logic in * your message listener (including database operations etc). This is only * supported by {@link DefaultMessageListenerContainer}, through specifying * an external "transactionManager" (typically a @@ -152,7 +152,8 @@ public abstract class AbstractMessageListenerContainer extends AbstractJmsListen implements MessageListenerContainer { private static final boolean micrometerJakartaPresent = ClassUtils.isPresent( - "io.micrometer.jakarta9.instrument.jms.JmsInstrumentation", AbstractMessageListenerContainer.class.getClassLoader()); + "io.micrometer.jakarta9.instrument.jms.JmsInstrumentation", + AbstractMessageListenerContainer.class.getClassLoader()); @Nullable private volatile Object destination; @@ -170,14 +171,14 @@ public abstract class AbstractMessageListenerContainer extends AbstractJmsListen @Nullable private String subscriptionName; + private boolean pubSubNoLocal = false; + @Nullable private Boolean replyPubSubDomain; @Nullable private QosSettings replyQosSettings; - private boolean pubSubNoLocal = false; - @Nullable private MessageConverter messageConverter; @@ -500,12 +501,7 @@ public void setReplyPubSubDomain(boolean replyPubSubDomain) { */ @Override public boolean isReplyPubSubDomain() { - if (this.replyPubSubDomain != null) { - return this.replyPubSubDomain; - } - else { - return isPubSubDomain(); - } + return (this.replyPubSubDomain != null ? this.replyPubSubDomain : isPubSubDomain()); } /** From b7654dd984dc5402ed040cda6f131983df5fbfec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 27 Mar 2025 12:02:40 +0100 Subject: [PATCH 073/108] Make sure the generated values are available from a static context This commit updates the tests of property values code generated to invoke the generated code from a `static` context. This ensures that the test fails if that's not the case. This commit also updated LinkedHashMap handling that did suffer from that problem. Closes gh-34661 --- ...onPropertyValueCodeGeneratorDelegates.java | 4 +++- ...pertyValueCodeGeneratorDelegatesTests.java | 23 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java index c4188077839a..f76322a0ca16 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java @@ -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"); * you may not use this file except in compliance with the License. @@ -156,6 +156,8 @@ private CodeBlock generateLinkedHashMapCode(ValueCodeGenerator valueCodeGenerato .builder(SuppressWarnings.class) .addMember("value", "{\"rawtypes\", \"unchecked\"}") .build()); + method.addModifiers(javax.lang.model.element.Modifier.PRIVATE, + javax.lang.model.element.Modifier.STATIC); method.returns(Map.class); method.addStatement("$T map = new $T($L)", Map.class, LinkedHashMap.class, map.size()); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java index 0dafc56c1a23..9d69bed5aa49 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.InputStream; import java.io.OutputStream; +import java.lang.reflect.Method; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.temporal.ChronoUnit; @@ -28,7 +29,6 @@ import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; -import java.util.function.Supplier; import javax.lang.model.element.Modifier; @@ -54,7 +54,7 @@ import org.springframework.core.testfixture.aot.generate.value.ExampleClass$$GeneratedBy; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.MethodSpec; -import org.springframework.javapoet.ParameterizedTypeName; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -83,14 +83,23 @@ private void compile(Object value, BiConsumer result) { CodeBlock generatedCode = createValueCodeGenerator(generatedClass).generateCode(value); typeBuilder.set(type -> { type.addModifiers(Modifier.PUBLIC); - type.addSuperinterface( - ParameterizedTypeName.get(Supplier.class, Object.class)); - type.addMethod(MethodSpec.methodBuilder("get").addModifiers(Modifier.PUBLIC) + type.addMethod(MethodSpec.methodBuilder("get").addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(Object.class).addStatement("return $L", generatedCode).build()); }); generationContext.writeGeneratedContent(); TestCompiler.forSystem().with(generationContext).compile(compiled -> - result.accept(compiled.getInstance(Supplier.class).get(), compiled)); + result.accept(getGeneratedCodeReturnValue(compiled, generatedClass), compiled)); + } + + private static Object getGeneratedCodeReturnValue(Compiled compiled, GeneratedClass generatedClass) { + try { + Object instance = compiled.getInstance(Object.class, generatedClass.getName().reflectionName()); + Method get = ReflectionUtils.findMethod(instance.getClass(), "get"); + return get.invoke(null); + } + catch (Exception ex) { + throw new RuntimeException("Failed to invoke generated code '%s':".formatted(generatedClass.getName()), ex); + } } @Nested From 077721bdb436e4ddf0eb2f207213a89d92c2c9cd Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 25 Mar 2025 17:03:13 +0100 Subject: [PATCH 074/108] Remove outdated notes on forwarded headers. Closes gh-34625 --- .../annotation/MvcUriComponentsBuilder.java | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java index 64ad6f1db126..d2a4c7250fef 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java @@ -151,8 +151,6 @@ public static MvcUriComponentsBuilder relativeTo(UriComponentsBuilder baseUrl) { * Create a {@link UriComponentsBuilder} from the mapping of a controller class * and current request information including Servlet mapping. If the controller * contains multiple mappings, only the first one is used. - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param controllerType the controller to build a URI for * @return a UriComponentsBuilder instance (never {@code null}) */ @@ -165,8 +163,6 @@ public static UriComponentsBuilder fromController(Class controllerType) { * {@code UriComponentsBuilder} representing the base URL. This is useful * when using MvcUriComponentsBuilder outside the context of processing a * request or to apply a custom baseUrl not matching the current request. - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param builder the builder for the base URL; the builder will be cloned * and therefore not modified and may be re-used for further calls. * @param controllerType the controller to build a URI for @@ -192,8 +188,6 @@ public static UriComponentsBuilder fromController(@Nullable UriComponentsBuilder * Create a {@link UriComponentsBuilder} from the mapping of a controller * method and an array of method argument values. This method delegates * to {@link #fromMethod(Class, Method, Object...)}. - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param controllerType the controller * @param methodName the method name * @param args the argument values @@ -213,8 +207,6 @@ public static UriComponentsBuilder fromMethodName(Class controllerType, * accepts a {@code UriComponentsBuilder} representing the base URL. This is * useful when using MvcUriComponentsBuilder outside the context of processing * a request or to apply a custom baseUrl not matching the current request. - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param builder the builder for the base URL; the builder will be cloned * and therefore not modified and may be re-used for further calls. * @param controllerType the controller @@ -239,8 +231,6 @@ public static UriComponentsBuilder fromMethodName(UriComponentsBuilder builder, * {@link org.springframework.web.method.support.UriComponentsContributor * UriComponentsContributor}) while remaining argument values are ignored and * can be {@code null}. - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param controllerType the controller type * @param method the controller method * @param args argument values for the controller method @@ -257,8 +247,6 @@ public static UriComponentsBuilder fromMethod(Class controllerType, Method me * This is useful when using MvcUriComponentsBuilder outside the context of * processing a request or to apply a custom baseUrl not matching the * current request. - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param baseUrl the builder for the base URL; the builder will be cloned * and therefore not modified and may be re-used for further calls. * @param controllerType the controller type @@ -305,8 +293,6 @@ public static UriComponentsBuilder fromMethod(UriComponentsBuilder baseUrl, * controller.getAddressesForCountry("US") * builder = MvcUriComponentsBuilder.fromMethodCall(controller); * - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param info either the value returned from a "mock" controller * invocation or the "mock" controller itself after an invocation * @return a UriComponents instance @@ -327,8 +313,6 @@ public static UriComponentsBuilder fromMethodCall(Object info) { * {@code UriComponentsBuilder} representing the base URL. This is useful * when using MvcUriComponentsBuilder outside the context of processing a * request or to apply a custom baseUrl not matching the current request. - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param builder the builder for the base URL; the builder will be cloned * and therefore not modified and may be re-used for further calls. * @param info either the value returned from a "mock" controller @@ -354,8 +338,6 @@ public static UriComponentsBuilder fromMethodCall(UriComponentsBuilder builder, *

     	 * MvcUriComponentsBuilder.fromMethodCall(on(FooController.class).getFoo(1)).build();
     	 * 
    - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param controllerType the target controller */ public static T on(Class controllerType) { @@ -378,8 +360,6 @@ public static T on(Class controllerType) { * fooController.saveFoo(2, null); * builder = MvcUriComponentsBuilder.fromMethodCall(fooController); * - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param controllerType the target controller */ public static T controller(Class controllerType) { @@ -422,9 +402,6 @@ public static T controller(Class controllerType) { * *

    Note that it's not necessary to specify all arguments. Only the ones * required to prepare the URL, mainly {@code @RequestParam} and {@code @PathVariable}). - * - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param mappingName the mapping name * @return a builder to prepare the URI String * @throws IllegalArgumentException if the mapping name is not found or @@ -440,8 +417,6 @@ public static MethodArgumentBuilder fromMappingName(String mappingName) { * {@code UriComponentsBuilder} representing the base URL. This is useful * when using MvcUriComponentsBuilder outside the context of processing a * request or to apply a custom baseUrl not matching the current request. - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param builder the builder for the base URL; the builder will be cloned * and therefore not modified and may be re-used for further calls. * @param name the mapping name @@ -481,8 +456,6 @@ else if (handlerMethods.size() != 1) { /** * An alternative to {@link #fromController(Class)} for use with an instance * of this class created via a call to {@link #relativeTo}. - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @since 4.2 */ public UriComponentsBuilder withController(Class controllerType) { @@ -492,8 +465,6 @@ public UriComponentsBuilder withController(Class controllerType) { /** * An alternative to {@link #fromMethodName(Class, String, Object...)}} for * use with an instance of this class created via {@link #relativeTo}. - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @since 4.2 */ public UriComponentsBuilder withMethodName(Class controllerType, String methodName, Object... args) { @@ -503,8 +474,6 @@ public UriComponentsBuilder withMethodName(Class controllerType, String metho /** * An alternative to {@link #fromMethodCall(Object)} for use with an instance * of this class created via {@link #relativeTo}. - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @since 4.2 */ public UriComponentsBuilder withMethodCall(Object invocationInfo) { @@ -514,8 +483,6 @@ public UriComponentsBuilder withMethodCall(Object invocationInfo) { /** * An alternative to {@link #fromMappingName(String)} for use with an instance * of this class created via {@link #relativeTo}. - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @since 4.2 */ public MethodArgumentBuilder withMappingName(String mappingName) { @@ -525,8 +492,6 @@ public MethodArgumentBuilder withMappingName(String mappingName) { /** * An alternative to {@link #fromMethod(Class, Method, Object...)} * for use with an instance of this class created via {@link #relativeTo}. - *

    Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @since 4.2 */ public UriComponentsBuilder withMethod(Class controllerType, Method method, Object... args) { From d68962903661fd3e69c8b891b9eb5ca31fea8e68 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 29 Mar 2025 16:45:39 +0100 Subject: [PATCH 075/108] =?UTF-8?q?Support=20abstract=20@=E2=81=A0Configur?= =?UTF-8?q?ation=20classes=20without=20@=E2=81=A0Bean=20methods=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Historically, @⁠Configuration classes that did not declare @⁠Bean methods were allowed to be abstract. However, the changes made in 76a6b9ea79 introduced a regression that prevents such classes from being abstract, resulting in a BeanInstantiationException. This change in behavior is caused by the fact that such a @⁠Configuration class is no longer replaced by a concrete subclass created dynamically by CGLIB. This commit restores support for abstract @⁠Configuration classes without @⁠Bean methods by modifying the "no enhancement required" check in ConfigurationClassParser. See gh-34486 Closes gh-34663 (cherry picked from commit 044258f08554ac9e0b71491e1d3d18f6b1d1e449) --- .../annotation/ConfigurationClassParser.java | 5 ++-- .../ConfigurationClassPostProcessorTests.java | 24 ++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index be2188021713..96a8d2b46ccc 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -172,8 +172,9 @@ else if (bd instanceof AbstractBeanDefinition abstractBeanDef && abstractBeanDef } // Downgrade to lite (no enhancement) in case of no instance-level @Bean methods. - if (!configClass.hasNonStaticBeanMethods() && ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals( - bd.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE))) { + if (!configClass.getMetadata().isAbstract() && !configClass.hasNonStaticBeanMethods() && + ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals( + bd.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE))) { bd.setAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE, ConfigurationClassUtils.CONFIGURATION_CLASS_LITE); } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java index 806887a5f5ee..1c94837a7086 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java @@ -129,6 +129,22 @@ void enhancementIsPresentBecauseSingletonSemanticsAreRespectedUsingAsm() { assertThat(beanFactory.getDependentBeans("config")).contains("bar"); } + @Test // gh-34663 + void enhancementIsPresentForAbstractConfigClassWithoutBeanMethods() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(AbstractConfigWithoutBeanMethods.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + RootBeanDefinition beanDefinition = (RootBeanDefinition) beanFactory.getBeanDefinition("config"); + assertThat(beanDefinition.hasBeanClass()).isTrue(); + assertThat(beanDefinition.getBeanClass().getName()).contains(ClassUtils.CGLIB_CLASS_SEPARATOR); + Foo foo = beanFactory.getBean("foo", Foo.class); + Bar bar = beanFactory.getBean("bar", Bar.class); + assertThat(bar.foo).isSameAs(foo); + assertThat(beanFactory.getDependentBeans("foo")).contains("bar"); + String[] dependentsOfSingletonBeanConfig = beanFactory.getDependentBeans(SingletonBeanConfig.class.getName()); + assertThat(dependentsOfSingletonBeanConfig).containsOnly("foo", "bar"); + } + @Test void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalse() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(NonEnhancedSingletonBeanConfig.class)); @@ -181,7 +197,7 @@ void enhancementIsNotPresentForStaticMethodsUsingAsm() { assertThat(bar.foo).isNotSameAs(foo); } - @Test + @Test // gh-34486 void enhancementIsNotPresentWithEmptyConfig() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(EmptyConfig.class)); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); @@ -1187,6 +1203,12 @@ public static Bar bar() { } } + @Configuration + @Import(SingletonBeanConfig.class) + abstract static class AbstractConfigWithoutBeanMethods { + // This class intentionally does NOT declare @Bean methods. + } + @Configuration static final class EmptyConfig { } From b73ca608115d3f069b618fa82daf3ab78f67afa8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 31 Mar 2025 16:39:18 +0200 Subject: [PATCH 076/108] Only attempt load for CGLIB classes in AOT mode Closes gh-34677 (cherry picked from commit 743f32675d445a18224ec8968277f095376bae76) --- .../aop/framework/CglibAopProxy.java | 5 +++-- .../CglibSubclassingInstantiationStrategy.java | 9 +++++---- .../annotation/ConfigurationClassEnhancer.java | 17 +++++++---------- .../ConfigurationClassEnhancerTests.java | 4 ++-- .../annotation/MvcUriComponentsBuilder.java | 11 ++++++----- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java index 44bdf08db7ba..b7936da30da9 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java @@ -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"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import org.springframework.aop.RawTargetAccess; import org.springframework.aop.TargetSource; import org.springframework.aop.support.AopUtils; +import org.springframework.aot.AotDetector; import org.springframework.cglib.core.ClassLoaderAwareGeneratorStrategy; import org.springframework.cglib.core.CodeGenerationException; import org.springframework.cglib.core.SpringNamingPolicy; @@ -201,7 +202,7 @@ private Object buildProxy(@Nullable ClassLoader classLoader, boolean classOnly) enhancer.setSuperclass(proxySuperClass); enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised)); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(true); + enhancer.setAttemptLoad(enhancer.getUseCache() && AotDetector.useGeneratedArtifacts()); enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader)); Callback[] callbacks = getCallbacks(rootClass); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java index 551e0050a9ff..1f940d51ff70 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java @@ -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"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.aot.AotDetector; import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; @@ -153,7 +154,7 @@ public Class createEnhancedSubclass(RootBeanDefinition beanDefinition) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(beanDefinition.getBeanClass()); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(true); + enhancer.setAttemptLoad(AotDetector.useGeneratedArtifacts()); if (this.owner instanceof ConfigurableBeanFactory cbf) { ClassLoader cl = cbf.getBeanClassLoader(); enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(cl)); @@ -265,7 +266,7 @@ public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp /** * CGLIB MethodInterceptor to override methods, replacing them with a call - * to a generic MethodReplacer. + * to a generic {@link MethodReplacer}. */ private static class ReplaceOverrideMethodInterceptor extends CglibIdentitySupport implements MethodInterceptor { @@ -277,10 +278,10 @@ public ReplaceOverrideMethodInterceptor(RootBeanDefinition beanDefinition, BeanF } @Override + @Nullable public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable { ReplaceOverride ro = (ReplaceOverride) getBeanDefinition().getMethodOverrides().getOverride(method); Assert.state(ro != null, "ReplaceOverride not found"); - // TODO could cache if a singleton for minor performance optimization MethodReplacer mr = this.owner.getBean(ro.getMethodReplacerBeanName(), MethodReplacer.class); return mr.reimplement(obj, method, args); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index a05ee31cb724..9f9721b01982 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -26,6 +26,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.scope.ScopedProxyFactoryBean; +import org.springframework.aot.AotDetector; import org.springframework.asm.Opcodes; import org.springframework.asm.Type; import org.springframework.beans.factory.BeanDefinitionStoreException; @@ -138,26 +139,22 @@ private Enhancer newEnhancer(Class configSuperClass, @Nullable ClassLoader cl Enhancer enhancer = new Enhancer(); if (classLoader != null) { enhancer.setClassLoader(classLoader); + if (classLoader instanceof SmartClassLoader smartClassLoader && + smartClassLoader.isClassReloadable(configSuperClass)) { + enhancer.setUseCache(false); + } } enhancer.setSuperclass(configSuperClass); enhancer.setInterfaces(new Class[] {EnhancedConfiguration.class}); enhancer.setUseFactory(false); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(!isClassReloadable(configSuperClass, classLoader)); + enhancer.setAttemptLoad(enhancer.getUseCache() && AotDetector.useGeneratedArtifacts()); enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader)); enhancer.setCallbackFilter(CALLBACK_FILTER); enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes()); return enhancer; } - /** - * Checks whether the given configuration class is reloadable. - */ - private boolean isClassReloadable(Class configSuperClass, @Nullable ClassLoader classLoader) { - return (classLoader instanceof SmartClassLoader smartClassLoader && - smartClassLoader.isClassReloadable(configSuperClass)); - } - /** * Uses enhancer to generate a subclass of superclass, * ensuring that callbacks are registered for the new subclass. @@ -548,7 +545,7 @@ private Object createCglibProxyForFactoryBean(Object factoryBean, Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(factoryBean.getClass()); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(true); + enhancer.setAttemptLoad(AotDetector.useGeneratedArtifacts()); enhancer.setCallbackType(MethodInterceptor.class); // Ideally create enhanced FactoryBean proxy without constructor side effects, diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java index 052f27f43f22..ea73c24e7087 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java @@ -76,7 +76,7 @@ void withPublicClass() { classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader); assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass); - assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); } @Test @@ -126,7 +126,7 @@ void withNonPublicMethod() { classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass); - assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java index d2a4c7250fef..f5a915d054cd 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java @@ -29,6 +29,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.aot.AotDetector; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.cglib.core.SpringNamingPolicy; @@ -139,7 +140,7 @@ protected MvcUriComponentsBuilder(UriComponentsBuilder baseUrl) { /** * Create an instance of this class with a base URL. After that calls to one - * of the instance based {@code withXxx(...}} methods will create URLs relative + * of the instance based {@code withXxx(...)} methods will create URLs relative * to the given base URL. */ public static MvcUriComponentsBuilder relativeTo(UriComponentsBuilder baseUrl) { @@ -463,7 +464,7 @@ public UriComponentsBuilder withController(Class controllerType) { } /** - * An alternative to {@link #fromMethodName(Class, String, Object...)}} for + * An alternative to {@link #fromMethodName(Class, String, Object...)} for * use with an instance of this class created via {@link #relativeTo}. * @since 4.2 */ @@ -631,8 +632,8 @@ private static CompositeUriComponentsContributor getUriComponentsContributor() { private static String resolveEmbeddedValue(String value) { if (value.contains(SystemPropertyUtils.PLACEHOLDER_PREFIX)) { WebApplicationContext webApplicationContext = getWebApplicationContext(); - if (webApplicationContext != null - && webApplicationContext.getAutowireCapableBeanFactory() instanceof ConfigurableBeanFactory cbf) { + if (webApplicationContext != null && + webApplicationContext.getAutowireCapableBeanFactory() instanceof ConfigurableBeanFactory cbf) { String resolvedEmbeddedValue = cbf.resolveEmbeddedValue(value); if (resolvedEmbeddedValue != null) { return resolvedEmbeddedValue; @@ -793,7 +794,7 @@ else if (classLoader.getParent() == null) { enhancer.setSuperclass(controllerType); enhancer.setInterfaces(new Class[] {MethodInvocationInfo.class}); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(true); + enhancer.setAttemptLoad(AotDetector.useGeneratedArtifacts()); enhancer.setCallbackType(MethodInterceptor.class); Class proxyClass = enhancer.createClass(); From 6cc6ea1b2b250818e8c84bc7c679596b980abab2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 31 Mar 2025 17:24:33 +0200 Subject: [PATCH 077/108] Make jar caching configurable through setUseCaches Closes gh-34694 --- .../PathMatchingResourcePatternResolver.java | 30 ++++++++++++++++--- ...hMatchingResourcePatternResolverTests.java | 6 ++-- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java index 1529dcd97dfe..94d731955e1f 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java @@ -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"); * you may not use this file except in compliance with the License. @@ -206,6 +206,8 @@ */ public class PathMatchingResourcePatternResolver implements ResourcePatternResolver { + private static final Resource[] EMPTY_RESOURCE_ARRAY = {}; + private static final Log logger = LogFactory.getLog(PathMatchingResourcePatternResolver.class); /** @@ -248,6 +250,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol private PathMatcher pathMatcher = new AntPathMatcher(); + private boolean useCaches = true; + /** * Create a {@code PathMatchingResourcePatternResolver} with a @@ -315,6 +319,21 @@ public PathMatcher getPathMatcher() { return this.pathMatcher; } + /** + * Specify whether this resolver should use jar caches. Default is {@code true}. + *

    Switch this flag to {@code false} in order to avoid jar caching at the + * {@link JarURLConnection} level. + *

    Note that {@link JarURLConnection#setDefaultUseCaches} can be turned off + * independently. This resolver-level setting is designed to only enforce + * {@code JarURLConnection#setUseCaches(false)} if necessary but otherwise + * leaves the JVM-level default in place. + * @since 6.1.19 + * @see JarURLConnection#setUseCaches + */ + public void setUseCaches(boolean useCaches) { + this.useCaches = useCaches; + } + @Override public Resource getResource(String location) { @@ -338,7 +357,7 @@ public Resource[] getResources(String locationPattern) throws IOException { // all class path resources with the given name Collections.addAll(resources, findAllClassPathResources(locationPatternWithoutPrefix)); } - return resources.toArray(new Resource[0]); + return resources.toArray(EMPTY_RESOURCE_ARRAY); } else { // Generally only look for a pattern after a prefix here, @@ -371,7 +390,7 @@ protected Resource[] findAllClassPathResources(String location) throws IOExcepti if (logger.isTraceEnabled()) { logger.trace("Resolved class path location [" + path + "] to resources " + result); } - return result.toArray(new Resource[0]); + return result.toArray(EMPTY_RESOURCE_ARRAY); } /** @@ -607,7 +626,7 @@ else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) { if (logger.isTraceEnabled()) { logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result); } - return result.toArray(new Resource[0]); + return result.toArray(EMPTY_RESOURCE_ARRAY); } /** @@ -695,6 +714,9 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource, if (con instanceof JarURLConnection jarCon) { // Should usually be the case for traditional JAR files. + if (!this.useCaches) { + jarCon.setUseCaches(false); + } jarFile = jarCon.getJarFile(); jarFileUrl = jarCon.getJarFileURL().toExternalForm(); JarEntry jarEntry = jarCon.getJarEntry(); diff --git a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java index 30f1ac941f31..7edec3a65e58 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -108,9 +108,11 @@ void encodedHashtagInPath() throws IOException { Path rootDir = Paths.get("src/test/resources/custom%23root").toAbsolutePath(); URL root = new URL("file:" + rootDir + "/"); resolver = new PathMatchingResourcePatternResolver(new DefaultResourceLoader(new URLClassLoader(new URL[] {root}))); + resolver.setUseCaches(false); assertExactFilenames("classpath*:scanned/*.txt", "resource#test1.txt", "resource#test2.txt"); } + @Nested class WithHashtagsInTheirFilenames { @@ -332,7 +334,7 @@ private String getPath(Resource resource) { // Tests fail if we use resource.getURL().getPath(). They would also fail on macOS when // using resource.getURI().getPath() if the resource paths are not Unicode normalized. // - // On the JVM, all tests should pass when using resouce.getFile().getPath(); however, + // On the JVM, all tests should pass when using resource.getFile().getPath(); however, // we use FileSystemResource#getPath since this test class is sometimes run within a // GraalVM native image which cannot support Path#toFile. // From 2548d44f3b8fdbc0ede981815994d99c315a8443 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 1 Apr 2025 22:12:17 +0200 Subject: [PATCH 078/108] Include cause in MethodInvocationException message Closes gh-34691 (cherry picked from commit 203ca30a64df5131822b457aa0a4f98181eeb899) --- .../springframework/beans/MethodInvocationException.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java b/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java index 327643cbbf3f..fed9d15e2bc2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java +++ b/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2025 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. @@ -25,6 +25,7 @@ * analogous to an InvocationTargetException. * * @author Rod Johnson + * @author Juergen Hoeller */ @SuppressWarnings("serial") public class MethodInvocationException extends PropertyAccessException { @@ -41,7 +42,9 @@ public class MethodInvocationException extends PropertyAccessException { * @param cause the Throwable raised by the invoked method */ public MethodInvocationException(PropertyChangeEvent propertyChangeEvent, @Nullable Throwable cause) { - super(propertyChangeEvent, "Property '" + propertyChangeEvent.getPropertyName() + "' threw exception", cause); + super(propertyChangeEvent, + "Property '" + propertyChangeEvent.getPropertyName() + "' threw exception: " + cause, + cause); } @Override From 0a157faec15c12736f405645a2949834186a203c Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 1 Apr 2025 22:36:40 +0200 Subject: [PATCH 079/108] Polishing (backported from 6.2.x) --- .../support/DefaultLifecycleProcessor.java | 19 +++--- .../DefaultLifecycleProcessorTests.java | 62 ++++++++++++------- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java index bd351637b40a..cb9521bc539a 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java @@ -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"); * you may not use this file except in compliance with the License. @@ -61,7 +61,8 @@ * interactions on a {@link org.springframework.context.ConfigurableApplicationContext}. * *

    As of 6.1, this also includes support for JVM checkpoint/restore (Project CRaC) - * when the {@code org.crac:crac} dependency on the classpath. + * when the {@code org.crac:crac} dependency is on the classpath. All running beans + * will get stopped and restarted according to the CRaC checkpoint/restore callbacks. * * @author Mark Fisher * @author Juergen Hoeller @@ -379,7 +380,7 @@ else if (bean instanceof SmartLifecycle) { } - // overridable hooks + // Overridable hooks /** * Retrieve all applicable Lifecycle beans: all singletons that have already been created, @@ -493,11 +494,13 @@ else if (member.bean instanceof SmartLifecycle) { } } try { - latch.await(this.timeout, TimeUnit.MILLISECONDS); - if (latch.getCount() > 0 && !countDownBeanNames.isEmpty() && logger.isInfoEnabled()) { - logger.info("Shutdown phase " + this.phase + " ends with " + countDownBeanNames.size() + - " bean" + (countDownBeanNames.size() > 1 ? "s" : "") + - " still running after timeout of " + this.timeout + "ms: " + countDownBeanNames); + if (!latch.await(this.timeout, TimeUnit.MILLISECONDS)) { + // Count is still >0 after timeout + if (!countDownBeanNames.isEmpty() && logger.isInfoEnabled()) { + logger.info("Shutdown phase " + this.phase + " ends with " + countDownBeanNames.size() + + " bean" + (countDownBeanNames.size() > 1 ? "s" : "") + + " still running after timeout of " + this.timeout + "ms: " + countDownBeanNames); + } } } catch (InterruptedException ex) { diff --git a/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java b/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java index da666fea6ec4..3f1cf9bff85c 100644 --- a/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -54,10 +54,11 @@ void defaultLifecycleProcessorInstance() { @Test void customLifecycleProcessorInstance() { + StaticApplicationContext context = new StaticApplicationContext(); BeanDefinition beanDefinition = new RootBeanDefinition(DefaultLifecycleProcessor.class); beanDefinition.getPropertyValues().addPropertyValue("timeoutPerShutdownPhase", 1000); - StaticApplicationContext context = new StaticApplicationContext(); - context.registerBeanDefinition("lifecycleProcessor", beanDefinition); + context.registerBeanDefinition(StaticApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME, beanDefinition); + context.refresh(); LifecycleProcessor bean = context.getBean("lifecycleProcessor", LifecycleProcessor.class); Object contextLifecycleProcessor = new DirectFieldAccessor(context).getPropertyValue("lifecycleProcessor"); @@ -70,11 +71,12 @@ void customLifecycleProcessorInstance() { @Test void singleSmartLifecycleAutoStartup() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(true); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); + assertThat(bean.isRunning()).isFalse(); context.refresh(); assertThat(bean.isRunning()).isTrue(); @@ -114,12 +116,13 @@ void singleSmartLifecycleAutoStartupWithLazyInitFactoryBean() { @Test void singleSmartLifecycleAutoStartupWithFailingLifecycleBean() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(true); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); context.registerSingleton("failingBean", FailingLifecycleBean.class); + assertThat(bean.isRunning()).isFalse(); assertThatExceptionOfType(ApplicationContextException.class) .isThrownBy(context::refresh).withCauseInstanceOf(IllegalStateException.class); @@ -130,11 +133,12 @@ void singleSmartLifecycleAutoStartupWithFailingLifecycleBean() { @Test void singleSmartLifecycleWithoutAutoStartup() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(false); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); + assertThat(bean.isRunning()).isFalse(); context.refresh(); assertThat(bean.isRunning()).isFalse(); @@ -148,15 +152,16 @@ void singleSmartLifecycleWithoutAutoStartup() { @Test void singleSmartLifecycleAutoStartupWithNonAutoStartupDependency() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(true); TestSmartLifecycleBean dependency = TestSmartLifecycleBean.forStartupTests(1, startedBeans); dependency.setAutoStartup(false); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); context.getBeanFactory().registerSingleton("dependency", dependency); context.getBeanFactory().registerDependentBean("dependency", "bean"); + assertThat(bean.isRunning()).isFalse(); assertThat(dependency.isRunning()).isFalse(); context.refresh(); @@ -171,18 +176,19 @@ void singleSmartLifecycleAutoStartupWithNonAutoStartupDependency() { @Test void smartLifecycleGroupStartup() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forStartupTests(1, startedBeans); TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forStartupTests(2, startedBeans); TestSmartLifecycleBean bean3 = TestSmartLifecycleBean.forStartupTests(3, startedBeans); TestSmartLifecycleBean beanMax = TestSmartLifecycleBean.forStartupTests(Integer.MAX_VALUE, startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean3", bean3); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("bean2", bean2); context.getBeanFactory().registerSingleton("beanMax", beanMax); context.getBeanFactory().registerSingleton("bean1", bean1); + assertThat(beanMin.isRunning()).isFalse(); assertThat(bean1.isRunning()).isFalse(); assertThat(bean2.isRunning()).isFalse(); @@ -202,16 +208,17 @@ void smartLifecycleGroupStartup() { @Test void contextRefreshThenStartWithMixedBeans() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans); TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1); context.getBeanFactory().registerSingleton("smartBean1", smartBean1); context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2); context.getBeanFactory().registerSingleton("smartBean2", smartBean2); + assertThat(simpleBean1.isRunning()).isFalse(); assertThat(simpleBean2.isRunning()).isFalse(); assertThat(smartBean1.isRunning()).isFalse(); @@ -233,16 +240,17 @@ void contextRefreshThenStartWithMixedBeans() { @Test void contextRefreshThenStopAndRestartWithMixedBeans() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans); TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1); context.getBeanFactory().registerSingleton("smartBean1", smartBean1); context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2); context.getBeanFactory().registerSingleton("smartBean2", smartBean2); + assertThat(simpleBean1.isRunning()).isFalse(); assertThat(simpleBean2.isRunning()).isFalse(); assertThat(smartBean1.isRunning()).isFalse(); @@ -270,16 +278,17 @@ void contextRefreshThenStopAndRestartWithMixedBeans() { @Test void contextRefreshThenStopForRestartWithMixedBeans() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans); TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1); context.getBeanFactory().registerSingleton("smartBean1", smartBean1); context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2); context.getBeanFactory().registerSingleton("smartBean2", smartBean2); + assertThat(simpleBean1.isRunning()).isFalse(); assertThat(simpleBean2.isRunning()).isFalse(); assertThat(smartBean1.isRunning()).isFalse(); @@ -319,6 +328,7 @@ void contextRefreshThenStopForRestartWithMixedBeans() { @Test @EnabledForTestGroups(LONG_RUNNING) void smartLifecycleGroupShutdown() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 300, stoppedBeans); TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(3, 100, stoppedBeans); @@ -327,7 +337,6 @@ void smartLifecycleGroupShutdown() { TestSmartLifecycleBean bean5 = TestSmartLifecycleBean.forShutdownTests(2, 700, stoppedBeans); TestSmartLifecycleBean bean6 = TestSmartLifecycleBean.forShutdownTests(Integer.MAX_VALUE, 200, stoppedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(3, 200, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean1", bean1); context.getBeanFactory().registerSingleton("bean2", bean2); context.getBeanFactory().registerSingleton("bean3", bean3); @@ -335,6 +344,7 @@ void smartLifecycleGroupShutdown() { context.getBeanFactory().registerSingleton("bean5", bean5); context.getBeanFactory().registerSingleton("bean6", bean6); context.getBeanFactory().registerSingleton("bean7", bean7); + context.refresh(); context.stop(); assertThat(stoppedBeans).satisfiesExactly(hasPhase(Integer.MAX_VALUE), hasPhase(3), @@ -345,11 +355,12 @@ void smartLifecycleGroupShutdown() { @Test @EnabledForTestGroups(LONG_RUNNING) void singleSmartLifecycleShutdown() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forShutdownTests(99, 300, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); context.refresh(); + assertThat(bean.isRunning()).isTrue(); context.stop(); assertThat(bean.isRunning()).isFalse(); @@ -359,10 +370,11 @@ void singleSmartLifecycleShutdown() { @Test void singleLifecycleShutdown() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); Lifecycle bean = new TestLifecycleBean(null, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); + context.refresh(); assertThat(bean.isRunning()).isFalse(); bean.start(); @@ -375,6 +387,7 @@ void singleLifecycleShutdown() { @Test void mixedShutdown() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); Lifecycle bean1 = TestLifecycleBean.forShutdownTests(stoppedBeans); Lifecycle bean2 = TestSmartLifecycleBean.forShutdownTests(500, 200, stoppedBeans); @@ -383,7 +396,6 @@ void mixedShutdown() { Lifecycle bean5 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); Lifecycle bean6 = TestSmartLifecycleBean.forShutdownTests(-1, 100, stoppedBeans); Lifecycle bean7 = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 300, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean1", bean1); context.getBeanFactory().registerSingleton("bean2", bean2); context.getBeanFactory().registerSingleton("bean3", bean3); @@ -391,6 +403,7 @@ void mixedShutdown() { context.getBeanFactory().registerSingleton("bean5", bean5); context.getBeanFactory().registerSingleton("bean6", bean6); context.getBeanFactory().registerSingleton("bean7", bean7); + context.refresh(); assertThat(bean2.isRunning()).isTrue(); assertThat(bean3.isRunning()).isTrue(); @@ -418,17 +431,18 @@ void mixedShutdown() { @Test void dependencyStartedFirstEvenIfItsPhaseIsHigher() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forStartupTests(2, startedBeans); TestSmartLifecycleBean bean99 = TestSmartLifecycleBean.forStartupTests(99, startedBeans); TestSmartLifecycleBean beanMax = TestSmartLifecycleBean.forStartupTests(Integer.MAX_VALUE, startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("bean2", bean2); context.getBeanFactory().registerSingleton("bean99", bean99); context.getBeanFactory().registerSingleton("beanMax", beanMax); context.getBeanFactory().registerDependentBean("bean99", "bean2"); + context.refresh(); assertThat(beanMin.isRunning()).isTrue(); assertThat(bean2.isRunning()).isTrue(); @@ -446,6 +460,7 @@ void dependencyStartedFirstEvenIfItsPhaseIsHigher() { @Test @EnabledForTestGroups(LONG_RUNNING) void dependentShutdownFirstEvenIfItsPhaseIsLower() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 100, stoppedBeans); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); @@ -453,7 +468,6 @@ void dependentShutdownFirstEvenIfItsPhaseIsLower() { TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(2, 300, stoppedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(7, 400, stoppedBeans); TestSmartLifecycleBean beanMax = TestSmartLifecycleBean.forShutdownTests(Integer.MAX_VALUE, 400, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("bean1", bean1); context.getBeanFactory().registerSingleton("bean2", bean2); @@ -461,6 +475,7 @@ void dependentShutdownFirstEvenIfItsPhaseIsLower() { context.getBeanFactory().registerSingleton("bean99", bean99); context.getBeanFactory().registerSingleton("beanMax", beanMax); context.getBeanFactory().registerDependentBean("bean99", "bean2"); + context.refresh(); assertThat(beanMin.isRunning()).isTrue(); assertThat(bean1.isRunning()).isTrue(); @@ -486,17 +501,18 @@ void dependentShutdownFirstEvenIfItsPhaseIsLower() { @Test void dependencyStartedFirstAndIsSmartLifecycle() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanNegative = TestSmartLifecycleBean.forStartupTests(-99, startedBeans); TestSmartLifecycleBean bean99 = TestSmartLifecycleBean.forStartupTests(99, startedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forStartupTests(7, startedBeans); TestLifecycleBean simpleBean = TestLifecycleBean.forStartupTests(startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanNegative", beanNegative); context.getBeanFactory().registerSingleton("bean7", bean7); context.getBeanFactory().registerSingleton("bean99", bean99); context.getBeanFactory().registerSingleton("simpleBean", simpleBean); context.getBeanFactory().registerDependentBean("bean7", "simpleBean"); + context.refresh(); context.stop(); startedBeans.clear(); @@ -514,6 +530,7 @@ void dependencyStartedFirstAndIsSmartLifecycle() { @Test @EnabledForTestGroups(LONG_RUNNING) void dependentShutdownFirstAndIsSmartLifecycle() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 400, stoppedBeans); TestSmartLifecycleBean beanNegative = TestSmartLifecycleBean.forShutdownTests(-99, 100, stoppedBeans); @@ -521,7 +538,6 @@ void dependentShutdownFirstAndIsSmartLifecycle() { TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(2, 300, stoppedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(7, 400, stoppedBeans); TestLifecycleBean simpleBean = TestLifecycleBean.forShutdownTests(stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("beanNegative", beanNegative); context.getBeanFactory().registerSingleton("bean1", bean1); @@ -529,6 +545,7 @@ void dependentShutdownFirstAndIsSmartLifecycle() { context.getBeanFactory().registerSingleton("bean7", bean7); context.getBeanFactory().registerSingleton("simpleBean", simpleBean); context.getBeanFactory().registerDependentBean("simpleBean", "beanNegative"); + context.refresh(); assertThat(beanMin.isRunning()).isTrue(); assertThat(beanNegative.isRunning()).isTrue(); @@ -551,15 +568,16 @@ void dependentShutdownFirstAndIsSmartLifecycle() { @Test void dependencyStartedFirstButNotSmartLifecycle() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forStartupTests(7, startedBeans); TestLifecycleBean simpleBean = TestLifecycleBean.forStartupTests(startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("bean7", bean7); context.getBeanFactory().registerSingleton("simpleBean", simpleBean); context.getBeanFactory().registerDependentBean("simpleBean", "beanMin"); + context.refresh(); assertThat(beanMin.isRunning()).isTrue(); assertThat(bean7.isRunning()).isTrue(); @@ -572,19 +590,20 @@ void dependencyStartedFirstButNotSmartLifecycle() { @Test @EnabledForTestGroups(LONG_RUNNING) void dependentShutdownFirstButNotSmartLifecycle() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); TestLifecycleBean simpleBean = TestLifecycleBean.forShutdownTests(stoppedBeans); TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(2, 300, stoppedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(7, 400, stoppedBeans); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 400, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("bean1", bean1); context.getBeanFactory().registerSingleton("bean2", bean2); context.getBeanFactory().registerSingleton("bean7", bean7); context.getBeanFactory().registerSingleton("simpleBean", simpleBean); context.getBeanFactory().registerDependentBean("bean2", "simpleBean"); + context.refresh(); assertThat(beanMin.isRunning()).isTrue(); assertThat(bean1.isRunning()).isTrue(); @@ -611,6 +630,7 @@ private Consumer hasPhase(int phase) { }; } + private static class TestLifecycleBean implements Lifecycle { private final CopyOnWriteArrayList startedBeans; From 86ea7003f7890f633e0b95e9955da9ebe9ddced8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 1 Apr 2025 23:42:56 +0200 Subject: [PATCH 080/108] Upgrade to Groovy 4.0.26, Jetty 12.0.18, Apache HttpClient 5.4.3, Checkstyle 10.22 --- .../springframework/build/CheckstyleConventions.java | 4 ++-- framework-platform/framework-platform.gradle | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java index f955f18ce6fa..6b9e022fee31 100644 --- a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java @@ -50,7 +50,7 @@ public void apply(Project project) { project.getPlugins().apply(CheckstylePlugin.class); project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g")); CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class); - checkstyle.setToolVersion("10.21.2"); + checkstyle.setToolVersion("10.22.0"); checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle")); String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion(); DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies(); @@ -64,7 +64,7 @@ private static void configureNoHttpPlugin(Project project) { NoHttpExtension noHttp = project.getExtensions().getByType(NoHttpExtension.class); noHttp.setAllowlistFile(project.file("src/nohttp/allowlist.lines")); noHttp.getSource().exclude("**/test-output/**", "**/.settings/**", - "**/.classpath", "**/.project", "**/.gradle/**", "**/node_modules/**"); + "**/.classpath", "**/.project", "**/.gradle/**", "**/node_modules/**", "buildSrc/build/**"); List buildFolders = List.of("bin", "build", "out"); project.allprojects(subproject -> { Path rootPath = project.getRootDir().toPath(); diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 87ec00e5075e..4afa918e169e 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -13,11 +13,11 @@ dependencies { api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) api(platform("io.projectreactor:reactor-bom:2023.0.16")) api(platform("io.rsocket:rsocket-bom:1.1.5")) - api(platform("org.apache.groovy:groovy-bom:4.0.24")) + api(platform("org.apache.groovy:groovy-bom:4.0.26")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) api(platform("org.assertj:assertj-bom:3.27.3")) - api(platform("org.eclipse.jetty:jetty-bom:12.0.17")) - api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.17")) + api(platform("org.eclipse.jetty:jetty-bom:12.0.18")) + api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.18")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3")) api(platform("org.junit:junit-bom:5.10.5")) @@ -101,8 +101,8 @@ dependencies { api("org.apache.derby:derby:10.16.1.1") api("org.apache.derby:derbyclient:10.16.1.1") api("org.apache.derby:derbytools:10.16.1.1") - api("org.apache.httpcomponents.client5:httpclient5:5.4.2") - api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.3") + api("org.apache.httpcomponents.client5:httpclient5:5.4.3") + api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.4") api("org.apache.poi:poi-ooxml:5.2.5") api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28") api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.28") From 6717fca4ec29fec10d246ec15c925ea73b8bafdc Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 2 Apr 2025 23:41:43 +0200 Subject: [PATCH 081/108] Explicitly use original ClassLoader in case of package visibility Closes gh-34684 (cherry picked from commit 6bb964e2d0eda488948a454e1434f6e25ccf5a77) --- .../ConfigurationClassEnhancer.java | 26 +++++++++++++++++++ .../ConfigurationClassEnhancerTests.java | 4 +-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index 9f9721b01982..07ff8d8c2c65 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -116,6 +116,12 @@ public Class enhance(Class configClass, @Nullable ClassLoader classLoader) boolean classLoaderMismatch = (classLoader != null && classLoader != configClass.getClassLoader()); if (classLoaderMismatch && classLoader instanceof SmartClassLoader smartClassLoader) { classLoader = smartClassLoader.getOriginalClassLoader(); + classLoaderMismatch = (classLoader != configClass.getClassLoader()); + } + // Use original ClassLoader if config class relies on package visibility + if (classLoaderMismatch && reliesOnPackageVisibility(configClass)) { + classLoader = configClass.getClassLoader(); + classLoaderMismatch = false; } Enhancer enhancer = newEnhancer(configClass, classLoader); Class enhancedClass = createClass(enhancer, classLoaderMismatch); @@ -132,6 +138,26 @@ public Class enhance(Class configClass, @Nullable ClassLoader classLoader) } } + /** + * Checks whether the given config class relies on package visibility, + * either for the class itself or for any of its {@code @Bean} methods. + */ + private boolean reliesOnPackageVisibility(Class configSuperClass) { + int mod = configSuperClass.getModifiers(); + if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) { + return true; + } + for (Method method : ReflectionUtils.getDeclaredMethods(configSuperClass)) { + if (BeanAnnotationHelper.isBeanAnnotated(method)) { + mod = method.getModifiers(); + if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) { + return true; + } + } + } + return false; + } + /** * Creates a new CGLIB {@link Enhancer} instance. */ diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java index ea73c24e7087..2dc8ba872a3b 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java @@ -111,7 +111,7 @@ void withNonPublicMethod() { ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader()); Class enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass); - assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); classLoader = new OverridingClassLoader(getClass().getClassLoader()); enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); @@ -126,7 +126,7 @@ void withNonPublicMethod() { classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass); - assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); } From 82b7fcb2cc1eabc7ae4613ec3148d5f6c6879b7a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 10 Apr 2025 18:30:45 +0200 Subject: [PATCH 082/108] Try loadClass on LinkageError in case of ClassLoader mismatch See gh-34677 (cherry picked from commit 7f2c1f447f62207aa6eb1f42d4f8f07434bd8913) --- .../cglib/core/ReflectUtils.java | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java index 102f333c074b..fd4077b78b19 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java @@ -527,15 +527,26 @@ public static Class defineClass(String className, byte[] b, ClassLoader loader, c = lookup.defineClass(b); } catch (LinkageError | IllegalAccessException ex) { - throw new CodeGenerationException(ex) { - @Override - public String getMessage() { - return "ClassLoader mismatch for [" + contextClass.getName() + - "]: JVM should be started with --add-opens=java.base/java.lang=ALL-UNNAMED " + - "for ClassLoader.defineClass to be accessible on " + loader.getClass().getName() + - "; consider co-locating the affected class in that target ClassLoader instead."; + if (ex instanceof LinkageError) { + // Could be a ClassLoader mismatch with the class pre-existing in a + // parent ClassLoader -> try loadClass before giving up completely. + try { + c = contextClass.getClassLoader().loadClass(className); } - }; + catch (ClassNotFoundException cnfe) { + } + } + if (c == null) { + throw new CodeGenerationException(ex) { + @Override + public String getMessage() { + return "ClassLoader mismatch for [" + contextClass.getName() + + "]: JVM should be started with --add-opens=java.base/java.lang=ALL-UNNAMED " + + "for ClassLoader.defineClass to be accessible on " + loader.getClass().getName() + + "; consider co-locating the affected class in that target ClassLoader instead."; + } + }; + } } catch (Throwable ex) { throw new CodeGenerationException(ex); From 0a71df7aee6f7530259122eaa624baaf71873075 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 10 Apr 2025 18:55:34 +0200 Subject: [PATCH 083/108] Polishing --- .../beans/factory/support/DefaultSingletonBeanRegistry.java | 6 +++--- .../java/org/springframework/core/SpringProperties.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java index 81e442404973..7d29ebc9ca5b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -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"); * you may not use this file except in compliance with the License. @@ -178,13 +178,13 @@ public Object getSingleton(String beanName) { */ @Nullable protected Object getSingleton(String beanName, boolean allowEarlyReference) { - // Quick check for existing instance without full singleton lock + // Quick check for existing instance without full singleton lock. Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { synchronized (this.singletonObjects) { - // Consistent creation of early reference within full singleton lock + // Consistent creation of early reference within full singleton lock. singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { singletonObject = this.earlySingletonObjects.get(beanName); diff --git a/spring-core/src/main/java/org/springframework/core/SpringProperties.java b/spring-core/src/main/java/org/springframework/core/SpringProperties.java index 3cb1cd51ebf5..ac4a72776ddf 100644 --- a/spring-core/src/main/java/org/springframework/core/SpringProperties.java +++ b/spring-core/src/main/java/org/springframework/core/SpringProperties.java @@ -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"); * you may not use this file except in compliance with the License. @@ -117,7 +117,7 @@ public static String getProperty(String key) { * @param key the property key */ public static void setFlag(String key) { - localProperties.put(key, Boolean.TRUE.toString()); + localProperties.setProperty(key, Boolean.TRUE.toString()); } /** From d8791128852673d50ffe7c9b2cd19c209eb14e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 15 Apr 2025 10:04:45 +0200 Subject: [PATCH 084/108] Upgrade to github-changelog-generator 0.0.12 Closes gh-34756 --- .github/actions/create-github-release/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/create-github-release/action.yml b/.github/actions/create-github-release/action.yml index a33b0b9b43da..56bf0a4a6738 100644 --- a/.github/actions/create-github-release/action.yml +++ b/.github/actions/create-github-release/action.yml @@ -11,7 +11,7 @@ runs: using: composite steps: - name: Generate Changelog - uses: spring-io/github-changelog-generator@185319ad7eaa75b0e8e72e4b6db19c8b2cb8c4c1 #v0.0.11 + uses: spring-io/github-changelog-generator@86958813a62af8fb223b3fd3b5152035504bcb83 #v0.0.12 with: config-file: .github/actions/create-github-release/changelog-generator.yml milestone: ${{ inputs.milestone }} From a5a7d63dab4f746cf12fa2603b915734be5644f3 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 17 Apr 2025 09:05:28 +0200 Subject: [PATCH 085/108] Next development version (v6.1.20-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e5b336dea287..10010c946b9f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.1.19-SNAPSHOT +version=6.1.20-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From 89ae20b9ff228c18eaae89e6a26e99a32894ee56 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 22 Apr 2025 10:41:39 +0100 Subject: [PATCH 086/108] Add ignoreCase variants to PatternMatchUtils See gh-34801 --- .../util/PatternMatchUtils.java | 61 ++++++++++++++++--- .../util/PatternMatchUtilsTests.java | 21 ++++++- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java index 9f050351f0b6..f2bbacd0003f 100644 --- a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java +++ b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java @@ -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"); * you may not use this file except in compliance with the License. @@ -37,13 +37,24 @@ public abstract class PatternMatchUtils { * @return whether the String matches the given pattern */ public static boolean simpleMatch(@Nullable String pattern, @Nullable String str) { + return simpleMatch(pattern, str, false); + } + + /** + * Variant of {@link #simpleMatch(String, String)} that ignores upper/lower case. + */ + public static boolean simpleMatchIgnoreCase(@Nullable String pattern, @Nullable String str) { + return simpleMatch(pattern, str, true); + } + + private static boolean simpleMatch(@Nullable String pattern, @Nullable String str, boolean ignoreCase) { if (pattern == null || str == null) { return false; } int firstIndex = pattern.indexOf('*'); if (firstIndex == -1) { - return pattern.equals(str); + return (ignoreCase ? pattern.equalsIgnoreCase(str) : pattern.equals(str)); } if (firstIndex == 0) { @@ -52,25 +63,43 @@ public static boolean simpleMatch(@Nullable String pattern, @Nullable String str } int nextIndex = pattern.indexOf('*', 1); if (nextIndex == -1) { - return str.endsWith(pattern.substring(1)); + String part = pattern.substring(1); + return (ignoreCase ? StringUtils.endsWithIgnoreCase(str, part) : str.endsWith(part)); } String part = pattern.substring(1, nextIndex); if (part.isEmpty()) { - return simpleMatch(pattern.substring(nextIndex), str); + return simpleMatch(pattern.substring(nextIndex), str, ignoreCase); } - int partIndex = str.indexOf(part); + int partIndex = indexOf(str, part, 0, ignoreCase); while (partIndex != -1) { - if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()))) { + if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()), ignoreCase)) { return true; } - partIndex = str.indexOf(part, partIndex + 1); + partIndex = indexOf(str, part, partIndex + 1, ignoreCase); } return false; } return (str.length() >= firstIndex && - pattern.startsWith(str.substring(0, firstIndex)) && - simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex))); + checkStartsWith(pattern, str, firstIndex, ignoreCase) && + simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex), ignoreCase)); + } + + private static boolean checkStartsWith(String pattern, String str, int index, boolean ignoreCase) { + String part = str.substring(0, index); + return (ignoreCase ? StringUtils.startsWithIgnoreCase(pattern, part) : pattern.startsWith(part)); + } + + private static int indexOf(String str, String otherStr, int startIndex, boolean ignoreCase) { + if (!ignoreCase) { + return str.indexOf(otherStr, startIndex); + } + for (int i = startIndex; i <= (str.length() - otherStr.length()); i++) { + if (str.regionMatches(true, i, otherStr, 0, otherStr.length())) { + return i; + } + } + return -1; } /** @@ -94,4 +123,18 @@ public static boolean simpleMatch(@Nullable String[] patterns, @Nullable String return false; } + /** + * Variant of {@link #simpleMatch(String[], String)} that ignores upper/lower case. + */ + public static boolean simpleMatchIgnoreCase(@Nullable String[] patterns, @Nullable String str) { + if (patterns != null) { + for (String pattern : patterns) { + if (simpleMatch(pattern, str, true)) { + return true; + } + } + } + return false; + } + } diff --git a/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java b/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java index b4618c090d78..d2ef171a30f5 100644 --- a/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -53,18 +53,22 @@ void trivial() { assertMatches(new String[] { null, "" }, ""); assertMatches(new String[] { null, "123" }, "123"); assertMatches(new String[] { null, "*" }, "123"); + + testMixedCaseMatch("abC", "Abc"); } @Test void startsWith() { assertMatches("get*", "getMe"); assertDoesNotMatch("get*", "setMe"); + testMixedCaseMatch("geT*", "GetMe"); } @Test void endsWith() { assertMatches("*Test", "getMeTest"); assertDoesNotMatch("*Test", "setMe"); + testMixedCaseMatch("*TeSt", "getMeTesT"); } @Test @@ -74,6 +78,10 @@ void between() { assertMatches("*stuff*", "stuffTest"); assertMatches("*stuff*", "getstuff"); assertMatches("*stuff*", "stuff"); + testMixedCaseMatch("*stuff*", "getStuffTest"); + testMixedCaseMatch("*stuff*", "StuffTest"); + testMixedCaseMatch("*stuff*", "getStuff"); + testMixedCaseMatch("*stuff*", "Stuff"); } @Test @@ -82,6 +90,8 @@ void startsEnds() { assertMatches("on*Event", "onEvent"); assertDoesNotMatch("3*3", "3"); assertMatches("3*3", "33"); + testMixedCaseMatch("on*Event", "OnMyEvenT"); + testMixedCaseMatch("on*Event", "OnEvenT"); } @Test @@ -122,18 +132,27 @@ void patternVariants() { private void assertMatches(String pattern, String str) { assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isTrue(); + assertThat(PatternMatchUtils.simpleMatchIgnoreCase(pattern, str)).isTrue(); } private void assertDoesNotMatch(String pattern, String str) { assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isFalse(); + assertThat(PatternMatchUtils.simpleMatchIgnoreCase(pattern, str)).isFalse(); + } + + private void testMixedCaseMatch(String pattern, String str) { + assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isFalse(); + assertThat(PatternMatchUtils.simpleMatchIgnoreCase(pattern, str)).isTrue(); } private void assertMatches(String[] patterns, String str) { assertThat(PatternMatchUtils.simpleMatch(patterns, str)).isTrue(); + assertThat(PatternMatchUtils.simpleMatchIgnoreCase(patterns, str)).isTrue(); } private void assertDoesNotMatch(String[] patterns, String str) { assertThat(PatternMatchUtils.simpleMatch(patterns, str)).isFalse(); + assertThat(PatternMatchUtils.simpleMatchIgnoreCase(patterns, str)).isFalse(); } } From daee9f1242264215876e67f6ef43b117195385c6 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 25 Apr 2025 12:08:39 +0200 Subject: [PATCH 087/108] =?UTF-8?q?Reinstate=20the=20@=E2=81=A0Inject=20Te?= =?UTF-8?q?chnology=20Compatibility=20Kit=20(TCK)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In commit 05ebca8677, the `public` modifier was removed from the SpringAtInjectTckTests class, which prevents it from being run as a JUnit 3 test class. To address that, this commit adds the missing `public` modifier as well as a a code comment to help prevent this from happening again. In addition, this commit updates spring-context.gradle to ensure that the JUnit Vintage test engine is always applied. However, that Gradle configuration is unfortunately ignored due to how our TestConventions class has been implemented. Thus, that issue will have to be addressed separately. Closes gh-34800 (cherry picked from commit e384389790652d4e052a5b9dc3f2ed0d724fa040) --- spring-context/spring-context.gradle | 7 +++++++ .../context/annotation/jsr330/SpringAtInjectTckTests.java | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/spring-context/spring-context.gradle b/spring-context/spring-context.gradle index 0256d6bfdbfb..6fa5e956f540 100644 --- a/spring-context/spring-context.gradle +++ b/spring-context/spring-context.gradle @@ -58,3 +58,10 @@ dependencies { testRuntimeOnly("org.javamoney:moneta") testRuntimeOnly("org.junit.vintage:junit-vintage-engine") // for @Inject TCK } + +test { + description = "Runs JUnit Jupiter tests and the @Inject TCK via JUnit Vintage." + useJUnitPlatform { + includeEngines "junit-jupiter", "junit-vintage" + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java b/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java index f9d0574d552a..bc36d8fd46f6 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -38,7 +38,8 @@ * @author Juergen Hoeller * @since 3.0 */ -class SpringAtInjectTckTests { +// WARNING: This class MUST be public, since it is based on JUnit 3. +public class SpringAtInjectTckTests { @SuppressWarnings("unchecked") public static Test suite() { From 71f27256381d72170f9c6d38eea3032ceb24f030 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 28 Apr 2025 16:12:45 +0200 Subject: [PATCH 088/108] Try loadClass on LinkageError in case of same ClassLoader as well Closes gh-34824 (cherry picked from commit 4172581f1b720ad9b0985c82ed4ea676055506bb) --- .../cglib/core/ReflectUtils.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java index fd4077b78b19..bbbdcbaeee32 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java @@ -463,10 +463,21 @@ public static Class defineClass(String className, byte[] b, ClassLoader loader, c = lookup.defineClass(b); } catch (LinkageError | IllegalArgumentException ex) { - // in case of plain LinkageError (class already defined) - // or IllegalArgumentException (class in different package): - // fall through to traditional ClassLoader.defineClass below - t = ex; + if (ex instanceof LinkageError) { + // Could be a ClassLoader mismatch with the class pre-existing in a + // parent ClassLoader -> try loadClass before giving up completely. + try { + c = contextClass.getClassLoader().loadClass(className); + } + catch (ClassNotFoundException cnfe) { + } + } + if (c == null) { + // in case of plain LinkageError (class already defined) + // or IllegalArgumentException (class in different package): + // fall through to traditional ClassLoader.defineClass below + t = ex; + } } catch (Throwable ex) { throw new CodeGenerationException(ex); From a5b0399a1d6f3e89ae3bbfeb0b13142ecaddb4e9 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 28 Apr 2025 16:13:04 +0200 Subject: [PATCH 089/108] Polishing (cherry picked from commit d0b186a1c7eb9f3e4565fa4441d4f931d5975995) --- .../org/springframework/util/backoff/ExponentialBackOff.java | 5 ++++- .../java/org/springframework/util/backoff/FixedBackOff.java | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java b/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java index 5cd39685fa28..79836518a165 100644 --- a/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java +++ b/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java @@ -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"); * you may not use this file except in compliance with the License. @@ -85,6 +85,7 @@ public class ExponentialBackOff implements BackOff { */ public static final int DEFAULT_MAX_ATTEMPTS = Integer.MAX_VALUE; + private long initialInterval = DEFAULT_INITIAL_INTERVAL; private double multiplier = DEFAULT_MULTIPLIER; @@ -204,6 +205,7 @@ public int getMaxAttempts() { return this.maxAttempts; } + @Override public BackOffExecution start() { return new ExponentialBackOffExecution(); @@ -225,6 +227,7 @@ public String toString() { .toString(); } + private class ExponentialBackOffExecution implements BackOffExecution { private long currentInterval = -1; diff --git a/spring-core/src/main/java/org/springframework/util/backoff/FixedBackOff.java b/spring-core/src/main/java/org/springframework/util/backoff/FixedBackOff.java index b4d80c481227..9695077362b1 100644 --- a/spring-core/src/main/java/org/springframework/util/backoff/FixedBackOff.java +++ b/spring-core/src/main/java/org/springframework/util/backoff/FixedBackOff.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2025 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. @@ -35,6 +35,7 @@ public class FixedBackOff implements BackOff { */ public static final long UNLIMITED_ATTEMPTS = Long.MAX_VALUE; + private long interval = DEFAULT_INTERVAL; private long maxAttempts = UNLIMITED_ATTEMPTS; @@ -86,6 +87,7 @@ public long getMaxAttempts() { return this.maxAttempts; } + @Override public BackOffExecution start() { return new FixedBackOffExecution(); From 5b5e2b68767537f204d8392201497805ce6562d7 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 5 May 2025 14:38:30 +0200 Subject: [PATCH 090/108] Fix HttpClient 5.3.x request config compatibility As of gh-33806, the HttpComponents client request factory is forward compatible with the 5.4+ versions of that library for configuring HTTP request configuration. This change would not tkae into account configuration set at the Spring level because it would consider the default `RequestConfig` instance as a custom one. This commit ensures that Spring sets its own configuration if no custom configuration was set for all supported HttpComponents generations. Fixes gh-34854 --- .../HttpComponentsClientHttpRequestFactory.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java index 2b93d88abdd7..06458b15f3c1 100644 --- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java @@ -216,9 +216,8 @@ public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IO context = HttpClientContext.create(); } - // Request configuration not set in the context - if (!(context instanceof HttpClientContext clientContext && clientContext.getRequestConfig() != null) && - context.getAttribute(HttpClientContext.REQUEST_CONFIG) == null) { + // No custom request configuration was set + if (!hasCustomRequestConfig(context)) { RequestConfig config = null; // Use request configuration given by the user, when available if (httpRequest instanceof Configurable configurable) { @@ -237,6 +236,18 @@ public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IO return new HttpComponentsClientHttpRequest(client, httpRequest, context); } + @SuppressWarnings("deprecation") // HttpClientContext.REQUEST_CONFIG + private static boolean hasCustomRequestConfig(HttpContext context) { + if (context instanceof HttpClientContext clientContext) { + // Prior to 5.4, the default config was set to RequestConfig.DEFAULT + // As of 5.4, it is set to null + RequestConfig requestConfig = clientContext.getRequestConfig(); + return requestConfig != null && !requestConfig.equals(RequestConfig.DEFAULT); + } + // Prior to 5.4, the config was stored as an attribute + return context.getAttribute(HttpClientContext.REQUEST_CONFIG) != null; + } + /** * Create a default {@link RequestConfig} to use with the given client. From cbb94193fe9f11d1af8b8958292b0edc8451cd4c Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 13 May 2025 16:08:03 +0200 Subject: [PATCH 091/108] Clarify CompositePropertySource behavior for EnumerablePropertySource contract Closes gh-34886 (cherry picked from commit 6a9444473f1aad080bf659563e56cc2bbd8f9512) --- .../springframework/core/env/CompositePropertySource.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java index 4c0553f0e4a3..1723a54bd329 100644 --- a/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java @@ -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"); * you may not use this file except in compliance with the License. @@ -33,7 +33,10 @@ * *

    As of Spring 4.1.2, this class extends {@link EnumerablePropertySource} instead * of plain {@link PropertySource}, exposing {@link #getPropertyNames()} based on the - * accumulated property names from all contained sources (as far as possible). + * accumulated property names from all contained sources - and failing with an + * {@code IllegalStateException} against any non-{@code EnumerablePropertySource}. + * When used through the {@code EnumerablePropertySource} contract, all contained + * sources are expected to be of type {@code EnumerablePropertySource} as well. * * @author Chris Beams * @author Juergen Hoeller From d5fca0d2c5d96b1a59a5814aa38c5f3b15238301 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 13 May 2025 16:57:19 +0200 Subject: [PATCH 092/108] Upgrade to Jetty 12.0.21, Netty 4.1.121, Apache HttpClient 5.4.4, Checkstyle 10.23.1 --- .../org/springframework/build/CheckstyleConventions.java | 2 +- framework-platform/framework-platform.gradle | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java index 6b9e022fee31..b35b3e3b5df6 100644 --- a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java @@ -50,7 +50,7 @@ public void apply(Project project) { project.getPlugins().apply(CheckstylePlugin.class); project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g")); CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class); - checkstyle.setToolVersion("10.22.0"); + checkstyle.setToolVersion("10.23.1"); checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle")); String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion(); DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies(); diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 4afa918e169e..0e3d371af43a 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -9,15 +9,15 @@ javaPlatform { dependencies { api(platform("com.fasterxml.jackson:jackson-bom:2.15.4")) api(platform("io.micrometer:micrometer-bom:1.12.12")) - api(platform("io.netty:netty-bom:4.1.119.Final")) + api(platform("io.netty:netty-bom:4.1.121.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) api(platform("io.projectreactor:reactor-bom:2023.0.16")) api(platform("io.rsocket:rsocket-bom:1.1.5")) api(platform("org.apache.groovy:groovy-bom:4.0.26")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) api(platform("org.assertj:assertj-bom:3.27.3")) - api(platform("org.eclipse.jetty:jetty-bom:12.0.18")) - api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.18")) + api(platform("org.eclipse.jetty:jetty-bom:12.0.21")) + api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.21")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3")) api(platform("org.junit:junit-bom:5.10.5")) @@ -101,7 +101,7 @@ dependencies { api("org.apache.derby:derby:10.16.1.1") api("org.apache.derby:derbyclient:10.16.1.1") api("org.apache.derby:derbytools:10.16.1.1") - api("org.apache.httpcomponents.client5:httpclient5:5.4.3") + api("org.apache.httpcomponents.client5:httpclient5:5.4.4") api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.4") api("org.apache.poi:poi-ooxml:5.2.5") api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28") From 6ab4c84bd528d9480071d3dec4ff0b4904dbbb2f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 14 May 2025 15:25:15 +0200 Subject: [PATCH 093/108] Upgrade to Reactor 2023.0.18 Closes gh-34899 --- 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 0e3d371af43a..8ce8cb73b30d 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -11,7 +11,7 @@ dependencies { api(platform("io.micrometer:micrometer-bom:1.12.12")) api(platform("io.netty:netty-bom:4.1.121.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2023.0.16")) + api(platform("io.projectreactor:reactor-bom:2023.0.18")) api(platform("io.rsocket:rsocket-bom:1.1.5")) api(platform("org.apache.groovy:groovy-bom:4.0.26")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) From f93132b11ef6aa5718d20a05846828659c082fe8 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 14 May 2025 14:51:14 +0100 Subject: [PATCH 094/108] Add missing @since tags in PatternMatchUtils See: gh-34801 --- .../main/java/org/springframework/util/PatternMatchUtils.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java index f2bbacd0003f..f0f0070567d0 100644 --- a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java +++ b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java @@ -42,6 +42,7 @@ public static boolean simpleMatch(@Nullable String pattern, @Nullable String str /** * Variant of {@link #simpleMatch(String, String)} that ignores upper/lower case. + * @since 6.1.20 */ public static boolean simpleMatchIgnoreCase(@Nullable String pattern, @Nullable String str) { return simpleMatch(pattern, str, true); @@ -125,6 +126,7 @@ public static boolean simpleMatch(@Nullable String[] patterns, @Nullable String /** * Variant of {@link #simpleMatch(String[], String)} that ignores upper/lower case. + * @since 6.1.20 */ public static boolean simpleMatchIgnoreCase(@Nullable String[] patterns, @Nullable String str) { if (patterns != null) { From edfcc6ffb188e4614ec9b212e3208b666981851c Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 14 Apr 2025 11:51:17 +0100 Subject: [PATCH 095/108] Make use of PatternMatchUtils ignoreCase option Closes gh-34801 --- .../validation/DataBinder.java | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/validation/DataBinder.java b/spring-context/src/main/java/org/springframework/validation/DataBinder.java index 981577568f9c..50ef24efc79e 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -27,7 +27,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -543,15 +542,13 @@ public String[] getAllowedFields() { *

    Mark fields as disallowed, for example to avoid unwanted * modifications by malicious users when binding HTTP request parameters. *

    Supports {@code "xxx*"}, {@code "*xxx"}, {@code "*xxx*"}, and - * {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), as - * well as direct equality. - *

    The default implementation of this method stores disallowed field patterns - * in {@linkplain PropertyAccessorUtils#canonicalPropertyName(String) canonical} - * form. As of Spring Framework 5.2.21, the default implementation also transforms - * disallowed field patterns to {@linkplain String#toLowerCase() lowercase} to - * support case-insensitive pattern matching in {@link #isAllowed}. Subclasses - * which override this method must therefore take both of these transformations - * into account. + * {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), + * as well as direct equality. + *

    The default implementation of this method stores disallowed field + * patterns in {@linkplain PropertyAccessorUtils#canonicalPropertyName(String) + * canonical} form, and subsequently pattern matching in {@link #isAllowed} + * is case-insensitive. Subclasses that override this method must therefore + * take this transformation into account. *

    More sophisticated matching can be implemented by overriding the * {@link #isAllowed} method. *

    Alternatively, specify a list of allowed field patterns. @@ -569,8 +566,7 @@ public void setDisallowedFields(@Nullable String... disallowedFields) { else { String[] fieldPatterns = new String[disallowedFields.length]; for (int i = 0; i < fieldPatterns.length; i++) { - String field = PropertyAccessorUtils.canonicalPropertyName(disallowedFields[i]); - fieldPatterns[i] = field.toLowerCase(Locale.ROOT); + fieldPatterns[i] = PropertyAccessorUtils.canonicalPropertyName(disallowedFields[i]); } this.disallowedFields = fieldPatterns; } @@ -1140,9 +1136,9 @@ protected void checkAllowedFields(MutablePropertyValues mpvs) { * Determine if the given field is allowed for binding. *

    Invoked for each passed-in property value. *

    Checks for {@code "xxx*"}, {@code "*xxx"}, {@code "*xxx*"}, and - * {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), as - * well as direct equality, in the configured lists of allowed field patterns - * and disallowed field patterns. + * {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), + * as well as direct equality, in the configured lists of allowed field + * patterns and disallowed field patterns. *

    Matching against allowed field patterns is case-sensitive; whereas, * matching against disallowed field patterns is case-insensitive. *

    A field matching a disallowed pattern will not be accepted even if it @@ -1158,8 +1154,13 @@ protected void checkAllowedFields(MutablePropertyValues mpvs) { protected boolean isAllowed(String field) { String[] allowed = getAllowedFields(); String[] disallowed = getDisallowedFields(); - return ((ObjectUtils.isEmpty(allowed) || PatternMatchUtils.simpleMatch(allowed, field)) && - (ObjectUtils.isEmpty(disallowed) || !PatternMatchUtils.simpleMatch(disallowed, field.toLowerCase(Locale.ROOT)))); + if (!ObjectUtils.isEmpty(allowed) && !PatternMatchUtils.simpleMatch(allowed, field)) { + return false; + } + if (!ObjectUtils.isEmpty(disallowed)) { + return !PatternMatchUtils.simpleMatchIgnoreCase(disallowed, field); + } + return true; } /** From 9a89654a64c7abf74f9533581df994ddcab91a46 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 15 May 2025 10:32:10 +0200 Subject: [PATCH 096/108] Next development version (v6.1.21-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 10010c946b9f..5b48d2b7d26a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.1.20-SNAPSHOT +version=6.1.21-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From 30bee4db0f480ac6ff91d1d2ae3bfedf7104e81b Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 26 May 2025 17:21:31 +0200 Subject: [PATCH 097/108] Polish Javadoc for MockPropertySource (cherry picked from commit 6a6abac0030e4798d154b6be92a7aa3dacc6ac47) --- .../core/testfixture/env/MockPropertySource.java | 6 +++--- .../org/springframework/mock/env/MockPropertySource.java | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java index a4cbd1f22ed1..f1404079bb29 100644 --- a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java @@ -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"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ * a user-provided {@link Properties} object, or if omitted during construction, * the implementation will initialize its own. * - * The {@link #setProperty} and {@link #withProperty} methods are exposed for + *

    The {@link #setProperty} and {@link #withProperty} methods are exposed for * convenience, for example: *

      * {@code
    @@ -95,7 +95,7 @@ public void setProperty(String name, Object value) {
     
     	/**
     	 * Convenient synonym for {@link #setProperty} that returns the current instance.
    -	 * Useful for method chaining and fluent-style use.
    +	 * 

    Useful for method chaining and fluent-style use. * @return this {@link MockPropertySource} instance */ public MockPropertySource withProperty(String name, Object value) { diff --git a/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java b/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java index 3ef180fcf22b..8b2c6e1d7767 100644 --- a/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java +++ b/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java @@ -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"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ * a user-provided {@link Properties} object, or if omitted during construction, * the implementation will initialize its own. * - * The {@link #setProperty} and {@link #withProperty} methods are exposed for + *

    The {@link #setProperty} and {@link #withProperty} methods are exposed for * convenience, for example: *

      * {@code
    @@ -36,7 +36,7 @@
      *
      * @author Chris Beams
      * @since 3.1
    - * @see org.springframework.mock.env.MockEnvironment
    + * @see MockEnvironment
      */
     public class MockPropertySource extends PropertiesPropertySource {
     
    @@ -95,7 +95,7 @@ public void setProperty(String name, Object value) {
     
     	/**
     	 * Convenient synonym for {@link #setProperty} that returns the current instance.
    -	 * Useful for method chaining and fluent-style use.
    +	 * 

    Useful for method chaining and fluent-style use. * @return this {@link MockPropertySource} instance */ public MockPropertySource withProperty(String name, Object value) { From c0a9da65f442d0d3760cb2b61c913744e71c391f Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 26 May 2025 17:29:12 +0200 Subject: [PATCH 098/108] Support only Object property values in MockEnvironment test fixture The setProperty() and withProperty() methods in MockEnvironment were originally introduced with (String, String) signatures; however, they should have always had (String, Object) signatures in order to comply with the MockPropertySource and PropertySource APIs. To address that, this commit changes the signatures of these methods so that they only accept Object values for properties. NOTE: this commit only affects the internal MockEnvironment used as a test fixture. This commit does not affect the official, public MockEnvironment implementation in spring-test. See gh-34947 See gh-34948 (cherry picked from commit d78264756e3a82ff0cf31a7f4a2f3f97dac52f5d) --- .../mock/env/MockEnvironment.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java b/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java index 9a533f563570..cf7887bc8b1b 100644 --- a/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java +++ b/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java @@ -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"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ * @author Chris Beams * @author Sam Brannen * @since 3.2 - * @see org.springframework.core.testfixture.env.MockPropertySource + * @see MockPropertySource */ public class MockEnvironment extends AbstractEnvironment { @@ -44,19 +44,21 @@ public MockEnvironment() { /** * Set a property on the underlying {@link MockPropertySource} for this environment. + * @see MockPropertySource#setProperty(String, Object) */ - public void setProperty(String key, String value) { - this.propertySource.setProperty(key, value); + public void setProperty(String name, Object value) { + this.propertySource.setProperty(name, value); } /** - * Convenient synonym for {@link #setProperty} that returns the current instance. - * Useful for method chaining and fluent-style use. + * Convenient synonym for {@link #setProperty(String, Object)} that returns + * the current instance. + *

    Useful for method chaining and fluent-style use. * @return this {@link MockEnvironment} instance - * @see MockPropertySource#withProperty + * @see MockPropertySource#withProperty(String, Object) */ - public MockEnvironment withProperty(String key, String value) { - setProperty(key, value); + public MockEnvironment withProperty(String name, Object value) { + setProperty(name, value); return this; } From 59ffbd7a598af7cc7ef3efa81061cb06a06371e5 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 26 May 2025 18:07:29 +0200 Subject: [PATCH 099/108] Test conversion support in PropertySourcesPlaceholderConfigurer This commit introduces a "regression test" which demonstrates that PropertySourcesPlaceholderConfigurer uses the ConversionService from the Environment. See gh-34936 --- ...ertySourcesPlaceholderConfigurerTests.java | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java b/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java index 33e27414c46f..f46be41d82d0 100644 --- a/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; @@ -72,6 +73,33 @@ void replacementFromEnvironmentProperties() { assertThat(ppc.getAppliedPropertySources()).isNotNull(); } + @Test // gh-34936 + void replacementFromEnvironmentPropertiesWithConversion() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + + record Point(int x, int y) { + } + + Converter pointToStringConverter = + point -> "(%d,%d)".formatted(point.x, point.y); + + DefaultConversionService conversionService = new DefaultConversionService(); + conversionService.addConverter(Point.class, String.class, pointToStringConverter); + + MockEnvironment env = new MockEnvironment(); + env.setConversionService(conversionService); + env.setProperty("my.name", new Point(4,5)); + + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("(4,5)"); + } + @Test void localPropertiesViaResource() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); From 3b6becac014f55e896de7e28344e2863ff90425a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 27 May 2025 09:47:29 +0200 Subject: [PATCH 100/108] Check for package-visible constructor in case of ClassLoader mismatch Closes gh-34950 (cherry picked from commit 15d1455acb919ee7c474f9376a32b9213106e9af) --- .../ConfigurationClassEnhancer.java | 11 +++++- .../ConfigurationClassEnhancerTests.java | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index 07ff8d8c2c65..b64def92af4d 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -16,6 +16,7 @@ package org.springframework.context.annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -139,14 +140,20 @@ public Class enhance(Class configClass, @Nullable ClassLoader classLoader) } /** - * Checks whether the given config class relies on package visibility, - * either for the class itself or for any of its {@code @Bean} methods. + * Checks whether the given config class relies on package visibility, either for + * the class and any of its constructors or for any of its {@code @Bean} methods. */ private boolean reliesOnPackageVisibility(Class configSuperClass) { int mod = configSuperClass.getModifiers(); if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) { return true; } + for (Constructor ctor : configSuperClass.getDeclaredConstructors()) { + mod = ctor.getModifiers(); + if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) { + return true; + } + } for (Method method : ReflectionUtils.getDeclaredMethods(configSuperClass)) { if (BeanAnnotationHelper.isBeanAnnotated(method)) { mod = method.getModifiers(); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java index 2dc8ba872a3b..38779f588cc9 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java @@ -104,6 +104,31 @@ void withNonPublicClass() { assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); } + @Test + void withNonPublicConstructor() { + ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer(); + + ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader()); + Class enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader); + assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new OverridingClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader); + assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new CustomSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader); + assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader); + assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + } + @Test void withNonPublicMethod() { ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer(); @@ -160,6 +185,19 @@ public String myBean() { } + @Configuration + public static class MyConfigWithNonPublicConstructor { + + MyConfigWithNonPublicConstructor() { + } + + @Bean + public String myBean() { + return "bean"; + } + } + + @Configuration public static class MyConfigWithNonPublicMethod { From a876bb41af418c35ff3409146e29c28e4ed1b619 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:11:55 +0200 Subject: [PATCH 101/108] Polish WebSession support and tests (cherry picked from commit 222702f750b7f98ebabdefd8eea7025849ba8207) --- .../web/server/ServerWebExchange.java | 14 +++---- .../session/InMemoryWebSessionStore.java | 28 +++++++------ .../web/server/session/WebSessionManager.java | 8 ++-- .../web/server/session/WebSessionStore.java | 4 +- .../session/InMemoryWebSessionStoreTests.java | 42 ++++++++++--------- 5 files changed, 50 insertions(+), 46 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java b/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java index da7a3bfb520d..b086e62f5bed 100644 --- a/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java +++ b/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java @@ -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"); * you may not use this file except in compliance with the License. @@ -107,12 +107,12 @@ default T getAttributeOrDefault(String name, T defaultValue) { } /** - * Return the web session for the current request. Always guaranteed to - * return an instance either matching to the session id requested by the - * client, or with a new session id either because the client did not - * specify one or because the underlying session had expired. Use of this - * method does not automatically create a session. See {@link WebSession} - * for more details. + * Return the web session for the current request. + *

    Always guaranteed to return either an instance matching the session id + * requested by the client, or a new session either because the client did not + * specify a session id or because the underlying session expired. + *

    Use of this method does not automatically create a session. See + * {@link WebSession} for more details. */ Mono getSession(); diff --git a/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java b/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java index ecc1557d6a82..6e76d76e9c11 100644 --- a/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java +++ b/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java @@ -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"); * you may not use this file except in compliance with the License. @@ -79,10 +79,10 @@ public int getMaxSessions() { } /** - * Configure the {@link Clock} to use to set lastAccessTime on every created - * session and to calculate if it is expired. - *

    This may be useful to align to different timezone or to set the clock - * back in a test, e.g. {@code Clock.offset(clock, Duration.ofMinutes(-31))} + * Configure the {@link Clock} to use to set the {@code lastAccessTime} on + * every created session and to calculate if the session has expired. + *

    This may be useful to align to different time zones or to set the clock + * back in a test, for example, {@code Clock.offset(clock, Duration.ofMinutes(-31))} * in order to simulate session expiration. *

    By default this is {@code Clock.system(ZoneId.of("GMT"))}. * @param clock the clock to use @@ -94,16 +94,17 @@ public void setClock(Clock clock) { } /** - * Return the configured clock for session lastAccessTime calculations. + * Return the configured clock for session {@code lastAccessTime} calculations. */ public Clock getClock() { return this.clock; } /** - * Return the map of sessions with an {@link Collections#unmodifiableMap - * unmodifiable} wrapper. This could be used for management purposes, to - * list active sessions, invalidate expired ones, etc. + * Return an {@linkplain Collections#unmodifiableMap unmodifiable} copy of the + * map of sessions. + *

    This could be used for management purposes, to list active sessions, + * to invalidate expired sessions, etc. * @since 5.0.8 */ public Map getSessions() { @@ -157,10 +158,11 @@ public Mono updateLastAccessTime(WebSession session) { } /** - * Check for expired sessions and remove them. Typically such checks are - * kicked off lazily during calls to {@link #createWebSession() create} or - * {@link #retrieveSession retrieve}, no less than 60 seconds apart. - * This method can be called to force a check at a specific time. + * Check for expired sessions and remove them. + *

    Typically such checks are kicked off lazily during calls to + * {@link #createWebSession()} or {@link #retrieveSession}, no less than 60 + * seconds apart. + *

    This method can be called to force a check at a specific time. * @since 5.0.8 */ public void removeExpiredSessions() { diff --git a/spring-web/src/main/java/org/springframework/web/server/session/WebSessionManager.java b/spring-web/src/main/java/org/springframework/web/server/session/WebSessionManager.java index 67648eb4e8f9..88da7d186dec 100644 --- a/spring-web/src/main/java/org/springframework/web/server/session/WebSessionManager.java +++ b/spring-web/src/main/java/org/springframework/web/server/session/WebSessionManager.java @@ -32,10 +32,10 @@ public interface WebSessionManager { /** - * Return the {@link WebSession} for the given exchange. Always guaranteed - * to return an instance either matching to the session id requested by the - * client, or a new session either because the client did not specify one - * or because the underlying session expired. + * Return the {@link WebSession} for the given exchange. + *

    Always guaranteed to return either an instance matching the session id + * requested by the client, or a new session either because the client did not + * specify a session id or because the underlying session expired. * @param exchange the current exchange * @return promise for the WebSession */ diff --git a/spring-web/src/main/java/org/springframework/web/server/session/WebSessionStore.java b/spring-web/src/main/java/org/springframework/web/server/session/WebSessionStore.java index 15eeb1284255..9a4faa315049 100644 --- a/spring-web/src/main/java/org/springframework/web/server/session/WebSessionStore.java +++ b/spring-web/src/main/java/org/springframework/web/server/session/WebSessionStore.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -43,7 +43,7 @@ public interface WebSessionStore { * Return the WebSession for the given id. *

    Note: This method should perform an expiration check, * and if it has expired remove the session and return empty. This method - * should also update the lastAccessTime of retrieved sessions. + * should also update the {@code lastAccessTime} of retrieved sessions. * @param sessionId the session to load * @return the session, or an empty {@code Mono} */ diff --git a/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java b/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java index 0bf488eda75a..726b3a2e530c 100644 --- a/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; -import java.util.Map; import java.util.stream.IntStream; import org.junit.jupiter.api.Test; @@ -35,10 +34,11 @@ * Tests for {@link InMemoryWebSessionStore}. * * @author Rob Winch + * @author Sam Brannen */ class InMemoryWebSessionStoreTests { - private InMemoryWebSessionStore store = new InMemoryWebSessionStore(); + private final InMemoryWebSessionStore store = new InMemoryWebSessionStore(); @Test @@ -59,7 +59,7 @@ void startsSessionImplicitly() { } @Test // gh-24027, gh-26958 - public void createSessionDoesNotBlock() { + void createSessionDoesNotBlock() { this.store.createWebSession() .doOnNext(session -> assertThat(Schedulers.isInNonBlockingThread()).isTrue()) .block(); @@ -103,7 +103,7 @@ void lastAccessTimeIsUpdatedOnRetrieve() { } @Test // SPR-17051 - public void sessionInvalidatedBeforeSave() { + void sessionInvalidatedBeforeSave() { // Request 1 creates session WebSession session1 = this.store.createWebSession().block(); assertThat(session1).isNotNull(); @@ -132,33 +132,31 @@ public void sessionInvalidatedBeforeSave() { @Test void expirationCheckPeriod() { - - DirectFieldAccessor accessor = new DirectFieldAccessor(this.store); - Map sessions = (Map) accessor.getPropertyValue("sessions"); - assertThat(sessions).isNotNull(); - // Create 100 sessions - IntStream.range(0, 100).forEach(i -> insertSession()); - assertThat(sessions).hasSize(100); + IntStream.rangeClosed(1, 100).forEach(i -> insertSession()); + assertNumSessions(100); - // Force a new clock (31 min later), don't use setter which would clean expired sessions + // Force a new clock (31 min later). Don't use setter which would clean expired sessions. + DirectFieldAccessor accessor = new DirectFieldAccessor(this.store); accessor.setPropertyValue("clock", Clock.offset(this.store.getClock(), Duration.ofMinutes(31))); - assertThat(sessions).hasSize(100); + assertNumSessions(100); - // Create 1 more which forces a time-based check (clock moved forward) + // Create 1 more which forces a time-based check (clock moved forward). insertSession(); - assertThat(sessions).hasSize(1); + assertNumSessions(1); } @Test void maxSessions() { + this.store.setMaxSessions(10); - IntStream.range(0, 10000).forEach(i -> insertSession()); - assertThatIllegalStateException().isThrownBy( - this::insertSession) - .withMessage("Max sessions limit reached: 10000"); + IntStream.rangeClosed(1, 10).forEach(i -> insertSession()); + assertThatIllegalStateException() + .isThrownBy(this::insertSession) + .withMessage("Max sessions limit reached: 10"); } + private WebSession insertSession() { WebSession session = this.store.createWebSession().block(); assertThat(session).isNotNull(); @@ -167,4 +165,8 @@ private WebSession insertSession() { return session; } + private void assertNumSessions(int numSessions) { + assertThat(store.getSessions()).hasSize(numSessions); + } + } From 59d2895c8289642ba233de93f38e7a109fc971c1 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:13:54 +0200 Subject: [PATCH 102/108] Fix InMemoryWebSessionStoreTests.startsSessionImplicitly() test (cherry picked from commit 3c265e104476d7c2ea18a36ce326febc1f3613f7) --- .../web/server/session/InMemoryWebSessionStoreTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java b/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java index 726b3a2e530c..7847cc3537c1 100644 --- a/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java @@ -53,7 +53,8 @@ void startsSessionExplicitly() { void startsSessionImplicitly() { WebSession session = this.store.createWebSession().block(); assertThat(session).isNotNull(); - session.start(); + // We intentionally do not invoke start(). + // session.start(); session.getAttributes().put("foo", "bar"); assertThat(session.isStarted()).isTrue(); } From cd44efaf687ce9a13e28e5569ee9c4fd4ee134f6 Mon Sep 17 00:00:00 2001 From: Mohammad Saeed Nouri Date: Sun, 8 Jun 2025 15:44:06 +0330 Subject: [PATCH 103/108] Allow update of existing WebSession after max sessions limit is reached Previously, when saving a WebSession, the system did not check whether the session ID already existed. As a result, even if the session being saved was an update to an existing one, it was incorrectly treated as a new session, and a "maximum sessions exceeded" error was triggered. This fix ensures that if a WebSession with the same ID already exists, it will be updated rather than counted as a new session, thereby preventing unnecessary session limit violations. See gh-35013 Closes gh-35018 Signed-off-by: Mohammad Saeed Nouri (cherry picked from commit c04902fefbe54e89d423addbf8d724870cf09213) --- .../session/InMemoryWebSessionStore.java | 2 +- .../session/InMemoryWebSessionStoreTests.java | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java b/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java index 6e76d76e9c11..e76b133363e9 100644 --- a/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java +++ b/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java @@ -280,7 +280,7 @@ public Mono save() { private void checkMaxSessionsLimit() { if (sessions.size() >= maxSessions) { expiredSessionChecker.removeExpiredSessions(clock.instant()); - if (sessions.size() >= maxSessions) { + if (sessions.size() >= maxSessions && !sessions.containsKey(this.getId())) { throw new IllegalStateException("Max sessions limit reached: " + sessions.size()); } } diff --git a/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java b/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java index 7847cc3537c1..baeac73d0088 100644 --- a/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test; import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; import org.springframework.beans.DirectFieldAccessor; import org.springframework.web.server.WebSession; @@ -157,6 +158,25 @@ void maxSessions() { .withMessage("Max sessions limit reached: 10"); } + @Test + void updateSession() { + WebSession oneWebSession = insertSession(); + + StepVerifier.create(oneWebSession.save()) + .expectComplete() + .verify(); + } + + @Test + void updateSession_whenMaxSessionsReached() { + WebSession onceWebSession = insertSession(); + IntStream.range(1, 10000).forEach(i -> insertSession()); + + StepVerifier.create(onceWebSession.save()) + .expectComplete() + .verify(); + } + private WebSession insertSession() { WebSession session = this.store.createWebSession().block(); From 8ecc553696cec1cc33a7c4c7e5748d0915f3c9b3 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:31:30 +0200 Subject: [PATCH 104/108] Polish contribution See gh-35013 See gh-35018 (cherry picked from commit 4d2cc4ae9720ae70f6a2eacad14fb31cd2edf7dd) --- .../session/InMemoryWebSessionStore.java | 2 +- .../session/InMemoryWebSessionStoreTests.java | 33 +++++++++++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java b/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java index e76b133363e9..b862a6de81d3 100644 --- a/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java +++ b/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java @@ -280,7 +280,7 @@ public Mono save() { private void checkMaxSessionsLimit() { if (sessions.size() >= maxSessions) { expiredSessionChecker.removeExpiredSessions(clock.instant()); - if (sessions.size() >= maxSessions && !sessions.containsKey(this.getId())) { + if (sessions.size() >= maxSessions && !sessions.containsKey(this.id.get())) { throw new IllegalStateException("Max sessions limit reached: " + sessions.size()); } } diff --git a/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java b/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java index baeac73d0088..a1d62c9710ef 100644 --- a/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java @@ -160,21 +160,40 @@ void maxSessions() { @Test void updateSession() { - WebSession oneWebSession = insertSession(); + WebSession session = insertSession(); - StepVerifier.create(oneWebSession.save()) + StepVerifier.create(session.save()) .expectComplete() .verify(); } - @Test - void updateSession_whenMaxSessionsReached() { - WebSession onceWebSession = insertSession(); - IntStream.range(1, 10000).forEach(i -> insertSession()); + @Test // gh-35013 + void updateSessionAfterMaxSessionLimitIsExceeded() { + this.store.setMaxSessions(10); + + WebSession session = insertSession(); + assertNumSessions(1); + + IntStream.rangeClosed(1, 9).forEach(i -> insertSession()); + assertNumSessions(10); + + // Updating an existing session should succeed. + StepVerifier.create(session.save()) + .expectComplete() + .verify(); + assertNumSessions(10); + + // Saving an additional new session should fail. + assertThatIllegalStateException() + .isThrownBy(this::insertSession) + .withMessage("Max sessions limit reached: 10"); + assertNumSessions(10); - StepVerifier.create(onceWebSession.save()) + // Updating an existing session again should still succeed. + StepVerifier.create(session.save()) .expectComplete() .verify(); + assertNumSessions(10); } From 28caa39020a9f7d73f0c181ae265093bedbe9139 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 11 Jun 2025 12:54:02 +0200 Subject: [PATCH 105/108] Upgrade to Reactor 2023.0.19 Includes SLF4J 2.0.17, Groovy 4.0.27, FreeMarker 2.3.34 Closes gh-35022 --- framework-platform/framework-platform.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 8ce8cb73b30d..3844e38ec3c6 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -11,9 +11,9 @@ dependencies { api(platform("io.micrometer:micrometer-bom:1.12.12")) api(platform("io.netty:netty-bom:4.1.121.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2023.0.18")) + api(platform("io.projectreactor:reactor-bom:2023.0.19")) api(platform("io.rsocket:rsocket-bom:1.1.5")) - api(platform("org.apache.groovy:groovy-bom:4.0.26")) + api(platform("org.apache.groovy:groovy-bom:4.0.27")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) api(platform("org.assertj:assertj-bom:3.27.3")) api(platform("org.eclipse.jetty:jetty-bom:12.0.21")) @@ -121,7 +121,7 @@ dependencies { api("org.eclipse:yasson:2.0.4") api("org.ehcache:ehcache:3.10.8") api("org.ehcache:jcache:1.0.1") - api("org.freemarker:freemarker:2.3.33") + api("org.freemarker:freemarker:2.3.34") api("org.glassfish.external:opendmk_jmxremote_optional_jar:1.0-b01-ea") api("org.glassfish:jakarta.el:4.0.2") api("org.glassfish.tyrus:tyrus-container-servlet:2.1.3") @@ -140,7 +140,7 @@ dependencies { api("org.seleniumhq.selenium:htmlunit-driver:2.70.0") api("org.seleniumhq.selenium:selenium-java:3.141.59") api("org.skyscreamer:jsonassert:1.5.3") - api("org.slf4j:slf4j-api:2.0.16") + api("org.slf4j:slf4j-api:2.0.17") api("org.testng:testng:7.9.0") api("org.webjars:underscorejs:1.8.3") api("org.webjars:webjars-locator-core:0.55") From fd68ea6fcbf94fc1d38bfefd3692fe094652ab3d Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 12 Jun 2025 08:39:41 +0200 Subject: [PATCH 106/108] Encode non-printable character in Content-Disposition parameter Prior to this commit, the "filename" parameter value for the "Content-Disposition" header would contain non-printable characters, causing parsing issues for HTTP clients. This commit ensures that all non-printable characters are encoded. Fixes gh-35035 --- .../org/springframework/http/ContentDisposition.java | 3 ++- .../springframework/http/ContentDispositionTests.java | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java index 7697538739d3..54b6235dfff6 100644 --- a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java +++ b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java @@ -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"); * you may not use this file except in compliance with the License. @@ -67,6 +67,7 @@ public final class ContentDisposition { for (int i=33; i<= 126; i++) { PRINTABLE.set(i); } + PRINTABLE.set(34, false); // " PRINTABLE.set(61, false); // = PRINTABLE.set(63, false); // ? PRINTABLE.set(95, false); // _ diff --git a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java index 50612a84d4db..eb8f5985688a 100644 --- a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java +++ b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -305,6 +305,13 @@ void formatWithFilenameWithQuotes() { tester.accept("foo.txt\\\\\\", "foo.txt\\\\\\\\\\\\"); } + @Test + void formatWithUtf8FilenameWithQuotes() { + String filename = "\"中文.txt"; + assertThat(ContentDisposition.formData().filename(filename, StandardCharsets.UTF_8).build().toString()) + .isEqualTo("form-data; filename=\"=?UTF-8?Q?=22=E4=B8=AD=E6=96=87.txt?=\"; filename*=UTF-8''%22%E4%B8%AD%E6%96%87.txt"); + } + @Test void formatWithEncodedFilenameUsingInvalidCharset() { assertThatIllegalArgumentException().isThrownBy(() -> From 498ccda8fc354a905875a79f2d29e25a447b718b Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 12 Jun 2025 10:00:55 +0200 Subject: [PATCH 107/108] Upgrade to Gradle 8.14.2 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e2847c820046..ff23a68d70f3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 99a366baf6640b275d08dde60f05da719139bb6a Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 12 Jun 2025 10:56:22 +0200 Subject: [PATCH 108/108] Next development version (v6.1.22-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5b48d2b7d26a..3d93772ff48e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.1.21-SNAPSHOT +version=6.1.22-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m