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 }} 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 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 diff --git a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java index bd4bcba5686e..b35b3e3b5df6 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.1"); + 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(); @@ -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-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 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/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..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,27 +12,27 @@ 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( - "Members.add(new org.spring.samples.spel.inventor.Inventor( - 'Albert Einstein', 'German'))").getValue(societyContext); + parser.parseExpression( + "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") + .getValue(societyContext); ---- 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 6e1e2bf81f64..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 @@ -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: @@ -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/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/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]# 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 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" } } diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 844089b50f0e..3844e38ec3c6 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.121.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2023.0.12")) - api(platform("io.rsocket:rsocket-bom:1.1.4")) - api(platform("org.apache.groovy:groovy-bom:4.0.24")) + 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.27")) 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.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")) @@ -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") @@ -54,11 +54,11 @@ 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.17.Final") - api("io.undertow:undertow-servlet:2.3.17.Final") - api("io.undertow:undertow-websockets-jsr: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.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") @@ -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.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") api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.28") @@ -116,12 +116,12 @@ 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") 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") diff --git a/gradle.properties b/gradle.properties index 531361dfa877..3d93772ff48e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.1.16-SNAPSHOT +version=6.1.22-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df97d72b8b91..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.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME 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/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/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-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) { + } + } + +} 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; } 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..efca60ebcb95 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. @@ -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('['); @@ -658,6 +657,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 +692,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/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 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) 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/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-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/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-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/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); 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 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; + } 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; 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/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/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 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/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); } 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..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 @@ -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. @@ -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; @@ -26,6 +27,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; @@ -109,8 +111,21 @@ 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 + 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); if (logger.isTraceEnabled()) { logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s", configClass.getName(), enhancedClass.getName())); @@ -124,36 +139,74 @@ public Class enhance(Class configClass, @Nullable ClassLoader classLoader) } } + /** + * 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(); + if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) { + return true; + } + } + } + return false; + } + /** * Creates a new CGLIB {@link Enhancer} instance. */ private Enhancer newEnhancer(Class configSuperClass, @Nullable ClassLoader classLoader) { 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. */ - private Class createClass(Enhancer enhancer) { - Class subclass = enhancer.createClass(); + private Class createClass(Enhancer enhancer, boolean fallback) { + Class subclass; + try { + subclass = enhancer.createClass(); + } + catch (Throwable ex) { + if (!fallback) { + 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 + 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); @@ -164,8 +217,7 @@ private Class createClass(Enhancer enhancer) { /** * 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 @@ -526,7 +578,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/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 9a7917011b7a..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 @@ -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,23 @@ 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.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); } } catch (BeanDefinitionStoreException ex) { @@ -183,31 +191,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 +230,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 +462,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 +650,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 +705,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 +717,7 @@ public void handle(ConfigurationClass configClass, DeferredImportSelector import } } - public void process() { + void process() { List deferredImports = this.deferredImportSelectors; this.deferredImportSelectors = null; try { @@ -727,7 +741,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 +751,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 +789,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 +814,7 @@ private static class DeferredImportSelectorGrouping { this.group = group; } - public void add(DeferredImportSelectorHolder deferredImport) { + void add(DeferredImportSelectorHolder deferredImport) { this.deferredImports.add(deferredImport); } @@ -808,7 +822,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 +830,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/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/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); } } 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/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; } /** 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); + } } } 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..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); @@ -210,7 +229,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 +237,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 +279,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 ""; } @@ -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 { + } + } 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..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 @@ -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,133 @@ 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); + } + + @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 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(); + + 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.getParent()); + + 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 +175,42 @@ 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 MyConfigWithNonPublicConstructor { + + MyConfigWithNonPublicConstructor() { + } + + @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 +234,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); + } } } 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..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 @@ -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); @@ -126,12 +129,29 @@ 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)); 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 +163,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 +175,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 +189,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 +197,15 @@ void enhancementIsNotPresentForStaticMethodsUsingAsm() { assertThat(bar.foo).isNotSameAs(foo); } + @Test // gh-34486 + 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 +1190,7 @@ static class NonEnhancedSingletonBeanConfig { } @Configuration - static class StaticSingletonBeanConfig { + static final class StaticSingletonBeanConfig { @Bean public static Foo foo() { @@ -1171,6 +1203,16 @@ 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 { + } + @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"); } } 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() { 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; 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(); 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; } 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-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"); 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); + } } } 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 } } 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..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); @@ -527,15 +538,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); 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/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()); } /** 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 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/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)); } } 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(); } 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-core/src/main/java/org/springframework/util/PatternMatchUtils.java b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java index 9f050351f0b6..f0f0070567d0 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,25 @@ 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. + * @since 6.1.20 + */ + 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 +64,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 +124,19 @@ public static boolean simpleMatch(@Nullable String[] patterns, @Nullable String return false; } + /** + * 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) { + for (String pattern : patterns) { + if (simpleMatch(pattern, str, true)) { + return true; + } + } + } + return false; + } + } 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(); 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 { } 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. // 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(); } } 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) { 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-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/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/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 29a4255254a1..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 @@ -32,6 +32,8 @@ * * @author Andy Clement * @author Sam Brannen + * @see ConstructorInvocationTests + * @see MethodInvocationTests */ class VariableAndFunctionTests extends AbstractExpressionTests { @@ -77,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); @@ -239,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(); 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-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); 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()); } /** 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/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; } 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-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-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() { 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; 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); } + } 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-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) { 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..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. @@ -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 @@ -723,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/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() 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-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index 6d5c92007d13..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. @@ -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()); @@ -619,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-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(); 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/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-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. 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-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-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-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..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 @@ -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); @@ -455,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); @@ -466,10 +441,12 @@ public void startDeferredResultProcessing( logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest)); } try { - if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) { + 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; } - deferredResult.setErrorResult(ex); } catch (Throwable interceptorEx) { setConcurrentResultAndDispatch(interceptorEx); @@ -508,6 +485,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 +524,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/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-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..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 @@ -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() { @@ -278,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.id.get())) { throw new IllegalStateException("Max sessions limit reached: " + sessions.size()); } } 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/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/main/java/org/springframework/web/util/DisconnectedClientHelper.java b/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java index b2b16f135adb..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 @@ -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. @@ -16,6 +16,7 @@ package org.springframework.web.util; +import java.util.HashSet; import java.util.Locale; import java.util.Set; @@ -24,12 +25,14 @@ 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 @@ -37,12 +40,28 @@ public class DisconnectedClientHelper { 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", "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; @@ -79,10 +98,25 @@ 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) { + 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); @@ -92,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/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(() -> 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-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)); 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..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 @@ -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,11 +19,11 @@ 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; import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; import org.springframework.beans.DirectFieldAccessor; import org.springframework.web.server.WebSession; @@ -35,10 +35,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 @@ -53,13 +54,14 @@ 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(); } @Test // gh-24027, gh-26958 - public void createSessionDoesNotBlock() { + void createSessionDoesNotBlock() { this.store.createWebSession() .doOnNext(session -> assertThat(Schedulers.isInNonBlockingThread()).isTrue()) .block(); @@ -103,7 +105,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 +134,69 @@ 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.rangeClosed(1, 10).forEach(i -> insertSession()); + assertThatIllegalStateException() + .isThrownBy(this::insertSession) + .withMessage("Max sessions limit reached: 10"); + } - IntStream.range(0, 10000).forEach(i -> insertSession()); - assertThatIllegalStateException().isThrownBy( - this::insertSession) - .withMessage("Max sessions limit reached: 10000"); + @Test + void updateSession() { + WebSession session = insertSession(); + + StepVerifier.create(session.save()) + .expectComplete() + .verify(); + } + + @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); + + // Updating an existing session again should still succeed. + StepVerifier.create(session.save()) + .expectComplete() + .verify(); + assertNumSessions(10); } + private WebSession insertSession() { WebSession session = this.store.createWebSession().block(); assertThat(session).isNotNull(); @@ -167,4 +205,8 @@ private WebSession insertSession() { return session; } + private void assertNumSessions(int numSessions) { + assertThat(store.getSessions()).hasSize(numSessions); + } + } 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..296a19209272 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/util/DisconnectedClientHelperTests.java @@ -0,0 +1,93 @@ +/* + * 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.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; + +/** + * 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("")); + } + + @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(); + } + +} 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..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. @@ -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 @@ -723,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-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)); } } 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())); 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/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/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/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..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 @@ -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.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; @@ -84,13 +85,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 @@ -140,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) { @@ -152,8 +152,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}) */ @@ -166,8 +164,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 @@ -193,8 +189,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 @@ -214,8 +208,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 @@ -240,8 +232,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 @@ -258,8 +248,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 @@ -306,8 +294,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 @@ -328,8 +314,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 @@ -355,8 +339,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) { @@ -379,8 +361,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) { @@ -423,9 +403,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 @@ -441,8 +418,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 @@ -482,8 +457,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) { @@ -491,10 +464,8 @@ 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}. - *

    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) { @@ -504,8 +475,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) { @@ -515,8 +484,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) { @@ -526,8 +493,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) { @@ -667,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; @@ -829,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(); 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..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 @@ -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,11 +333,18 @@ 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); + try { + this.emitter.completeWithError(ex); + } + catch (Exception ex2) { + if (logger.isDebugEnabled()) { + logger.debug("Failure from emitter completeWithError: " + ex2); + } + } return; } } @@ -347,16 +354,30 @@ 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); + } + try { + this.emitter.completeWithError(ex); + } + catch (Exception ex2) { + if (logger.isDebugEnabled()) { + logger.debug("Failure from emitter completeWithError: " + ex2); + } } - this.emitter.completeWithError(ex); } 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; } 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) { 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/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-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/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 @@ + + + + + + + + + + + + + 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"/> 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/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/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 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) { 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