diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 126d25b6ba..9eaafdc2da 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -19,7 +19,7 @@ inputs: java-version: description: 'Java version to compile and test with' required: false - default: '17' + default: '24' publish: description: 'Whether to publish artifacts ready for deployment to Artifactory' required: false diff --git a/.github/actions/create-github-release/action.yml b/.github/actions/create-github-release/action.yml index 03452537ad..b82e149cb5 100644 --- a/.github/actions/create-github-release/action.yml +++ b/.github/actions/create-github-release/action.yml @@ -15,7 +15,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/actions/prepare-gradle-build/action.yml b/.github/actions/prepare-gradle-build/action.yml index 9b305a8179..c07a74c1df 100644 --- a/.github/actions/prepare-gradle-build/action.yml +++ b/.github/actions/prepare-gradle-build/action.yml @@ -19,7 +19,7 @@ inputs: java-version: description: 'Java version to use for the build' required: false - default: '17' + default: '24' runs: using: composite steps: @@ -31,7 +31,7 @@ runs: ${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }} ${{ inputs.java-toolchain == 'true' && '17' || '' }} - name: Set Up Gradle - uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 + uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 with: cache-read-only: false develocity-access-key: ${{ inputs.develocity-access-key }} diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 3ffb330e72..112c6340c5 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -46,7 +46,7 @@ jobs: distribution: 'liberica' java-version: 17 - name: Set Up Gradle - uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 + uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 with: cache-read-only: false - name: Configure Gradle Properties diff --git a/build.gradle b/build.gradle index 6dfc08b0ee..ba18d62245 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ plugins { - id 'io.freefair.aspectj' version '8.13' apply false + id 'io.freefair.aspectj' version '8.13.1' apply false // kotlinVersion is managed in gradle.properties id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false id 'org.jetbrains.dokka' version '1.9.20' id 'com.github.bjornvester.xjc' version '1.8.2' apply false id 'io.github.goooler.shadow' version '8.1.8' apply false id 'me.champeau.jmh' version '0.7.2' apply false - id "net.ltgt.errorprone" version "4.1.0" apply false + id "io.spring.nullability" version "0.0.1" apply false } ext { @@ -75,12 +75,11 @@ configure([rootProject] + javaProjects) { project -> "https://docs.oracle.com/en/java/javase/17/docs/api/", "https://jakarta.ee/specifications/platform/11/apidocs/", "https://docs.jboss.org/hibernate/orm/5.6/javadocs/", - "https://eclipse.dev/aspectj/doc/latest/runtime-api/", "https://www.quartz-scheduler.org/api/2.3.0/", - "https://hc.apache.org/httpcomponents-client-5.4.x/current/httpclient5/apidocs/", + "https://hc.apache.org/httpcomponents-client-5.5.x/current/httpclient5/apidocs/", "https://projectreactor.io/docs/test/release/api/", "https://junit.org/junit4/javadoc/4.13.2/", - "https://junit.org/junit5/docs/5.12.1/api/", + "https://junit.org/junit5/docs/5.13.1/api/", "https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/", //"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/", "https://r2dbc.io/spec/1.0.0.RELEASE/api/", @@ -88,7 +87,9 @@ configure([rootProject] + javaProjects) { project -> // but since 6.0 JSR 250 annotations such as @Resource and @PostConstruct have been replaced by their // JakartaEE equivalents in the jakarta.annotation package. //"https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.2/", - "https://jspecify.dev/docs/api/" + "https://jspecify.dev/docs/api/", + "https://www.javadoc.io/doc/tools.jackson.core/jackson-databind/3.0.0-rc4/" + ] as String[] } diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 00019ff5de..aada8c4f11 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -27,7 +27,9 @@ dependencies { implementation "io.spring.nohttp:nohttp-gradle:0.0.11" testImplementation("org.assertj:assertj-core:${assertjVersion}") - testImplementation("org.junit.jupiter:junit-jupiter:${junitJupiterVersion}") + testImplementation(platform("org.junit:junit-bom:${junitVersion}")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } gradlePlugin { diff --git a/buildSrc/gradle.properties b/buildSrc/gradle.properties index 48dbb008b6..361684dbe0 100644 --- a/buildSrc/gradle.properties +++ b/buildSrc/gradle.properties @@ -1,4 +1,4 @@ org.gradle.caching=true -javaFormatVersion=0.0.42 -junitJupiterVersion=5.11.4 -assertjVersion=3.27.3 \ No newline at end of file +assertjVersion=3.27.3 +javaFormatVersion=0.0.43 +junitVersion=5.12.2 diff --git a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java index 23ebe53aa9..170bb5c78e 100644 --- a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java @@ -50,7 +50,7 @@ public class CheckstyleConventions { project.getPlugins().apply(CheckstylePlugin.class); project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g")); CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class); - checkstyle.setToolVersion("10.21.4"); + checkstyle.setToolVersion("10.25.0"); checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle")); String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion(); DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies(); diff --git a/buildSrc/src/main/java/org/springframework/build/JavaConventions.java b/buildSrc/src/main/java/org/springframework/build/JavaConventions.java index 99d9d97ef6..1cf2574cac 100644 --- a/buildSrc/src/main/java/org/springframework/build/JavaConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/JavaConventions.java @@ -26,7 +26,6 @@ import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.tasks.compile.JavaCompile; import org.gradle.jvm.toolchain.JavaLanguageVersion; -import org.gradle.jvm.toolchain.JvmVendorSpec; /** * {@link Plugin} that applies conventions for compiling Java sources in Spring Framework. @@ -42,12 +41,15 @@ public class JavaConventions { private static final List TEST_COMPILER_ARGS; /** - * The Java version we should use as the JVM baseline for building the project + * The Java version we should use as the JVM baseline for building the project. + *

NOTE: If you update this value, you should also update the value used in + * the {@code javadoc} task in {@code framework-api.gradle}. */ private static final JavaLanguageVersion DEFAULT_LANGUAGE_VERSION = JavaLanguageVersion.of(24); /** - * The Java version we should use as the baseline for the compiled bytecode (the "-release" compiler argument) + * The Java version we should use as the baseline for the compiled bytecode + * (the "-release" compiler argument). */ private static final JavaLanguageVersion DEFAULT_RELEASE_VERSION = JavaLanguageVersion.of(17); @@ -83,7 +85,6 @@ public class JavaConventions { */ private static void applyToolchainConventions(Project project) { project.getExtensions().getByType(JavaPluginExtension.class).toolchain(toolchain -> { - toolchain.getVendor().set(JvmVendorSpec.BELLSOFT); toolchain.getLanguageVersion().set(DEFAULT_LANGUAGE_VERSION); }); } diff --git a/buildSrc/src/main/java/org/springframework/build/TestConventions.java b/buildSrc/src/main/java/org/springframework/build/TestConventions.java index baa741781c..e6e2dd0d9a 100644 --- a/buildSrc/src/main/java/org/springframework/build/TestConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/TestConventions.java @@ -21,6 +21,8 @@ import java.util.Map; import org.gradle.api.Project; import org.gradle.api.plugins.JavaBasePlugin; import org.gradle.api.tasks.testing.Test; +import org.gradle.api.tasks.testing.TestFrameworkOptions; +import org.gradle.api.tasks.testing.junitplatform.JUnitPlatformOptions; import org.gradle.testretry.TestRetryPlugin; import org.gradle.testretry.TestRetryTaskExtension; @@ -34,6 +36,7 @@ import org.gradle.testretry.TestRetryTaskExtension; * * @author Brian Clozel * @author Andy Wilkinson + * @author Sam Brannen */ class TestConventions { @@ -50,7 +53,12 @@ class TestConventions { } private void configureTests(Project project, Test test) { - test.useJUnitPlatform(); + TestFrameworkOptions existingOptions = test.getOptions(); + test.useJUnitPlatform(options -> { + if (existingOptions instanceof JUnitPlatformOptions junitPlatformOptions) { + options.copyFrom(junitPlatformOptions); + } + }); test.include("**/*Tests.class", "**/*Test.class"); test.setSystemProperties(Map.of( "java.awt.headless", "true", diff --git a/framework-api/framework-api.gradle b/framework-api/framework-api.gradle index c8456268c1..f9da96bcd0 100644 --- a/framework-api/framework-api.gradle +++ b/framework-api/framework-api.gradle @@ -1,6 +1,6 @@ plugins { id 'java-platform' - id 'io.freefair.aggregate-javadoc' version '8.3' + id 'io.freefair.aggregate-javadoc' version '8.13.1' } description = "Spring Framework API Docs" @@ -20,7 +20,12 @@ dependencies { } javadoc { + javadocTool.set(javaToolchains.javadocToolFor({ + languageVersion = JavaLanguageVersion.of(24) + })) + title = "${rootProject.description} ${version} API" + failOnError = true options { encoding = "UTF-8" memberLevel = JavadocMemberLevel.PROTECTED @@ -31,8 +36,13 @@ javadoc { destinationDir = project.java.docsDir.dir("javadoc-api").get().asFile splitIndex = true links(rootProject.ext.javadocLinks) - addBooleanOption('Xdoclint:syntax,reference', true) // only check syntax and reference with doclint - addBooleanOption('Werror', true) // fail build on Javadoc warnings + // Check for 'syntax' and 'reference' during linting. + addBooleanOption('Xdoclint:syntax,reference', true) + // Change modularity mismatch from warn to info. + // See https://github.com/spring-projects/spring-framework/issues/27497 + addStringOption("-link-modularity-mismatch", "info") + // Fail build on Javadoc warnings. + addBooleanOption('Werror', true) } maxMemory = "1024m" doFirst { diff --git a/framework-docs/framework-docs.gradle b/framework-docs/framework-docs.gradle index 9556374598..84527746bd 100644 --- a/framework-docs/framework-docs.gradle +++ b/framework-docs/framework-docs.gradle @@ -45,39 +45,39 @@ repositories { // To avoid a redeclaration error with Kotlin compiler tasks.named('compileKotlin', KotlinCompilationTask.class) { - javaSources.from = [] + javaSources.from = [] } dependencies { - api(project(":spring-aspects")) - api(project(":spring-context")) - api(project(":spring-context-support")) - api(project(":spring-core-test")) - api(project(":spring-jdbc")) - api(project(":spring-jms")) - api(project(":spring-test")) - api(project(":spring-web")) - api(project(":spring-webflux")) - api(project(":spring-webmvc")) - api(project(":spring-websocket")) - - api("com.fasterxml.jackson.core:jackson-databind") - api("com.fasterxml.jackson.module:jackson-module-parameter-names") - api("com.mchange:c3p0:0.9.5.5") - api("com.oracle.database.jdbc:ojdbc11") - api("io.projectreactor.netty:reactor-netty-http") - api("jakarta.jms:jakarta.jms-api") - api("jakarta.servlet:jakarta.servlet-api") - api("jakarta.resource:jakarta.resource-api") - api("jakarta.validation:jakarta.validation-api") - api("jakarta.websocket:jakarta.websocket-client-api") - api("javax.cache:cache-api") - api("org.apache.activemq:activemq-ra:6.1.2") - api("org.apache.commons:commons-dbcp2:2.11.0") - api("org.aspectj:aspectjweaver") - api("org.assertj:assertj-core") - api("org.eclipse.jetty.websocket:jetty-websocket-jetty-api") - api("org.jetbrains.kotlin:kotlin-stdlib") - api("org.junit.jupiter:junit-jupiter-api") + implementation(project(":spring-aspects")) + implementation(project(":spring-context")) + implementation(project(":spring-context-support")) + implementation(project(":spring-core-test")) + implementation(project(":spring-jdbc")) + implementation(project(":spring-jms")) + implementation(project(":spring-test")) + implementation(project(":spring-web")) + implementation(project(":spring-webflux")) + implementation(project(":spring-webmvc")) + implementation(project(":spring-websocket")) + implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("com.fasterxml.jackson.module:jackson-module-parameter-names") + implementation("com.github.ben-manes.caffeine:caffeine") + implementation("com.mchange:c3p0:0.9.5.5") + implementation("com.oracle.database.jdbc:ojdbc11") + implementation("io.projectreactor.netty:reactor-netty-http") + implementation("jakarta.jms:jakarta.jms-api") + implementation("jakarta.servlet:jakarta.servlet-api") + implementation("jakarta.resource:jakarta.resource-api") + implementation("jakarta.validation:jakarta.validation-api") + implementation("jakarta.websocket:jakarta.websocket-client-api") + implementation("javax.cache:cache-api") + implementation("org.apache.activemq:activemq-ra:6.1.2") + implementation("org.apache.commons:commons-dbcp2:2.11.0") + implementation("org.aspectj:aspectjweaver") + implementation("org.assertj:assertj-core") + implementation("org.eclipse.jetty.websocket:jetty-websocket-jetty-api") + implementation("org.jetbrains.kotlin:kotlin-stdlib") + implementation("org.junit.jupiter:junit-jupiter-api") } diff --git a/framework-docs/modules/ROOT/nav.adoc b/framework-docs/modules/ROOT/nav.adoc index c2710277cd..f8389c3fdc 100644 --- a/framework-docs/modules/ROOT/nav.adoc +++ b/framework-docs/modules/ROOT/nav.adoc @@ -197,6 +197,7 @@ *** xref:web/webmvc/mvc-uri-building.adoc[] *** xref:web/webmvc/mvc-ann-async.adoc[] *** xref:web/webmvc-cors.adoc[] +*** xref:web/webmvc-versioning.adoc[] *** xref:web/webmvc/mvc-ann-rest-exceptions.adoc[] *** xref:web/webmvc/mvc-security.adoc[] *** xref:web/webmvc/mvc-caching.adoc[] @@ -225,6 +226,7 @@ **** xref:web/webmvc/mvc-config/static-resources.adoc[] **** xref:web/webmvc/mvc-config/default-servlet-handler.adoc[] **** xref:web/webmvc/mvc-config/path-matching.adoc[] +**** xref:web/webmvc/mvc-config/api-version.adoc[] **** xref:web/webmvc/mvc-config/advanced-java.adoc[] **** xref:web/webmvc/mvc-config/advanced-xml.adoc[] *** xref:web/webmvc/mvc-http2.adoc[] @@ -292,6 +294,7 @@ *** xref:web/webflux-functional.adoc[] *** xref:web/webflux/uri-building.adoc[] *** xref:web/webflux-cors.adoc[] +*** xref:web/webflux-versioning.adoc[] *** xref:web/webflux/ann-rest-exceptions.adoc[] *** xref:web/webflux/security.adoc[] *** xref:web/webflux/caching.adoc[] @@ -432,8 +435,8 @@ *** xref:integration/cache/plug.adoc[] *** xref:integration/cache/specific-config.adoc[] ** xref:integration/observability.adoc[] +** xref:integration/aot-cache.adoc[] ** xref:integration/checkpoint-restore.adoc[] -** xref:integration/cds.adoc[] ** xref:integration/appendix.adoc[] * xref:languages.adoc[] ** xref:languages/kotlin.adoc[] diff --git a/framework-docs/modules/ROOT/pages/appendix.adoc b/framework-docs/modules/ROOT/pages/appendix.adoc index 896453d8e9..6e7e5cecd0 100644 --- a/framework-docs/modules/ROOT/pages/appendix.adoc +++ b/framework-docs/modules/ROOT/pages/appendix.adoc @@ -92,11 +92,25 @@ the repeated JNDI lookup overhead. See {spring-framework-api}++/jndi/JndiLocatorDelegate.html#IGNORE_JNDI_PROPERTY_NAME++[`JndiLocatorDelegate`] for details. +| `spring.locking.strict` +| Instructs Spring to enforce strict locking during bean creation, rather than the mix of +strict and lenient locking that 6.2 applies by default. See +{spring-framework-api}++/beans/factory/support/DefaultListableBeanFactory.html#STRICT_LOCKING_PROPERTY_NAME++[`DefaultListableBeanFactory`] +for details. + | `spring.objenesis.ignore` | Instructs Spring to ignore Objenesis, not even attempting to use it. See {spring-framework-api}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`] for details. +| `spring.placeholder.escapeCharacter.default` +| The default escape character for property placeholder support. If not set, `'\'` will +be used. Can be set to a custom escape character or an empty string to disable support +for an escape character. The default escape character be explicitly overridden in +`PropertySourcesPlaceholderConfigurer` and subclasses of `AbstractPropertyResolver`. See +{spring-framework-api}++/core/env/AbstractPropertyResolver.html#DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME++[`AbstractPropertyResolver`] +for details. + | `spring.test.aot.processing.failOnError` | A boolean flag that controls whether errors encountered during AOT processing in the _Spring TestContext Framework_ should result in an exception that fails the overall process. diff --git a/framework-docs/modules/ROOT/pages/core/aot.adoc b/framework-docs/modules/ROOT/pages/core/aot.adoc index 80a75965d7..ce75e7fa59 100644 --- a/framework-docs/modules/ROOT/pages/core/aot.adoc +++ b/framework-docs/modules/ROOT/pages/core/aot.adoc @@ -469,20 +469,20 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - RootBeanDefinition beanDefinition = new RootBeanDefinition(ClientFactoryBean.class); - beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean.class, MyClient.class)); - // ... - registry.registerBeanDefinition("myClient", beanDefinition); + RootBeanDefinition beanDefinition = new RootBeanDefinition(ClientFactoryBean.class); + beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean.class, MyClient.class)); + // ... + registry.registerBeanDefinition("myClient", beanDefinition); ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val beanDefinition = RootBeanDefinition(ClientFactoryBean::class.java) - beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean::class.java, MyClient::class.java)); - // ... - registry.registerBeanDefinition("myClient", beanDefinition) + val beanDefinition = RootBeanDefinition(ClientFactoryBean::class.java) + beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean::class.java, MyClient::class.java)); + // ... + registry.registerBeanDefinition("myClient", beanDefinition) ---- ====== diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc index 13f20afe73..d91aaafb19 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc @@ -9,15 +9,15 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - @Component - public class MovieRecommender { + @Component + public class MovieRecommender { - private final String catalog; + private final String catalog; - public MovieRecommender(@Value("${catalog.name}") String catalog) { - this.catalog = catalog; - } - } + public MovieRecommender(@Value("${catalog.name}") String catalog) { + this.catalog = catalog; + } + } ---- Kotlin:: @@ -37,9 +37,9 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - @Configuration - @PropertySource("classpath:application.properties") - public class AppConfig { } + @Configuration + @PropertySource("classpath:application.properties") + public class AppConfig { } ---- Kotlin:: @@ -56,7 +56,7 @@ And the following `application.properties` file: [source,java,indent=0,subs="verbatim,quotes"] ---- - catalog.name=MovieCatalog + catalog.name=MovieCatalog ---- In that case, the `catalog` parameter and field will be equal to the `MovieCatalog` value. @@ -101,8 +101,11 @@ NOTE: When configuring a `PropertySourcesPlaceholderConfigurer` using JavaConfig Using the above configuration ensures Spring initialization failure if any `${}` placeholder could not be resolved. It is also possible to use methods like -`setPlaceholderPrefix`, `setPlaceholderSuffix`, `setValueSeparator`, or -`setEscapeCharacter` to customize placeholders. +`setPlaceholderPrefix()`, `setPlaceholderSuffix()`, `setValueSeparator()`, or +`setEscapeCharacter()` to customize the placeholder syntax. In addition, the default +escape character can be changed or disabled globally by setting the +`spring.placeholder.escapeCharacter.default` property via a JVM system property (or via +the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism). NOTE: Spring Boot configures by default a `PropertySourcesPlaceholderConfigurer` bean that will get properties from `application.properties` and `application.yml` files. @@ -119,15 +122,15 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - @Component - public class MovieRecommender { + @Component + public class MovieRecommender { - private final String catalog; + private final String catalog; - public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) { - this.catalog = catalog; - } - } + public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) { + this.catalog = catalog; + } + } ---- Kotlin:: @@ -150,16 +153,16 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - @Configuration - public class AppConfig { + @Configuration + public class AppConfig { - @Bean - public ConversionService conversionService() { - DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); - conversionService.addConverter(new MyCustomConverter()); - return conversionService; - } - } + @Bean + public ConversionService conversionService() { + DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); + conversionService.addConverter(new MyCustomConverter()); + return conversionService; + } + } ---- Kotlin:: @@ -188,15 +191,15 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - @Component - public class MovieRecommender { + @Component + public class MovieRecommender { - private final String catalog; + private final String catalog; - public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) { - this.catalog = catalog; - } - } + public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) { + this.catalog = catalog; + } + } ---- Kotlin:: @@ -217,16 +220,16 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - @Component - public class MovieRecommender { + @Component + public class MovieRecommender { - private final Map countOfMoviesPerCatalog; + private final Map countOfMoviesPerCatalog; - public MovieRecommender( - @Value("#{{'Thriller': 100, 'Comedy': 300}}") Map countOfMoviesPerCatalog) { - this.countOfMoviesPerCatalog = countOfMoviesPerCatalog; - } - } + public MovieRecommender( + @Value("#{{'Thriller': 100, 'Comedy': 300}}") Map countOfMoviesPerCatalog) { + this.countOfMoviesPerCatalog = countOfMoviesPerCatalog; + } + } ---- Kotlin:: diff --git a/framework-docs/modules/ROOT/pages/core/beans/basics.adoc b/framework-docs/modules/ROOT/pages/core/beans/basics.adoc index ab6562f740..7bc227cdc0 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/basics.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/basics.adoc @@ -119,7 +119,7 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val context = ClassPathXmlApplicationContext("services.xml", "daos.xml") + val context = ClassPathXmlApplicationContext("services.xml", "daos.xml") ---- ====== @@ -309,16 +309,16 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- - import org.springframework.beans.factory.getBean + import org.springframework.beans.factory.getBean // create and configure beans - val context = ClassPathXmlApplicationContext("services.xml", "daos.xml") + val context = ClassPathXmlApplicationContext("services.xml", "daos.xml") - // retrieve configured instance - val service = context.getBean("petStore") + // retrieve configured instance + val service = context.getBean("petStore") - // use configured instance - var userList = service.getUsernameList() + // use configured instance + var userList = service.getUsernameList() ---- ====== diff --git a/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc b/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc index 612185813e..537b90cc5f 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc @@ -513,7 +513,7 @@ the classes above: - + diff --git a/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc b/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc index cf9e68e3a8..56641fd847 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc @@ -226,7 +226,7 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- - import org.springframework.beans.factory.getBean + import org.springframework.beans.factory.getBean fun main() { val ctx = ClassPathXmlApplicationContext("scripting/beans.xml") @@ -314,7 +314,7 @@ Thus, marking it for lazy initialization will be ignored, and the [[beans-factory-placeholderconfigurer]] -=== Example: The Class Name Substitution `PropertySourcesPlaceholderConfigurer` +=== Example: Property Placeholder Substitution with `PropertySourcesPlaceholderConfigurer` You can use the `PropertySourcesPlaceholderConfigurer` to externalize property values from a bean definition in a separate file by using the standard Java `Properties` format. @@ -341,8 +341,8 @@ with placeholder values is defined: The example shows properties configured from an external `Properties` file. At runtime, a `PropertySourcesPlaceholderConfigurer` is applied to the metadata that replaces some -properties of the DataSource. The values to replace are specified as placeholders of the -form pass:q[`${property-name}`], which follows the Ant and log4j and JSP EL style. +properties of the `DataSource`. The values to replace are specified as placeholders of the +form pass:q[`${property-name}`], which follows the Ant, log4j, and JSP EL style. The actual values come from another file in the standard Java `Properties` format: @@ -355,11 +355,15 @@ jdbc.password=root ---- Therefore, the `${jdbc.username}` string is replaced at runtime with the value, 'sa', and -the same applies for other placeholder values that match keys in the properties file. -The `PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and -attributes of a bean definition. Furthermore, you can customize the placeholder prefix and suffix. +the same applies for other placeholder values that match keys in the properties file. The +`PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and +attributes of a bean definition. Furthermore, you can customize the placeholder prefix, +suffix, default value separator, and escape character. In addition, the default escape +character can be changed or disabled globally by setting the +`spring.placeholder.escapeCharacter.default` property via a JVM system property (or via +the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism). -With the `context` namespace introduced in Spring 2.5, you can configure property placeholders +With the `context` namespace, you can configure property placeholders with a dedicated configuration element. You can provide one or more locations as a comma-separated list in the `location` attribute, as the following example shows: diff --git a/framework-docs/modules/ROOT/pages/core/null-safety.adoc b/framework-docs/modules/ROOT/pages/core/null-safety.adoc index 5264259da1..23355b6bb3 100644 --- a/framework-docs/modules/ROOT/pages/core/null-safety.adoc +++ b/framework-docs/modules/ROOT/pages/core/null-safety.adoc @@ -1,56 +1,56 @@ [[null-safety]] = Null-safety -Although Java does not let you express null-safety with its type system, the Spring Framework codebase is annotated with -https://jspecify.dev/docs/start-here/[JSpecify] annotations to declare the nullness of APIs, fields and related type -usages. Reading the https://jspecify.dev/docs/user-guide/[JSpecify user guide] is highly recommended in order to get -familiar with those annotations and semantics. +Although Java does not let you express nullness markers with its type system yet, the Spring Framework codebase is +annotated with https://jspecify.dev/docs/start-here/[JSpecify] annotations to declare the nullability of its APIs, +fields, and related type usages. Reading the https://jspecify.dev/docs/user-guide/[JSpecify user guide] is highly +recommended in order to get familiar with those annotations and semantics. -The primary goal of this explicit null-safety arrangement is to prevent `NullPointerException` to be thrown at runtime via -build time checks and to turn explicit nullness into a way to express the possible absence of value. It is useful in -both Java by leveraging some tooling (https://github.com/uber/NullAway[NullAway] or IDEs supporting null-safety -annotations such as IntelliJ IDEA or Eclipse) and Kotlin where JSpecify annotations are automatically translated to +The primary goal of this null-safety arrangement is to prevent a `NullPointerException` from being thrown at runtime via build +time checks and to use explicit nullability as a way to express the possible absence of value. It is useful in both +Java by leveraging some tooling (https://github.com/uber/NullAway[NullAway] or IDEs supporting JSpecify annotations +such as IntelliJ IDEA) and Kotlin where JSpecify annotations are automatically translated to {kotlin-docs}/null-safety.html[Kotlin's null safety]. The {spring-framework-api}/core/Nullness.html[`Nullness` Spring API] can be used at runtime to detect the nullness of a -type usage, a field, a method return type or a parameter. It provides full support for JSpecify annotations, -Kotlin null safety, Java primitive types, as well as a pragmatic check on any `@Nullable` annotation (regardless of the +type usage, a field, a method return type, or a parameter. It provides full support for JSpecify annotations, +Kotlin null safety, and Java primitive types, as well as a pragmatic check on any `@Nullable` annotation (regardless of the package). [[null-safety-libraries]] == Annotating libraries with JSpecify annotations As of Spring Framework 7, the Spring Framework codebase leverages JSpecify annotations to expose null-safe APIs and -to check the consistency of those null-safety declarations with https://github.com/uber/NullAway[NullAway] as part of -its build. It is recommended for each library depending on Spring Framework (Spring portfolio projects), as -well as other libraries related to the Spring ecosystem (Reactor, Micrometer and Spring community projects), to do the +to check the consistency of those nullability declarations with https://github.com/uber/NullAway[NullAway] as part of +its build. It is recommended for each library depending on Spring Framework and Spring portfolio projects, as +well as other libraries related to the Spring ecosystem (Reactor, Micrometer, and Spring community projects), to do the same. [[null-safety-applications]] == Leveraging JSpecify annotations in Spring applications -Developing applications with IDEs supporting null-safety annotations, such as IntelliJ IDEA or Eclipse, will provide -warnings in Java and errors in Kotlin when the null-safety contracts are not honored, allowing Spring application -developers to refine their null handling to prevent `NullPointerException` to be thrown at runtime. +Developing applications with IDEs that support nullness annotations will provide warnings in Java and errors in Kotlin +when the nullability contracts are not honored, allowing Spring application developers to refine their null handling to +prevent a `NullPointerException` from being thrown at runtime. -Optionally, Spring application developers can annotate their codebase and use https://github.com/uber/NullAway[NullAway] -to enforce null-safety during build time at application level. +Optionally, Spring application developers can annotate their codebase and use build plugins like +https://github.com/uber/NullAway[NullAway] to enforce null-safety at the application level during build time. [[null-safety-guidelines]] == Guidelines -The purpose of this section is to share some guidelines proposed for specifying explicitly the nullness of Spring-related -libraries or applications. +The purpose of this section is to share some proposed guidelines for explicitly specifying the nullability of +Spring-related libraries or applications. -[[null-safety-guidelines-jpecify]] +[[null-safety-guidelines-jspecify]] === JSpecify -The key points to understand is that by default, the nullness of types is unknown in Java, and that non-null type -usages are by far more frequent than nullable ones. In order to keep codebases readable, we typically want to define -that by default, type usages are non-null unless marked as nullable for a specific scope. This is exactly the purpose of -https://jspecify.dev/docs/api/org/jspecify/annotations/NullMarked.html[`@NullMarked`] that is typically set with Spring -at package level via a `package-info.java` file, for example: +The key points to understand are that the nullness of types is unknown in Java by default and that non-null type +usage is by far more frequent than nullable usage. In order to keep codebases readable, we typically want to define +by default that type usage is non-null unless marked as nullable for a specific scope. This is exactly the purpose of +https://jspecify.dev/docs/api/org/jspecify/annotations/NullMarked.html[`@NullMarked`] which is typically set in Spring +projects at the package level via a `package-info.java` file, for example: [source,java,subs="verbatim,quotes",chomp="-packages",fold="none"] ---- @@ -60,9 +60,9 @@ package org.springframework.core; import org.jspecify.annotations.NullMarked; ---- -In the various Java files belonging to the package, nullable type usages are defined explicitly with +In the various Java files belonging to the package, nullable type usage is defined explicitly with https://jspecify.dev/docs/api/org/jspecify/annotations/Nullable.html[`@Nullable`]. It is recommended that this -annotation is specified just before the related type. +annotation is specified just before the related type on the same line. For example, for a field: @@ -71,7 +71,7 @@ For example, for a field: private @Nullable String fileEncoding; ---- -Or for method parameters and return value: +Or for method parameters and method return types: [source,java,subs="verbatim,quotes"] ---- @@ -81,20 +81,23 @@ public static @Nullable String buildMessage(@Nullable String message, } ---- -When overriding a method, nullness annotations are not inherited from the superclass method. That means those -nullness annotations should be repeated if you just want to override the implementation and keep the same API -nullness. +[NOTE] +==== +When overriding a method, JSpecify annotations are not inherited from the original +method. That means the JSpecify annotations should be copied to the overriding method if +you want to override the implementation and keep the same nullability semantics. +==== With arrays and varargs, you need to be able to differentiate the nullness of the elements from the nullness of the array itself. Pay attention to the syntax https://docs.oracle.com/javase/specs/jls/se17/html/jls-9.html#jls-9.7.4[defined by the Java specification] which may be initially surprising: -- `@Nullable Object[] array` means individual elements can be null but the array itself can't. -- `Object @Nullable [] array` means individual elements can't be null but the array itself can. +- `@Nullable Object[] array` means individual elements can be null but the array itself cannot. +- `Object @Nullable [] array` means individual elements cannot be null but the array itself can. - `@Nullable Object @Nullable [] array` means both individual elements and the array can be null. -The Java specifications also enforces that annotations defined with `@Target(ElementType.TYPE_USE)` like JSpecify +The Java specification also enforces that annotations defined with `@Target(ElementType.TYPE_USE)` like JSpecify `@Nullable` should be specified after the last `.` with inner or fully qualified types: - `Cache.@Nullable ValueWrapper` @@ -111,15 +114,15 @@ typical use cases. The recommended configuration is: - - `NullAway:OnlyNullMarked=true` in order to perform nullness checks only for packages annotated with `@NullMarked`. + - `NullAway:OnlyNullMarked=true` in order to perform nullability checks only for packages annotated with `@NullMarked`. - `NullAway:CustomContractAnnotations=org.springframework.lang.Contract` which makes NullAway aware of the {spring-framework-api}/lang/Contract.html[@Contract] annotation in the `org.springframework.lang` package which -can be used to express complementary semantics to avoid non-relevant null-safety warnings in your codebase. +can be used to express complementary semantics to avoid irrelevant warnings in your codebase. -A good example of `@Contract` benefits is -{spring-framework-api}/util/Assert.html#notNull(java.lang.Object,java.lang.String)[`Assert#notnull`] which is annotated -with `@Contract("null, _ -> fail")`. With the configuration above, NullAway will understand that after a successful -invocation, the value passed as a parameter is not null. +A good example of the benefits of a `@Contract` declaration can be seen with +{spring-framework-api}/util/Assert.html#notNull(java.lang.Object,java.lang.String)[`Assert.notNull()`] which is annotated +with `@Contract("null, _ -> fail")`. With that contract declaration, NullAway will understand that the value passed as a +parameter cannot be null after a successful invocation of `Assert.notNull()`. Optionally, it is possible to set `NullAway:JSpecifyMode=true` to enable https://github.com/uber/NullAway/wiki/JSpecify-Support[checks on the full JSpecify semantics], including annotations on @@ -127,26 +130,26 @@ generic types. Be aware that this mode is https://github.com/uber/NullAway/issues?q=is%3Aissue+is%3Aopen+label%3Ajspecify[still under development] and requires using JDK 22 or later (typically combined with the `--release` Java compiler flag to configure the expected baseline). It is recommended to enable the JSpecify mode only as a second step, after making sure the codebase -generates no warning with the recommended configuration mentioned above. +generates no warning with the recommended configuration mentioned previously in this section. ==== Warnings suppression -There are a few valid use cases where NullAway will wrongly detect nullness problems. In such case, it is recommended +There are a few valid use cases where NullAway will incorrectly detect nullability problems. In such case, it is recommended to suppress related warnings and to document the reason: - - `@SuppressWarnings("NullAway.Init")` at field, constructor or class level can be used to avoid unnecessary warnings -due to the lazy initialization of fields, for example due to a class implementing + - `@SuppressWarnings("NullAway.Init")` at field, constructor, or class level can be used to avoid unnecessary warnings +due to the lazy initialization of fields – for example, due to a class implementing {spring-framework-api}/beans/factory/InitializingBean.html[`InitializingBean`]. - `@SuppressWarnings("NullAway") // Dataflow analysis limitation` can be used when NullAway dataflow analysis is not -able to detect that the path involving a nullness problem will never happen. +able to detect that the path involving a nullability problem will never happen. - `@SuppressWarnings("NullAway") // Lambda` can be used when NullAway does not take into account assertions performed outside of a lambda for the code path within the lambda. -- `@SuppressWarnings("NullAway") // Reflection` can be used for some reflection operations that are known returning -non-null values even if that can't be expressed by the API. -- `@SuppressWarnings("NullAway") // Well-known map keys` can be used when `Map#get` invocations are done with keys known -to be present and non-null related values inserted previously. -- `@SuppressWarnings("NullAway") // Overridden method does not define nullness` can be used when the super class does -not define nullness (typically when the super class is coming from a dependency). +- `@SuppressWarnings("NullAway") // Reflection` can be used for some reflection operations that are known to return +non-null values even if that cannot be expressed by the API. +- `@SuppressWarnings("NullAway") // Well-known map keys` can be used when `Map#get` invocations are performed with keys that are known +to be present and when non-null related values have been inserted previously. +- `@SuppressWarnings("NullAway") // Overridden method does not define nullability` can be used when the superclass does +not define nullability (typically when the superclass comes from a dependency). [[null-safety-migrating]] @@ -155,30 +158,30 @@ not define nullness (typically when the super class is coming from a dependency) Spring null-safety annotations {spring-framework-api}/lang/Nullable.html[`@Nullable`], {spring-framework-api}/lang/NonNull.html[`@NonNull`], {spring-framework-api}/lang/NonNullApi.html[`@NonNullApi`], and -{spring-framework-api}/lang/NonNullFields.html[`@NonNullFields`] in the `org.springframework.lang` package have been -introduced in Spring Framework 5 when JSpecify did not exist and the best option was to leverage JSR 305 (a dormant -but widespread JSR) meta-annotations. They are deprecated as of Spring Framework 7 in favor of +{spring-framework-api}/lang/NonNullFields.html[`@NonNullFields`] in the `org.springframework.lang` package were +introduced in Spring Framework 5 when JSpecify did not exist, and the best option at that time was to leverage +meta-annotations from JSR 305 (a dormant but widespread JSR). They are deprecated as of Spring Framework 7 in favor of https://jspecify.dev/docs/start-here/[JSpecify] annotations, which provide significant enhancements such as properly -defined specifications, a canonical dependency with no split-package issue, better tooling, better Kotlin integration -and the capability to specify the nullness more precisely for more use cases. +defined specifications, a canonical dependency with no split-package issues, better tooling, better Kotlin integration, +and the capability to specify nullability more precisely for more use cases. -A key difference is that Spring null-safety annotations, following JSR 305 semantics, apply to fields, -parameters and return values while JSpecify annotations apply to type usages. This subtle difference -is in practice pretty significant, as it allows for example to differentiate the nullness of elements from the -nullness of arrays/varargs as well as defining the nullness of generic types. +A key difference is that Spring's deprecated null-safety annotations, which follow JSR 305 semantics, apply to fields, +parameters, and return values; while JSpecify annotations apply to type usage. This subtle difference +is in practice pretty significant, as it allows developers to differentiate between the nullness of elements and the +nullness of arrays/varargs as well as to define the nullness of generic types. -That means array and varargs null-safety declarations have to be updated to keep the same semantic. For example +That means array and varargs null-safety declarations have to be updated to keep the same semantics. For example `@Nullable Object[] array` with Spring annotations needs to be changed to `Object @Nullable [] array` with JSpecify -annotations. Same for varargs. +annotations. The same applies to varargs. -It is also recommended to move field and return value annotations closer to the type, for example: +It is also recommended to move field and return value annotations closer to the type and on the same line, for example: - For fields, instead of `@Nullable private String field` with Spring annotations, use `private @Nullable String field` with JSpecify annotations. -- For return values, instead of `@Nullable public String method()` with Spring annotations, use +- For method return types, instead of `@Nullable public String method()` with Spring annotations, use `public @Nullable String method()` with JSpecify annotations. -Also, with JSpecify, you don't need to specify `@NonNull` when overriding a type usage annotated with `@Nullable` in the +Also, with JSpecify, you do not need to specify `@NonNull` when overriding a type usage annotated with `@Nullable` in the super method to "undo" the nullable declaration in null-marked code. Just declare it unannotated and the null-marked defaults (a type usage is considered non-null unless explicitly annotated as nullable) will apply. diff --git a/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc b/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc index 5d087e5641..f5d83d4ad7 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc @@ -399,7 +399,7 @@ A `ConstraintViolation` on the `degrees` method parameter is adapted to a `MessageSourceResolvable` with the following: - Error codes `"Max.myService#addStudent.degrees"`, `"Max.degrees"`, `"Max.int"`, `"Max"` -- Message arguments "degrees2 and 2 (the field name and the constraint attribute) +- Message arguments "degrees" and 2 (the field name and the constraint attribute) - Default message "must be less than or equal to 2" To customize the above default message, you can add a property such as: diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc index 96a6023dac..83ccd98d84 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc @@ -78,27 +78,27 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - @Configuration - public class DataSourceConfig { + @Configuration + public class DataSourceConfig { - @Bean - public DataSource dataSource() { - return new EmbeddedDatabaseBuilder() - .setDatabaseConfigurer(EmbeddedDatabaseConfigurers - .customizeConfigurer(H2, this::customize)) - .addScript("schema.sql") - .build(); - } + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .setDatabaseConfigurer(EmbeddedDatabaseConfigurers + .customizeConfigurer(H2, this::customize)) + .addScript("schema.sql") + .build(); + } - private EmbeddedDatabaseConfigurer customize(EmbeddedDatabaseConfigurer defaultConfigurer) { - return new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) { - @Override - public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { - super.configureConnectionProperties(properties, databaseName); - properties.setDriverClass(CustomDriver.class); - } - }; - } + private EmbeddedDatabaseConfigurer customize(EmbeddedDatabaseConfigurer defaultConfigurer) { + return new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) { + @Override + public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { + super.configureConnectionProperties(properties, databaseName); + properties.setDriverClass(CustomDriver.class); + } + }; + } } ---- diff --git a/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc b/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc index c27fd7ec45..086562d73b 100644 --- a/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc @@ -136,7 +136,7 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- Mono completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);") - .then(); + .then(); ---- Kotlin:: @@ -144,7 +144,7 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);") - .await() + .await() ---- ====== @@ -173,7 +173,7 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- Mono> first = client.sql("SELECT id, name FROM person") - .fetch().first(); + .fetch().first(); ---- Kotlin:: @@ -181,7 +181,7 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- val first = client.sql("SELECT id, name FROM person") - .fetch().awaitSingle() + .fetch().awaitSingle() ---- ====== @@ -194,8 +194,8 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- Mono> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn") - .bind("fn", "Joe") - .fetch().first(); + .bind("fn", "Joe") + .fetch().first(); ---- Kotlin:: @@ -203,8 +203,8 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- val first = client.sql("SELECT id, name FROM person WHERE first_name = :fn") - .bind("fn", "Joe") - .fetch().awaitSingle() + .bind("fn", "Joe") + .fetch().awaitSingle() ---- ====== @@ -240,8 +240,8 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- Flux names = client.sql("SELECT name FROM person") - .map(row -> row.get("name", String.class)) - .all(); + .map(row -> row.get("name", String.class)) + .all(); ---- Kotlin:: @@ -249,8 +249,8 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- val names = client.sql("SELECT name FROM person") - .map{ row: Row -> row.get("name", String.class) } - .flow() + .map{ row: Row -> row.get("name", String.class) } + .flow() ---- ====== @@ -301,8 +301,8 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- Mono affectedRows = client.sql("UPDATE person SET first_name = :fn") - .bind("fn", "Joe") - .fetch().rowsUpdated(); + .bind("fn", "Joe") + .fetch().rowsUpdated(); ---- Kotlin:: @@ -310,8 +310,8 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- val affectedRows = client.sql("UPDATE person SET first_name = :fn") - .bind("fn", "Joe") - .fetch().awaitRowsUpdated() + .bind("fn", "Joe") + .fetch().awaitRowsUpdated() ---- ====== @@ -337,9 +337,9 @@ The following example shows parameter binding for a query: [source,java] ---- - db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") - .bind("id", "joe") - .bind("name", "Joe") + db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bind("id", "joe") + .bind("name", "Joe") .bind("age", 34); ---- @@ -369,9 +369,9 @@ Indices are zero based. [source,java] ---- - db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") - .bind(0, "joe") - .bind(1, "Joe") + db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bind(0, "joe") + .bind(1, "Joe") .bind(2, 34); ---- @@ -379,9 +379,9 @@ In case your application is binding to many parameters, the same can be achieved [source,java] ---- - List values = List.of("joe", "Joe", 34); - db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") - .bindValues(values); + List values = List.of("joe", "Joe", 34); + db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bindValues(values); ---- @@ -428,7 +428,7 @@ Java:: tuples.add(new Object[] {"Ann", 50}); client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)") - .bind("tuples", tuples); + .bind("tuples", tuples); ---- Kotlin:: @@ -440,7 +440,7 @@ Kotlin:: tuples.add(arrayOf("Ann", 50)) client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)") - .bind("tuples", tuples) + .bind("tuples", tuples) ---- ====== @@ -455,7 +455,7 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)") - .bind("ages", Arrays.asList(35, 50)); + .bind("ages", Arrays.asList(35, 50)); ---- Kotlin:: @@ -463,7 +463,7 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)") - .bind("ages", arrayOf(35, 50)) + .bind("ages", arrayOf(35, 50)) ---- ====== @@ -490,9 +490,9 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") - .filter((s, next) -> next.execute(s.returnGeneratedValues("id"))) - .bind("name", …) - .bind("state", …); + .filter((s, next) -> next.execute(s.returnGeneratedValues("id"))) + .bind("name", …) + .bind("state", …); ---- Kotlin:: @@ -516,10 +516,10 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") - .filter(statement -> s.returnGeneratedValues("id")); + .filter(statement -> s.returnGeneratedValues("id")); client.sql("SELECT id, name, state FROM table") - .filter(statement -> s.fetchSize(25)); + .filter(statement -> s.fetchSize(25)); ---- Kotlin:: @@ -527,10 +527,10 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") - .filter { statement -> s.returnGeneratedValues("id") } + .filter { statement -> s.returnGeneratedValues("id") } client.sql("SELECT id, name, state FROM table") - .filter { statement -> s.fetchSize(25) } + .filter { statement -> s.fetchSize(25) } ---- ====== diff --git a/framework-docs/modules/ROOT/pages/integration/aot-cache.adoc b/framework-docs/modules/ROOT/pages/integration/aot-cache.adoc new file mode 100644 index 0000000000..8ef9878fa3 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/integration/aot-cache.adoc @@ -0,0 +1,105 @@ +[[aot-cache]] += JVM AOT Cache +:page-aliases: integration/class-data-sharing.adoc +:page-aliases: integration/cds.adoc + +The ahead-of-time cache is a JVM feature introduced in Java 24 via the +https://openjdk.org/jeps/483[JEP 483] that can help reduce the startup time and memory +footprint of Java applications. AOT cache is a natural evolution of https://docs.oracle.com/en/java/javase/17/vm/class-data-sharing.html[Class Data Sharing (CDS)]. +Spring Framework supports both CDS and AOT cache, and it is recommended that you use the +later if available in the JVM version your are using (Java 24+). + +To use this feature, an AOT cache should be created for the particular classpath of the +application. It is possible to create this cache on the deployed instance, or during a +training run performed for example when packaging the application thanks to an hook-point +provided by the Spring Framework to ease such use case. Once the cache is available, users +should opt in to use it via a JVM flag. + +NOTE: If you are using Spring Boot, it is highly recommended to leverage its +{spring-boot-docs-ref}/packaging/efficient.html#packaging.efficient.unpacking[executable JAR unpacking support] +which is designed to fulfill the class loading requirements of both AOT cache and CDS. + +== Creating the cache + +An AOT cache can typically be created when the application exits. The Spring Framework +provides a mode of operation where the process can exit automatically once the +`ApplicationContext` has refreshed. In this mode, all non-lazy initialized singletons +have been instantiated, and `InitializingBean#afterPropertiesSet` callbacks have been +invoked; but the lifecycle has not started, and the `ContextRefreshedEvent` has not yet +been published. + +To create the cache during the training run, it is possible to specify the `-Dspring.context.exit=onRefresh` +JVM flag to start then exit your Spring application once the +`ApplicationContext` has refreshed: + + +-- +[tabs] +====== +AOT cache:: ++ +[source,bash,subs="verbatim,quotes"] +---- +# Both commands need to be run with the same classpath +java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -Dspring.context.exit=onRefresh ... +java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot ... +---- + +CDS:: ++ +[source,bash,subs="verbatim,quotes"] +---- +# To create a CDS archive, your JDK/JRE must have a base image +java -XX:ArchiveClassesAtExit=app.jsa -Dspring.context.exit=onRefresh ... +---- +====== +-- + +== Using the cache + +Once the cache file has been created, you can use it to start your application faster: + +-- +[tabs] +====== +AOT cache:: ++ +[source,bash,subs="verbatim"] +---- +# With the same classpath (or a superset) tan the training run +java -XX:AOTCache=app.aot ... +---- + +CDS:: ++ +[source,bash,subs="verbatim"] +---- +# With the same classpath (or a superset) tan the training run +java -XX:SharedArchiveFile=app.jsa ... +---- +====== +-- + +Pay attention to the logs and the startup time to check if the AOT cache is used successfully. +To figure out how effective the cache is, you can enable class loading logs by adding +an extra attribute: `-Xlog:class+load:file=aot-cache.log`. This creates a `aot-cache.log` with +every attempt to load a class and its source. Classes that are loaded from the cache should have +a "shared objects file" source, as shown in the following example: + +[source,shell,subs="verbatim"] +---- +[0.151s][info][class,load] org.springframework.core.env.EnvironmentCapable source: shared objects file +[0.151s][info][class,load] org.springframework.beans.factory.BeanFactory source: shared objects file +[0.151s][info][class,load] org.springframework.beans.factory.ListableBeanFactory source: shared objects file +[0.151s][info][class,load] org.springframework.beans.factory.HierarchicalBeanFactory source: shared objects file +[0.151s][info][class,load] org.springframework.context.MessageSource source: shared objects file +---- + +If the AOT cache can't be enabled or if you have a large number of classes that are not loaded from +the cache, make sure that the following conditions are fulfilled when creating and using the cache: + + - The very same JVM must be used. + - The classpath must be specified as a JAR or a list of JARs, and avoid the usage of directories and `*` wildcard characters. + - The timestamps of the JARs must be preserved. + - When using the cache, the classpath must be the same than the one used to create it, in the same order. +Additional JARs or directories can be specified *at the end* (but won't be cached). diff --git a/framework-docs/modules/ROOT/pages/integration/cds.adoc b/framework-docs/modules/ROOT/pages/integration/cds.adoc deleted file mode 100644 index aeffe326c1..0000000000 --- a/framework-docs/modules/ROOT/pages/integration/cds.adoc +++ /dev/null @@ -1,72 +0,0 @@ -[[cds]] -= CDS -:page-aliases: integration/class-data-sharing.adoc - -Class Data Sharing (CDS) is a https://docs.oracle.com/en/java/javase/17/vm/class-data-sharing.html[JVM feature] -that can help reduce the startup time and memory footprint of Java applications. - -To use this feature, a CDS archive should be created for the particular classpath of the -application. The Spring Framework provides a hook-point to ease the creation of the -archive. Once the archive is available, users should opt in to use it via a JVM flag. - -== Creating the CDS Archive - -A CDS archive for an application can be created when the application exits. The Spring -Framework provides a mode of operation where the process can exit automatically once the -`ApplicationContext` has refreshed. In this mode, all non-lazy initialized singletons -have been instantiated, and `InitializingBean#afterPropertiesSet` callbacks have been -invoked; but the lifecycle has not started, and the `ContextRefreshedEvent` has not yet -been published. - -To create the archive, two additional JVM flags must be specified: - -* `-XX:ArchiveClassesAtExit=application.jsa`: creates the CDS archive on exit -* `-Dspring.context.exit=onRefresh`: starts and then immediately exits your Spring - application as described above - -To create a CDS archive, your JDK/JRE must have a base image. If you add the flags above to -your startup script, you may get a warning that looks like this: - -[source,shell,indent=0,subs="verbatim"] ----- - -XX:ArchiveClassesAtExit is unsupported when base CDS archive is not loaded. Run with -Xlog:cds for more info. ----- - -The base CDS archive is usually provided out-of-the-box, but can also be created if needed by issuing the following -command: - -[source,shell,indent=0,subs="verbatim"] ----- - $ java -Xshare:dump ----- - -== Using the Archive - -Once the archive is available, add `-XX:SharedArchiveFile=application.jsa` to your startup -script to use it, assuming an `application.jsa` file in the working directory. - -To check if the CDS cache is effective, you can use (for testing purposes only, not in production) `-Xshare:on` which -prints an error message and exits if CDS can't be enabled. - -To figure out how effective the cache is, you can enable class loading logs by adding -an extra attribute: `-Xlog:class+load:file=cds.log`. This creates a `cds.log` with every -attempt to load a class and its source. Classes that are loaded from the cache should have -a "shared objects file" source, as shown in the following example: - -[source,shell,indent=0,subs="verbatim"] ----- - [0.064s][info][class,load] org.springframework.core.env.EnvironmentCapable source: shared objects file (top) - [0.064s][info][class,load] org.springframework.beans.factory.BeanFactory source: shared objects file (top) - [0.064s][info][class,load] org.springframework.beans.factory.ListableBeanFactory source: shared objects file (top) - [0.064s][info][class,load] org.springframework.beans.factory.HierarchicalBeanFactory source: shared objects file (top) - [0.065s][info][class,load] org.springframework.context.MessageSource source: shared objects file (top) ----- - -If CDS can't be enabled or if you have a large number of classes that are not loaded from the cache, make sure that -the following conditions are fulfilled when creating and using the archive: - - - The very same JVM must be used. - - The classpath must be specified as a list of JARs, and avoid the usage of directories and `*` wildcard characters. - - The timestamps of the JARs must be preserved. - - When using the archive, the classpath must be the same than the one used to create the archive, in the same order. -Additional JARs or directories can be specified *at the end* (but won't be cached). diff --git a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc index d6a143eab1..057cee0d2c 100644 --- a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc +++ b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc @@ -3,26 +3,34 @@ The Spring Framework provides the following choices for making calls to REST endpoints: -* xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] - synchronous client with a fluent API. -* xref:integration/rest-clients.adoc#rest-webclient[`WebClient`] - non-blocking, reactive client with fluent API. -* xref:integration/rest-clients.adoc#rest-resttemplate[`RestTemplate`] - synchronous client with template method API. -* xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] - annotated interface with generated, dynamic proxy implementation. +* xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] -- synchronous client with a fluent API +* xref:integration/rest-clients.adoc#rest-webclient[`WebClient`] -- non-blocking, reactive client with fluent API +* xref:integration/rest-clients.adoc#rest-resttemplate[`RestTemplate`] -- synchronous client with template method API +* xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface Clients] -- annotated interface backed by generated proxy [[rest-restclient]] == `RestClient` -The `RestClient` is a synchronous HTTP client that offers a modern, fluent API. -It offers an abstraction over HTTP libraries that allows for convenient conversion from a Java object to an HTTP request, and the creation of objects from an HTTP response. +`RestClient` is a synchronous HTTP client that provides a fluent API to perform requests. +It serves as an abstraction over HTTP libraries, and handles conversion of HTTP request and response content to and from higher level Java objects. -=== Creating a `RestClient` +=== Create a `RestClient` -The `RestClient` is created using one of the static `create` methods. -You can also use `builder()` to get a builder with further options, such as specifying which HTTP library to use (see <>) and which message converters to use (see <>), setting a default URI, default path variables, default request headers, or `uriBuilderFactory`, or registering interceptors and initializers. +`RestClient` has static `create` shortcut methods. +It also exposes a `builder()` with further options: -Once created (or built), the `RestClient` can be used safely by multiple threads. +- select the HTTP library to use, see <> +- configure message converters, see <> +- set a baseUrl +- set default request headers, cookies, path variables, API version +- configure an `ApiVersionInserter` +- register interceptors +- register request initializers -The following sample shows how to create a default `RestClient`, and how to build a custom one. +Once created, a `RestClient` is safe to use in multiple threads. + +The below shows how to create or build a `RestClient`: [tabs] ====== @@ -30,50 +38,54 @@ Java:: + [source,java,indent=0,subs="verbatim"] ---- -RestClient defaultClient = RestClient.create(); - -RestClient customClient = RestClient.builder() - .requestFactory(new HttpComponentsClientHttpRequestFactory()) - .messageConverters(converters -> converters.add(new MyCustomMessageConverter())) - .baseUrl("https://example.com") - .defaultUriVariables(Map.of("variable", "foo")) - .defaultHeader("My-Header", "Foo") - .defaultCookie("My-Cookie", "Bar") - .requestInterceptor(myCustomInterceptor) - .requestInitializer(myCustomInitializer) - .build(); + RestClient defaultClient = RestClient.create(); + + RestClient customClient = RestClient.builder() + .requestFactory(new HttpComponentsClientHttpRequestFactory()) + .messageConverters(converters -> converters.add(new MyCustomMessageConverter())) + .baseUrl("https://example.com") + .defaultUriVariables(Map.of("variable", "foo")) + .defaultHeader("My-Header", "Foo") + .defaultCookie("My-Cookie", "Bar") + .defaultVersion("1.2") + .apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build()) + .requestInterceptor(myCustomInterceptor) + .requestInitializer(myCustomInitializer) + .build(); ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim"] ---- -val defaultClient = RestClient.create() - -val customClient = RestClient.builder() - .requestFactory(HttpComponentsClientHttpRequestFactory()) - .messageConverters { converters -> converters.add(MyCustomMessageConverter()) } - .baseUrl("https://example.com") - .defaultUriVariables(mapOf("variable" to "foo")) - .defaultHeader("My-Header", "Foo") - .defaultCookie("My-Cookie", "Bar") - .requestInterceptor(myCustomInterceptor) - .requestInitializer(myCustomInitializer) - .build() + val defaultClient = RestClient.create() + + val customClient = RestClient.builder() + .requestFactory(HttpComponentsClientHttpRequestFactory()) + .messageConverters { converters -> converters.add(MyCustomMessageConverter()) } + .baseUrl("https://example.com") + .defaultUriVariables(mapOf("variable" to "foo")) + .defaultHeader("My-Header", "Foo") + .defaultCookie("My-Cookie", "Bar") + .defaultVersion("1.2") + .apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build()) + .requestInterceptor(myCustomInterceptor) + .requestInitializer(myCustomInitializer) + .build() ---- ====== -=== Using the `RestClient` +=== Use the `RestClient` -When making an HTTP request with the `RestClient`, the first thing to specify is which HTTP method to use. -This can be done with `method(HttpMethod)` or with the convenience methods `get()`, `head()`, `post()`, and so on. +To perform an HTTP request, first specify the HTTP method to use. +Use the convenience methods like `get()`, `head()`, `post()`, and others, or `method(HttpMethod)`. ==== Request URL -Next, the request URI can be specified with the `uri` methods. -This step is optional and can be skipped if the `RestClient` is configured with a default URI. +Next, specify the request URI with the `uri` methods. +This is optional, and you can skip this step if you configured a baseUrl through the builder. The URL is typically specified as a `String`, with optional URI template variables. -The following example configures a GET request to `https://example.com/orders/42`: +The following shows how to perform a request: [tabs] ====== @@ -81,20 +93,20 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -int id = 42; -restClient.get() - .uri("https://example.com/orders/{id}", id) - .... + int id = 42; + restClient.get() + .uri("https://example.com/orders/{id}", id) + // ... ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -val id = 42 -restClient.get() - .uri("https://example.com/orders/{id}", id) - ... + val id = 42 + restClient.get() + .uri("https://example.com/orders/{id}", id) + // ... ---- ====== @@ -108,6 +120,7 @@ For more details on working with and encoding URIs, see xref:web/webmvc/mvc-uri- If necessary, the HTTP request can be manipulated by adding request headers with `header(String, String)`, `headers(Consumer`, or with the convenience methods `accept(MediaType...)`, `acceptCharset(Charset...)` and so on. For HTTP requests that can contain a body (`POST`, `PUT`, and `PATCH`), additional methods are available: `contentType(MediaType)`, and `contentLength(long)`. +You can set an API version for the request if the client is configured with `ApiVersionInserter`. The request body itself can be set by `body(Object)`, which internally uses <>. Alternatively, the request body can be set using a `ParameterizedTypeReference`, allowing you to use generics. @@ -133,12 +146,12 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -String result = restClient.get() <1> - .uri("https://example.com") <2> - .retrieve() <3> - .body(String.class); <4> - -System.out.println(result); <5> + String result = restClient.get() <1> + .uri("https://example.com") <2> + .retrieve() <3> + .body(String.class); <4> + + System.out.println(result); <5> ---- <1> Set up a GET request <2> Specify the URL to connect to @@ -150,12 +163,12 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -val result= restClient.get() <1> - .uri("https://example.com") <2> - .retrieve() <3> - .body() <4> - -println(result) <5> + val result= restClient.get() <1> + .uri("https://example.com") <2> + .retrieve() <3> + .body() <4> + + println(result) <5> ---- <1> Set up a GET request <2> Specify the URL to connect to @@ -172,14 +185,14 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -ResponseEntity result = restClient.get() <1> - .uri("https://example.com") <1> - .retrieve() - .toEntity(String.class); <2> - -System.out.println("Response status: " + result.getStatusCode()); <3> -System.out.println("Response headers: " + result.getHeaders()); <3> -System.out.println("Contents: " + result.getBody()); <3> + ResponseEntity result = restClient.get() <1> + .uri("https://example.com") <1> + .retrieve() + .toEntity(String.class); <2> + + System.out.println("Response status: " + result.getStatusCode()); <3> + System.out.println("Response headers: " + result.getHeaders()); <3> + System.out.println("Contents: " + result.getBody()); <3> ---- <1> Set up a GET request for the specified URL <2> Convert the response into a `ResponseEntity` @@ -189,14 +202,14 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -val result = restClient.get() <1> - .uri("https://example.com") <1> - .retrieve() - .toEntity() <2> - -println("Response status: " + result.statusCode) <3> -println("Response headers: " + result.headers) <3> -println("Contents: " + result.body) <3> + val result = restClient.get() <1> + .uri("https://example.com") <1> + .retrieve() + .toEntity() <2> + + println("Response status: " + result.statusCode) <3> + println("Response headers: " + result.headers) <3> + println("Contents: " + result.body) <3> ---- <1> Set up a GET request for the specified URL <2> Convert the response into a `ResponseEntity` @@ -212,12 +225,12 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -int id = ...; -Pet pet = restClient.get() - .uri("https://petclinic.example.com/pets/{id}", id) <1> - .accept(APPLICATION_JSON) <2> - .retrieve() - .body(Pet.class); <3> + int id = ...; + Pet pet = restClient.get() + .uri("https://petclinic.example.com/pets/{id}", id) <1> + .accept(APPLICATION_JSON) <2> + .retrieve() + .body(Pet.class); <3> ---- <1> Using URI variables <2> Set the `Accept` header to `application/json` @@ -227,12 +240,12 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -val id = ... -val pet = restClient.get() - .uri("https://petclinic.example.com/pets/{id}", id) <1> - .accept(APPLICATION_JSON) <2> - .retrieve() - .body() <3> + val id = ... + val pet = restClient.get() + .uri("https://petclinic.example.com/pets/{id}", id) <1> + .accept(APPLICATION_JSON) <2> + .retrieve() + .body() <3> ---- <1> Using URI variables <2> Set the `Accept` header to `application/json` @@ -247,13 +260,13 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -Pet pet = ... <1> -ResponseEntity response = restClient.post() <2> - .uri("https://petclinic.example.com/pets/new") <2> - .contentType(APPLICATION_JSON) <3> - .body(pet) <4> - .retrieve() - .toBodilessEntity(); <5> + Pet pet = ... <1> + ResponseEntity response = restClient.post() <2> + .uri("https://petclinic.example.com/pets/new") <2> + .contentType(APPLICATION_JSON) <3> + .body(pet) <4> + .retrieve() + .toBodilessEntity(); <5> ---- <1> Create a `Pet` domain object <2> Set up a POST request, and the URL to connect to @@ -265,13 +278,13 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -val pet: Pet = ... <1> -val response = restClient.post() <2> - .uri("https://petclinic.example.com/pets/new") <2> - .contentType(APPLICATION_JSON) <3> - .body(pet) <4> - .retrieve() - .toBodilessEntity() <5> + val pet: Pet = ... <1> + val response = restClient.post() <2> + .uri("https://petclinic.example.com/pets/new") <2> + .contentType(APPLICATION_JSON) <3> + .body(pet) <4> + .retrieve() + .toBodilessEntity() <5> ---- <1> Create a `Pet` domain object <2> Set up a POST request, and the URL to connect to @@ -291,13 +304,13 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -String result = restClient.get() <1> - .uri("https://example.com/this-url-does-not-exist") <1> - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { <2> - throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <3> - }) - .body(String.class); + String result = restClient.get() <1> + .uri("https://example.com/this-url-does-not-exist") <1> + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { <2> + throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <3> + }) + .body(String.class); ---- <1> Create a GET request for a URL that returns a 404 status code <2> Set up a status handler for all 4xx status codes @@ -307,12 +320,12 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -val result = restClient.get() <1> - .uri("https://example.com/this-url-does-not-exist") <1> - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError) { _, response -> <2> - throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) } <3> - .body() + val result = restClient.get() <1> + .uri("https://example.com/this-url-does-not-exist") <1> + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError) { _, response -> <2> + throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) } <3> + .body() ---- <1> Create a GET request for a URL that returns a 404 status code <2> Set up a status handler for all 4xx status codes @@ -330,18 +343,18 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -Pet result = restClient.get() - .uri("https://petclinic.example.com/pets/{id}", id) - .accept(APPLICATION_JSON) - .exchange((request, response) -> { <1> - if (response.getStatusCode().is4xxClientError()) { <2> - throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <2> - } - else { - Pet pet = convertResponse(response); <3> - return pet; - } - }); + Pet result = restClient.get() + .uri("https://petclinic.example.com/pets/{id}", id) + .accept(APPLICATION_JSON) + .exchange((request, response) -> { <1> + if (response.getStatusCode().is4xxClientError()) { <2> + throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <2> + } + else { + Pet pet = convertResponse(response); <3> + return pet; + } + }); ---- <1> `exchange` provides the request and response <2> Throw an exception when the response has a 4xx status code @@ -351,17 +364,17 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -val result = restClient.get() - .uri("https://petclinic.example.com/pets/{id}", id) - .accept(MediaType.APPLICATION_JSON) - .exchange { request, response -> <1> - if (response.getStatusCode().is4xxClientError()) { <2> - throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) <2> - } else { - val pet: Pet = convertResponse(response) <3> - pet - } - } + val result = restClient.get() + .uri("https://petclinic.example.com/pets/{id}", id) + .accept(MediaType.APPLICATION_JSON) + .exchange { request, response -> <1> + if (response.getStatusCode().is4xxClientError()) { <2> + throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) <2> + } else { + val pet: Pet = convertResponse(response) <3> + pet + } + } ---- <1> `exchange` provides the request and response <2> Throw an exception when the response has a 4xx status code @@ -380,15 +393,14 @@ To serialize only a subset of the object properties, you can specify a {baeldung [source,java,indent=0,subs="verbatim"] ---- -MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23")); -value.setSerializationView(User.WithoutPasswordView.class); - -ResponseEntity response = restClient.post() // or RestTemplate.postForEntity - .contentType(APPLICATION_JSON) - .body(value) - .retrieve() - .toBodilessEntity(); - + MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23")); + value.setSerializationView(User.WithoutPasswordView.class); + + ResponseEntity response = restClient.post() // or RestTemplate.postForEntity + .contentType(APPLICATION_JSON) + .body(value) + .retrieve() + .toBodilessEntity(); ---- ==== Multipart @@ -398,24 +410,24 @@ For example: [source,java,indent=0,subs="verbatim"] ---- -MultiValueMap parts = new LinkedMultiValueMap<>(); - -parts.add("fieldPart", "fieldValue"); -parts.add("filePart", new FileSystemResource("...logo.png")); -parts.add("jsonPart", new Person("Jason")); - -HttpHeaders headers = new HttpHeaders(); -headers.setContentType(MediaType.APPLICATION_XML); -parts.add("xmlPart", new HttpEntity<>(myBean, headers)); - -// send using RestClient.post or RestTemplate.postForEntity + MultiValueMap parts = new LinkedMultiValueMap<>(); + + parts.add("fieldPart", "fieldValue"); + parts.add("filePart", new FileSystemResource("...logo.png")); + parts.add("jsonPart", new Person("Jason")); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); + parts.add("xmlPart", new HttpEntity<>(myBean, headers)); + + // send using RestClient.post or RestTemplate.postForEntity ---- In most cases, you do not have to specify the `Content-Type` for each part. The content type is determined automatically based on the `HttpMessageConverter` chosen to serialize it or, in the case of a `Resource`, based on the file extension. If necessary, you can explicitly provide the `MediaType` with an `HttpEntity` wrapper. -Once the `MultiValueMap` is ready, you can use it as the body of a `POST` request, using `RestClient.post().body(parts)` (or `RestTemplate.postForObject`). +Once the `MultiValueMap` is ready, you can use it as the body of a `POST` request, using `RestClient.post().body(parts)` (or `RestTemplate.postForObject`). If the `MultiValueMap` contains at least one non-`String` value, the `Content-Type` is set to `multipart/form-data` by the `FormHttpMessageConverter`. If the `MultiValueMap` has `String` values, the `Content-Type` defaults to `application/x-www-form-urlencoded`. @@ -845,15 +857,17 @@ It can be used to migrate from the latter to the former. [[rest-http-interface]] -== HTTP Interface +== HTTP Interface Clients -The Spring Framework lets you define an HTTP service as a Java interface with -`@HttpExchange` methods. You can pass such an interface to `HttpServiceProxyFactory` -to create a proxy which performs requests through an HTTP client such as `RestClient` -or `WebClient`. You can also implement the interface from an `@Controller` for server -request handling. +You can define an HTTP Service as a Java interface with `@HttpExchange` methods, and use +`HttpServiceProxyFactory` to create a client proxy from it for remote access over HTTP via +`RestClient`, `WebClient`, or `RestTemplate`. On the server side, an `@Controller` class +can implement the same interface to handle requests with +xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-httpexchange-annotation[@HttpExchange] +controller methods. -Start by creating the interface with `@HttpExchange` methods: + +First, create the Java interface: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -867,43 +881,7 @@ Start by creating the interface with `@HttpExchange` methods: } ---- -Now you can create a proxy that performs requests when methods are called. - -For `RestClient`: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - RestClient restClient = RestClient.builder().baseUrl("https://api.github.com/").build(); - RestClientAdapter adapter = RestClientAdapter.create(restClient); - HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); - - RepositoryService service = factory.createClient(RepositoryService.class); ----- - -For `WebClient`: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - WebClient webClient = WebClient.builder().baseUrl("https://api.github.com/").build(); - WebClientAdapter adapter = WebClientAdapter.create(webClient); - HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); - - RepositoryService service = factory.createClient(RepositoryService.class); ----- - -For `RestTemplate`: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - RestTemplate restTemplate = new RestTemplate(); - restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("https://api.github.com/")); - RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate); - HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); - - RepositoryService service = factory.createClient(RepositoryService.class); ----- - -`@HttpExchange` is supported at the type level where it applies to all methods: +Optionally, use `@HttpExchange` at the type level to declare common attributes for all methods: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -921,15 +899,46 @@ For `RestTemplate`: ---- +Next, configure the client and create the `HttpServiceProxyFactory`: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + // Using RestClient... + + RestClient restClient = RestClient.create("..."); + RestClientAdapter adapter = RestClientAdapter.create(restClient); + + // or WebClient... + + WebClient webClient = WebClient.create("..."); + WebClientAdapter adapter = WebClientAdapter.create(webClient); + + // or RestTemplate... + + RestTemplate restTemplate = new RestTemplate(); + RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate); + + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); +---- + +Now, you're ready to create client proxies: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + RepositoryService service = factory.createClient(RepositoryService.class); + // Use service methods for remote calls... +---- + + + [[rest-http-interface-method-parameters]] === Method Parameters -Annotated, HTTP exchange methods support flexible method signatures with the following -method parameters: +`@HttpExchange` methods support flexible method signatures with the following inputs: [cols="1,2", options="header"] |=== -| Method argument | Description +| Method parameter | Description | `URI` | Dynamically set the URL for the request, overriding the annotation's `url` attribute. @@ -990,29 +999,33 @@ Method parameters cannot be `null` unless the `required` attribute (where availa parameter annotation) is set to `false`, or the parameter is marked optional as determined by {spring-framework-api}/core/MethodParameter.html#isOptional()[`MethodParameter#isOptional`]. +`RestClientAdapter` provides additional support for a method parameter of type +`StreamingHttpOutputMessage.Body` that allows sending the request body by writing to an +`OutputStream`. + [[rest-http-interface.custom-resolver]] -=== Custom argument resolver +=== Custom Arguments -For more complex cases, HTTP interfaces do not support `RequestEntity` types as method parameters. -This would take over the entire HTTP request and not improve the semantics of the interface. -Instead of adding many method parameters, developers can combine them into a custom type -and configure a dedicated `HttpServiceArgumentResolver` implementation. - -In the following HTTP interface, we are using a custom `Search` type as a parameter: +You can configure a custom `HttpServiceArgumentResolver`. The example interface below +uses a custom `Search` method parameter type: include-code::./CustomHttpServiceArgumentResolver[tag=httpinterface,indent=0] -We can implement our own `HttpServiceArgumentResolver` that supports our custom `Search` type -and writes its data in the outgoing HTTP request. +A custom argument resolver could be implemented like this: include-code::./CustomHttpServiceArgumentResolver[tag=argumentresolver,indent=0] -Finally, we can use this argument resolver during the setup and use our HTTP interface. +To configure the custom argument resolver: include-code::./CustomHttpServiceArgumentResolver[tag=usage,indent=0] +TIP: By default, `RequestEntity` is not supported as a method parameter, instead encouraging +the use of more fine-grained method parameters for individual parts of the request. + + + [[rest-http-interface-return-values]] === Return Values @@ -1085,65 +1098,180 @@ depends on how the underlying HTTP client is configured. You can set a `blockTim value on the adapter level as well, but we recommend relying on timeout settings of the underlying HTTP client, which operates at a lower level and provides more control. +`RestClientAdapter` provides supports additional support for a return value of type +`InputStream` or `ResponseEntity` that provides access to the raw response +body content. + + [[rest-http-interface-exceptions]] === Error Handling -To customize error response handling, you need to configure the underlying HTTP client. - -For `RestClient`: - -By default, `RestClient` raises `RestClientException` for 4xx and 5xx HTTP status codes. -To customize this, register a response status handler that applies to all responses -performed through the client: +To customize error handling for HTTP Service client proxies, you can configure the +underlying client as needed. By default, clients raise an exception for 4xx and 5xx HTTP +status codes. To customize this, register a response status handler that applies to all +responses performed through the client as follows: [source,java,indent=0,subs="verbatim,quotes"] ---- + // For RestClient RestClient restClient = RestClient.builder() .defaultStatusHandler(HttpStatusCode::isError, (request, response) -> ...) .build(); - RestClientAdapter adapter = RestClientAdapter.create(restClient); - HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); ----- -For more details and options, such as suppressing error status codes, see the Javadoc of -`defaultStatusHandler` in `RestClient.Builder`. - -For `WebClient`: - -By default, `WebClient` raises `WebClientResponseException` for 4xx and 5xx HTTP status codes. -To customize this, register a response status handler that applies to all responses -performed through the client: - -[source,java,indent=0,subs="verbatim,quotes"] ----- + // or for WebClient... WebClient webClient = WebClient.builder() .defaultStatusHandler(HttpStatusCode::isError, resp -> ...) .build(); - WebClientAdapter adapter = WebClientAdapter.create(webClient); - HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(adapter).build(); + + // or for RestTemplate... + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setErrorHandler(myErrorHandler); + + RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate); + + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); ---- -For more details and options, such as suppressing error status codes, see the Javadoc of -`defaultStatusHandler` in `WebClient.Builder`. +For more details and options such as suppressing error status codes, see the reference +documentation for each client, as well as the Javadoc of `defaultStatusHandler` in +`RestClient.Builder` or `WebClient.Builder`, and the `setErrorHandler` of `RestTemplate`. -For `RestTemplate`: -By default, `RestTemplate` raises `RestClientException` for 4xx and 5xx HTTP status codes. -To customize this, register an error handler that applies to all responses -performed through the client: + +[[rest-http-interface-group-config]] +=== HTTP Interface Groups + +It's trivial to create client proxies with `HttpServiceProxyFactory`, but to have them +declared as beans leads to repetitive configuration. You may also have multiple +target hosts, and therefore multiple clients to configure, and even more client proxy +beans to create. + +To make it easier to work with interface clients at scale the Spring Framework provides +dedicated configuration support. It lets applications focus on identifying HTTP Services +by group, and customizing the client for each group, while the framework transparently +creates a registry of client proxies, and declares each proxy as a bean. + +An HTTP Service group is simply a set of interfaces that share the same client setup and +`HttpServiceProxyFactory` instance to create proxies. Typically, that means one group per +host, but you can have more than one group for the same target host in case the +underlying client needs to be configured differently. + +One way to declare HTTP Service groups is via `@ImportHttpServices` annotations in +`@Configuration` classes as shown below: [source,java,indent=0,subs="verbatim,quotes"] ---- - RestTemplate restTemplate = new RestTemplate(); - restTemplate.setErrorHandler(myErrorHandler); + @Configuration + @ImportHttpServices(group = "echo", types = {EchoServiceA.class, EchoServiceB.class}) // <1> + @ImportHttpServices(group = "greeting", basePackageClasses = GreetServiceA.class) // <2> + public class ClientConfig { + } - RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate); - HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); +---- +<1> Manually list interfaces for group "echo" +<2> Detect interfaces for group "greeting" under a base package + +It is also possible to declare groups programmatically by creating an HTTP Service +registrar and then importing it: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public class MyHttpServiceRegistrar extends AbstractHttpServiceRegistrar { // <1> + + @Override + protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) { + registry.forGroup("echo").register(EchoServiceA.class, EchoServiceB.class); // <2> + registry.forGroup("greeting").detectInBasePackages(GreetServiceA.class); // <3> + } + } + + @Configuration + @Import(MyHttpServiceRegistrar.class) // <4> + public class ClientConfig { + } + +---- +<1> Create extension class of `AbstractHttpServiceRegistrar` +<2> Manually list interfaces for group "echo" +<3> Detect interfaces for group "greeting" under a base package +<4> Import the registrar + +TIP: You can mix and match `@ImportHttpService` annotations with programmatic registrars, +and you can spread the imports across multiple configuration classes. All imports +contribute collaboratively the same, shared `HttpServiceProxyRegistry` instance. + +Once HTTP Service groups are declared, add an `HttpServiceGroupConfigurer` bean to +customize the client for each group. For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + @ImportHttpServices(group = "echo", types = {EchoServiceA.class, EchoServiceB.class}) + @ImportHttpServices(group = "greeting", basePackageClasses = GreetServiceA.class) + public class ClientConfig { + + @Bean + public RestClientHttpServiceGroupConfigurer groupConfigurer() { + return groups -> { + // configure client for group "echo" + groups.filterByName("echo").forEachClient((group, clientBuilder) -> ...); + + // configure the clients for all groups + groups.forEachClient((group, clientBuilder) -> ...); + + // configure client and proxy factory for each group + groups.forEachGroup((group, clientBuilder, factoryBuilder) -> ...); + }; + } + } ---- -For more details and options, see the Javadoc of `setErrorHandler` in `RestTemplate` and -the `ResponseErrorHandler` hierarchy. +TIP: Spring Boot uses an `HttpServiceGroupConfigurer` to add support for client properties +by HTTP Service group, Spring Security to add OAuth support, and Spring Cloud to add load +balancing. +As a result of the above, each client proxy is available as a bean that you can +conveniently autowire by type: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @RestController + public class EchoController { + + private final EchoService echoService; + + public EchoController(EchoService echoService) { + this.echoService = echoService; + } + + // ... + } +---- + +However, if there are multiple client proxies of the same type, e.g. the same interface +in multiple groups, then there is no unique bean of that type, and you cannot autowire by +type only. For such cases, you can work directly with the `HttpServiceProxyRegistry` that +holds all proxies, and obtain the ones you need by group: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @RestController + public class EchoController { + + private final EchoService echoService1; + + private final EchoService echoService2; + + public EchoController(HttpServiceProxyRegistry registry) { + this.echoService1 = registry.getClient("echo1", EchoService.class); // <1> + this.echoService2 = registry.getClient("echo2", EchoService.class); // <2> + } + + // ... + } +---- +<1> Access the `EchoService` client proxy for group "echo1" +<2> Access the `EchoService` client proxy for group "echo2" diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc index 9e09a69411..6893fb5a48 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc @@ -215,45 +215,43 @@ For suspending functions, a `TransactionalOperator.executeAndAwait` extension is [source,kotlin,indent=0] ---- - import org.springframework.transaction.reactive.executeAndAwait + import org.springframework.transaction.reactive.executeAndAwait - class PersonRepository(private val operator: TransactionalOperator) { + class PersonRepository(private val operator: TransactionalOperator) { - suspend fun initDatabase() = operator.executeAndAwait { - insertPerson1() - insertPerson2() - } + suspend fun initDatabase() = operator.executeAndAwait { + insertPerson1() + insertPerson2() + } - private suspend fun insertPerson1() { - // INSERT SQL statement - } + private suspend fun insertPerson1() { + // INSERT SQL statement + } - private suspend fun insertPerson2() { - // INSERT SQL statement - } - } + private suspend fun insertPerson2() { + // INSERT SQL statement + } + } ---- For Kotlin `Flow`, a `Flow.transactional` extension is provided. [source,kotlin,indent=0] ---- - import org.springframework.transaction.reactive.transactional + import org.springframework.transaction.reactive.transactional - class PersonRepository(private val operator: TransactionalOperator) { + class PersonRepository(private val operator: TransactionalOperator) { - fun updatePeople() = findPeople().map(::updatePerson).transactional(operator) + fun updatePeople() = findPeople().map(::updatePerson).transactional(operator) - private fun findPeople(): Flow { - // SELECT SQL statement - } + private fun findPeople(): Flow { + // SELECT SQL statement + } - private suspend fun updatePerson(person: Person): Person { - // UPDATE SQL statement - } - } + private suspend fun updatePerson(person: Person): Person { + // UPDATE SQL statement + } + } ---- - - diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc index 64da5a0b63..90b0d6fb0d 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc @@ -190,7 +190,7 @@ NOTE: If you use Spring Boot, you should probably use instead of `@Value` annotations. As an alternative, you can customize the property placeholder prefix by declaring the -following configuration beans: +following `PropertySourcesPlaceholderConfigurer` bean: [source,kotlin,indent=0] ---- @@ -200,8 +200,10 @@ following configuration beans: } ---- -You can customize existing code (such as Spring Boot actuators or `@LocalServerPort`) -that uses the `${...}` syntax, with configuration beans, as the following example shows: +You can support components (such as Spring Boot actuators or `@LocalServerPort`) that use +the standard `${...}` syntax alongside components that use the custom `%{...}` syntax by +declaring multiple `PropertySourcesPlaceholderConfigurer` beans, as the following example +shows: [source,kotlin,indent=0] ---- @@ -215,6 +217,9 @@ that uses the `${...}` syntax, with configuration beans, as the following exampl fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer() ---- +In addition, the default escape character can be changed or disabled globally by setting +the `spring.placeholder.escapeCharacter.default` property via a JVM system property (or +via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism). [[checked-exceptions]] @@ -296,17 +301,17 @@ for example when writing a `org.springframework.core.convert.converter.Converter [source,kotlin,indent=0] ---- -class ListOfFooConverter : Converter, CustomJavaList> { - // ... -} + class ListOfFooConverter : Converter, CustomJavaList> { + // ... + } ---- When converting any kind of objects, star projection with `*` can be used instead of `out Any`. [source,kotlin,indent=0] ---- -class ListOfAnyConverter : Converter, CustomJavaList<*>> { - // ... -} + class ListOfAnyConverter : Converter, CustomJavaList<*>> { + // ... + } ---- NOTE: Spring Framework does not leverage yet declaration-site variance type information for injecting beans, @@ -319,7 +324,7 @@ progresses. == Testing This section addresses testing with the combination of Kotlin and Spring Framework. -The recommended testing framework is https://junit.org/junit5/[JUnit 5] along with +The recommended testing framework is https://junit.org/junit5/[JUnit] along with https://mockk.io/[Mockk] for mocking. NOTE: If you are using Spring Boot, see @@ -330,7 +335,7 @@ NOTE: If you are using Spring Boot, see === Constructor injection As described in the xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[dedicated section], -JUnit Jupiter (JUnit 5) allows constructor injection of beans which is pretty useful with Kotlin +JUnit Jupiter allows constructor injection of beans which is pretty useful with Kotlin in order to use `val` instead of `lateinit var`. You can use {spring-framework-api}/test/context/TestConstructor.html[`@TestConstructor(autowireMode = AutowireMode.ALL)`] to enable autowiring for all parameters. @@ -340,13 +345,14 @@ file with a `spring.test.constructor.autowire.mode = all` property. [source,kotlin,indent=0] ---- -@SpringJUnitConfig(TestConfig::class) -@TestConstructor(autowireMode = AutowireMode.ALL) -class OrderServiceIntegrationTests(val orderService: OrderService, - val customerService: CustomerService) { - - // tests that use the injected OrderService and CustomerService -} + @SpringJUnitConfig(TestConfig::class) + @TestConstructor(autowireMode = AutowireMode.ALL) + class OrderServiceIntegrationTests( + val orderService: OrderService, + val customerService: CustomerService) { + + // tests that use the injected OrderService and CustomerService + } ---- @@ -354,7 +360,7 @@ class OrderServiceIntegrationTests(val orderService: OrderService, === `PER_CLASS` Lifecycle Kotlin lets you specify meaningful test function names between backticks (```). -With JUnit Jupiter (JUnit 5), Kotlin test classes can use the `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` +With JUnit Jupiter, Kotlin test classes can use the `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` annotation to enable single instantiation of test classes, which allows the use of `@BeforeAll` and `@AfterAll` annotations on non-static methods, which is a good fit for Kotlin. @@ -368,29 +374,29 @@ The following example demonstrates `@BeforeAll` and `@AfterAll` annotations on n @TestInstance(TestInstance.Lifecycle.PER_CLASS) class IntegrationTests { - val application = Application(8181) - val client = WebClient.create("http://localhost:8181") + val application = Application(8181) + val client = WebClient.create("http://localhost:8181") - @BeforeAll - fun beforeAll() { - application.start() - } + @BeforeAll + fun beforeAll() { + application.start() + } - @Test - fun `Find all users on HTML page`() { - client.get().uri("/users") - .accept(TEXT_HTML) - .retrieve() - .bodyToMono() - .test() - .expectNextMatches { it.contains("Foo") } - .verifyComplete() - } + @Test + fun `Find all users on HTML page`() { + client.get().uri("/users") + .accept(TEXT_HTML) + .retrieve() + .bodyToMono() + .test() + .expectNextMatches { it.contains("Foo") } + .verifyComplete() + } - @AfterAll - fun afterAll() { - application.stop() - } + @AfterAll + fun afterAll() { + application.stop() + } } ---- @@ -398,31 +404,32 @@ class IntegrationTests { [[specification-like-tests]] === Specification-like Tests -You can create specification-like tests with JUnit 5 and Kotlin. -The following example shows how to do so: +You can create specification-like tests with Kotlin and JUnit Jupiter's `@Nested` test +class support. The following example shows how to do so: [source,kotlin,indent=0] ---- -class SpecificationLikeTests { + class SpecificationLikeTests { + + @Nested + @DisplayName("a calculator") + inner class Calculator { - @Nested - @DisplayName("a calculator") - inner class Calculator { - val calculator = SampleCalculator() - - @Test - fun `should return the result of adding the first number to the second number`() { - val sum = calculator.sum(2, 4) - assertEquals(6, sum) - } - - @Test - fun `should return the result of subtracting the second number from the first number`() { - val subtract = calculator.subtract(4, 2) - assertEquals(2, subtract) - } - } -} + val calculator = SampleCalculator() + + @Test + fun `should return the result of adding the first number to the second number`() { + val sum = calculator.sum(2, 4) + assertEquals(6, sum) + } + + @Test + fun `should return the result of subtracting the second number from the first number`() { + val subtract = calculator.subtract(4, 2) + assertEquals(2, subtract) + } + } + } ---- diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc index 0b4a6e555c..a1aeafd5e5 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc @@ -1,8 +1,6 @@ [[kotlin-web]] = Web - - [[router-dsl]] == Router DSL @@ -16,27 +14,27 @@ These DSL let you write clean and idiomatic Kotlin code to build a `RouterFuncti [source,kotlin,indent=0] ---- -@Configuration -class RouterRouterConfiguration { - - @Bean - fun mainRouter(userHandler: UserHandler) = router { - accept(TEXT_HTML).nest { - GET("/") { ok().render("index") } - GET("/sse") { ok().render("sse") } - GET("/users", userHandler::findAllView) - } - "/api".nest { - accept(APPLICATION_JSON).nest { - GET("/users", userHandler::findAll) + @Configuration + class RouterRouterConfiguration { + + @Bean + fun mainRouter(userHandler: UserHandler) = router { + accept(TEXT_HTML).nest { + GET("/") { ok().render("index") } + GET("/sse") { ok().render("sse") } + GET("/users", userHandler::findAllView) } - accept(TEXT_EVENT_STREAM).nest { - GET("/users", userHandler::stream) + "/api".nest { + accept(APPLICATION_JSON).nest { + GET("/users", userHandler::findAll) + } + accept(TEXT_EVENT_STREAM).nest { + GET("/users", userHandler::stream) + } } + resources("/**", ClassPathResource("static/")) } - resources("/**", ClassPathResource("static/")) } -} ---- NOTE: This DSL is programmatic, meaning that it allows custom registration logic of beans @@ -55,22 +53,22 @@ idiomatic Kotlin API and to allow better discoverability (no usage of static met [source,kotlin,indent=0] ---- -val mockMvc: MockMvc = ... -mockMvc.get("/person/{name}", "Lee") { - secure = true - accept = APPLICATION_JSON - headers { - contentLanguage = Locale.FRANCE + val mockMvc: MockMvc = ... + mockMvc.get("/person/{name}", "Lee") { + secure = true + accept = APPLICATION_JSON + headers { + contentLanguage = Locale.FRANCE + } + principal = Principal { "foo" } + }.andExpect { + status { isOk } + content { contentType(APPLICATION_JSON) } + jsonPath("$.name") { value("Lee") } + content { json("""{"someBoolean": false}""", false) } + }.andDo { + print() } - principal = Principal { "foo" } -}.andExpect { - status { isOk } - content { contentType(APPLICATION_JSON) } - jsonPath("$.name") { value("Lee") } - content { json("""{"someBoolean": false}""", false) } -}.andDo { - print() -} ---- diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc index 41b16558e1..5ab998f969 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc @@ -2,8 +2,8 @@ = Spring JUnit Jupiter Testing Annotations The following annotations are supported when used in conjunction with the -xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] and JUnit Jupiter -(that is, the programming model in JUnit 5): +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] +and JUnit Jupiter (that is, the programming model in JUnit): * xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-springjunitconfig[`@SpringJUnitConfig`] * xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-springjunitwebconfig[`@SpringJUnitWebConfig`] diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit4.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit4.adoc index 17fdf5c9be..f7e41bb4f6 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit4.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit4.adoc @@ -1,9 +1,17 @@ [[integration-testing-annotations-junit4]] = Spring JUnit 4 Testing Annotations +[WARNING] +==== +JUnit 4 support is deprecated since Spring Framework 7.0 in favor of the +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] +and JUnit Jupiter. +==== + The following annotations are supported only when used in conjunction with the -xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-runner[SpringRunner], xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit 4 rules] -, or xref:testing/testcontext-framework/support-classes.adoc#testcontext-support-classes-junit4[Spring's JUnit 4 support classes]: +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-runner[SpringRunner], +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit 4 rules], or +xref:testing/testcontext-framework/support-classes.adoc#testcontext-support-classes-junit4[Spring's JUnit 4 support classes]: * xref:testing/annotations/integration-junit4.adoc#integration-testing-annotations-junit4-ifprofilevalue[`@IfProfileValue`] * xref:testing/annotations/integration-junit4.adoc#integration-testing-annotations-junit4-profilevaluesourceconfiguration[`@ProfileValueSourceConfiguration`] @@ -205,6 +213,3 @@ Kotlin:: ---- <1> Repeat this test ten times. ====== - - - diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-meta.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-meta.adoc index ce2381c7c1..a956a7c7bc 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-meta.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-meta.adoc @@ -140,8 +140,8 @@ Kotlin:: ====== If we write tests that use JUnit Jupiter, we can reduce code duplication even further, -since annotations in JUnit 5 can also be used as meta-annotations. Consider the following -example: +since annotations in JUnit Jupiter can also be used as meta-annotations. Consider the +following example: [tabs] ====== diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc index 153a3b0343..f92d5c584a 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc @@ -47,6 +47,21 @@ the same bean in several test classes, make sure to name the fields consistently creating unnecessary contexts. ==== +[WARNING] +==== +Using `@MockitoBean` or `@MockitoSpyBean` in conjunction with `@ContextHierarchy` can +lead to undesirable results since each `@MockitoBean` or `@MockitoSpyBean` will be +applied to all context hierarchy levels by default. To ensure that a particular +`@MockitoBean` or `@MockitoSpyBean` is applied to a single context hierarchy level, set +the `contextName` attribute to match a configured `@ContextConfiguration` name – for +example, `@MockitoBean(contextName = "app-config")` or +`@MockitoSpyBean(contextName = "app-config")`. + +See +xref:testing/testcontext-framework/ctx-management/hierarchies.adoc#testcontext-ctx-management-ctx-hierarchies-with-bean-overrides[context +hierarchies with bean overrides] for further details and examples. +==== + Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior. The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE` diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc index a9cc9ced52..4ec33c0c15 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc @@ -31,6 +31,19 @@ same bean in several tests, make sure to name the field consistently to avoid cr unnecessary contexts. ==== +[WARNING] +==== +Using `@TestBean` in conjunction with `@ContextHierarchy` can lead to undesirable results +since each `@TestBean` will be applied to all context hierarchy levels by default. To +ensure that a particular `@TestBean` is applied to a single context hierarchy level, set +the `contextName` attribute to match a configured `@ContextConfiguration` name – for +example, `@TestBean(contextName = "app-config")`. + +See +xref:testing/testcontext-framework/ctx-management/hierarchies.adoc#testcontext-ctx-management-ctx-hierarchies-with-bean-overrides[context +hierarchies with bean overrides] for further details and examples. +==== + [NOTE] ==== There are no restrictions on the visibility of `@TestBean` fields or factory methods. diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/async-requests.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/async-requests.adoc index 949b9ab8a9..f6f5d9f14f 100644 --- a/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/async-requests.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/async-requests.adoc @@ -26,16 +26,16 @@ Java:: @Test void test() throws Exception { - MvcResult mvcResult = this.mockMvc.perform(get("/path")) - .andExpect(status().isOk()) <1> - .andExpect(request().asyncStarted()) <2> - .andExpect(request().asyncResult("body")) <3> - .andReturn(); + MvcResult mvcResult = this.mockMvc.perform(get("/path")) + .andExpect(status().isOk()) <1> + .andExpect(request().asyncStarted()) <2> + .andExpect(request().asyncResult("body")) <3> + .andReturn(); - this.mockMvc.perform(asyncDispatch(mvcResult)) <4> - .andExpect(status().isOk()) <5> - .andExpect(content().string("body")); - } + this.mockMvc.perform(asyncDispatch(mvcResult)) <4> + .andExpect(status().isOk()) <5> + .andExpect(content().string("body")); + } ---- <1> Check response status is still unchanged <2> Async processing must have started diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc index 175af0f55e..d3a8df109d 100644 --- a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc @@ -166,7 +166,7 @@ following sections to make this pattern much easier to implement. == MockMvc and WebDriver Setup To use Selenium WebDriver with `MockMvc`, make sure that your project includes a test -dependency on `org.seleniumhq.selenium:selenium-htmlunit3-driver`. +dependency on `org.seleniumhq.selenium:htmlunit3-driver`. We can easily create a Selenium WebDriver that integrates with MockMvc by using the `MockMvcHtmlUnitDriverBuilder` as the following example shows: diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-client.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-client.adoc index 55738c214f..b145277f21 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-client.adoc +++ b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-client.adoc @@ -1,10 +1,26 @@ [[spring-mvc-test-client]] = Testing Client Applications -You can use client-side tests to test code that internally uses the `RestTemplate`. The -idea is to declare expected requests and to provide "`stub`" responses so that you can -focus on testing the code in isolation (that is, without running a server). The following -example shows how to do so: +To test code that uses the `RestClient` or `RestTemplate`, you can use a mock web server, such as +https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer] or +https://wiremock.org/[WireMock]. Mock web servers accept requests over HTTP like a regular +server, and that means you can test with the same HTTP client that is also configured in +the same way as in production, which is important because there are often subtle +differences in the way different clients handle network I/O. Another advantage of mock +web servers is the ability to simulate specific network issues and conditions at the +transport level, in combination with the client used in production. + +In addition to dedicated mock web servers, historically the Spring Framework has provided +a built-in option to test `RestClient` or `RestTemplate` through `MockRestServiceServer`. +This relies on configuring the client under test with a custom `ClientHttpRequestFactory` +backed by the mock server that is in turn set up to expect requests and send "`stub`" +responses so that you can focus on testing the code in isolation, without running a server. + +TIP: `MockRestServiceServer` predates the existence of mock web servers. At present, we +recommend using mock web servers for more complete testing of the transport layer and +network conditions. + +The following example shows an example of using `MockRestServiceServer`: [tabs] ====== diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework.adoc index 0cf24faa9a..13de70cdef 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework.adoc @@ -9,11 +9,11 @@ deal of importance on convention over configuration, with reasonable defaults th can override through annotation-based configuration. In addition to generic testing infrastructure, the TestContext framework provides -explicit support for JUnit 4, JUnit Jupiter (AKA JUnit 5), and TestNG. For JUnit 4 and -TestNG, Spring provides `abstract` support classes. Furthermore, Spring provides a custom -JUnit `Runner` and custom JUnit `Rules` for JUnit 4 and a custom `Extension` for JUnit -Jupiter that let you write so-called POJO test classes. POJO test classes are not -required to extend a particular class hierarchy, such as the `abstract` support classes. +explicit support for JUnit Jupiter, JUnit 4, and TestNG. For JUnit 4 and TestNG, Spring +provides `abstract` support classes. Furthermore, Spring provides a custom JUnit `Runner` +and custom JUnit `Rules` for JUnit 4 and a custom `Extension` for JUnit Jupiter that let +you write so-called POJO test classes. POJO test classes are not required to extend a +particular class hierarchy, such as the `abstract` support classes. The following section provides an overview of the internals of the TestContext framework. If you are interested only in using the framework and are not interested in extending it diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc index a75d6314aa..cec19b9185 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc @@ -4,7 +4,7 @@ Once the TestContext framework loads an `ApplicationContext` (or `WebApplicationContext`) for a test, that context is cached and reused for all subsequent tests that declare the same unique context configuration within the same test suite. To understand how caching -works, it is important to understand what is meant by "`unique`" and "`test suite.`" +works, it is important to understand what is meant by "unique" and "test suite." An `ApplicationContext` can be uniquely identified by the combination of configuration parameters that is used to load it. Consequently, the unique combination of configuration @@ -15,8 +15,8 @@ framework uses the following configuration parameters to build the context cache * `classes` (from `@ContextConfiguration`) * `contextInitializerClasses` (from `@ContextConfiguration`) * `contextCustomizers` (from `ContextCustomizerFactory`) – this includes - `@DynamicPropertySource` methods as well as various features from Spring Boot's - testing support such as `@MockBean` and `@SpyBean`. + `@DynamicPropertySource` methods, bean overrides (such as `@TestBean`, `@MockitoBean`, + `@MockitoSpyBean` etc.), as well as various features from Spring Boot's testing support. * `contextLoader` (from `@ContextConfiguration`) * `parent` (from `@ContextHierarchy`) * `activeProfiles` (from `@ActiveProfiles`) diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc index 1698c61692..f1af4efc3c 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc @@ -1,5 +1,5 @@ [[testcontext-context-customizers]] -= Configuration Configuration with Context Customizers += Context Configuration with Context Customizers A `ContextCustomizer` is responsible for customizing the supplied `ConfigurableApplicationContext` after bean definitions have been loaded into the context diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/hierarchies.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/hierarchies.adoc index c8d57c4276..22f97cc1a0 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/hierarchies.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/hierarchies.adoc @@ -22,8 +22,19 @@ given level in the hierarchy, the configuration resource type (that is, XML conf files or component classes) must be consistent. Otherwise, it is perfectly acceptable to have different levels in a context hierarchy configured using different resource types. -The remaining JUnit Jupiter based examples in this section show common configuration -scenarios for integration tests that require the use of context hierarchies. +[NOTE] +==== +If you use `@DirtiesContext` in a test whose context is configured as part of a context +hierarchy, you can use the `hierarchyMode` flag to control how the context cache is +cleared. + +For further details, see the discussion of `@DirtiesContext` in +xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations] +and the {spring-framework-api}/test/annotation/DirtiesContext.html[`@DirtiesContext`] javadoc. +==== + +The JUnit Jupiter based examples in this section show common configuration scenarios for +integration tests that require the use of context hierarchies. **Single test class with context hierarchy** -- @@ -229,12 +240,118 @@ Kotlin:: class ExtendedTests : BaseTests() {} ---- ====== - -.Dirtying a context within a context hierarchy -NOTE: If you use `@DirtiesContext` in a test whose context is configured as part of a -context hierarchy, you can use the `hierarchyMode` flag to control how the context cache -is cleared. For further details, see the discussion of `@DirtiesContext` in -xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations] and the -{spring-framework-api}/test/annotation/DirtiesContext.html[`@DirtiesContext`] javadoc. -- +[[testcontext-ctx-management-ctx-hierarchies-with-bean-overrides]] +**Context hierarchies with bean overrides** +-- +When `@ContextHierarchy` is used in conjunction with +xref:testing/testcontext-framework/bean-overriding.adoc[bean overrides] such as +`@TestBean`, `@MockitoBean`, or `@MockitoSpyBean`, it may be desirable or necessary to +have the override applied to a single level in the context hierarchy. To achieve that, +the bean override must specify a context name that matches a name configured via the +`name` attribute in `@ContextConfiguration`. + +The following test class configures the name of the second hierarchy level to be +`"user-config"` and simultaneously specifies that the `UserService` should be wrapped in +a Mockito spy in the context named `"user-config"`. Consequently, Spring will only +attempt to create the spy in the `"user-config"` context and will not attempt to create +the spy in the parent context. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @ExtendWith(SpringExtension.class) + @ContextHierarchy({ + @ContextConfiguration(classes = AppConfig.class), + @ContextConfiguration(classes = UserConfig.class, name = "user-config") + }) + class IntegrationTests { + + @MockitoSpyBean(contextName = "user-config") + UserService userService; + + // ... + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @ExtendWith(SpringExtension::class) + @ContextHierarchy( + ContextConfiguration(classes = [AppConfig::class]), + ContextConfiguration(classes = [UserConfig::class], name = "user-config")) + class IntegrationTests { + + @MockitoSpyBean(contextName = "user-config") + lateinit var userService: UserService + + // ... + } +---- +====== + +When applying bean overrides in different levels of the context hierarchy, you may need +to have all of the bean override instances injected into the test class in order to +interact with them — for example, to configure stubbing for mocks. However, `@Autowired` +will always inject a matching bean found in the lowest level of the context hierarchy. +Thus, to inject bean override instances from specific levels in the context hierarchy, +you need to annotate fields with appropriate bean override annotations and configure the +name of the context level. + +The following test class configures the names of the hierarchy levels to be `"parent"` +and `"child"`. It also declares two `PropertyService` fields that are configured to +create or replace `PropertyService` beans with Mockito mocks in the respective contexts, +named `"parent"` and `"child"`. Consequently, the mock from the `"parent"` context will +be injected into the `propertyServiceInParent` field, and the mock from the `"child"` +context will be injected into the `propertyServiceInChild` field. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @ExtendWith(SpringExtension.class) + @ContextHierarchy({ + @ContextConfiguration(classes = ParentConfig.class, name = "parent"), + @ContextConfiguration(classes = ChildConfig.class, name = "child") + }) + class IntegrationTests { + + @MockitoBean(contextName = "parent") + PropertyService propertyServiceInParent; + + @MockitoBean(contextName = "child") + PropertyService propertyServiceInChild; + + // ... + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @ExtendWith(SpringExtension::class) + @ContextHierarchy( + ContextConfiguration(classes = [ParentConfig::class], name = "parent"), + ContextConfiguration(classes = [ChildConfig::class], name = "child")) + class IntegrationTests { + + @MockitoBean(contextName = "parent") + lateinit var propertyServiceInParent: PropertyService + + @MockitoBean(contextName = "child") + lateinit var propertyServiceInChild: PropertyService + + // ... + } +---- +====== +-- diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/executing-sql.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/executing-sql.adoc index 967f774288..44613c3483 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/executing-sql.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/executing-sql.adoc @@ -250,7 +250,7 @@ Java:: @SqlGroup({ @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")), @Sql("/test-user-data.sql") - )} + }) void userTest() { // run code that uses the test schema and test data } diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/parallel-test-execution.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/parallel-test-execution.adoc index 6e3c268f63..c95363d946 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/parallel-test-execution.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/parallel-test-execution.adoc @@ -18,9 +18,9 @@ Do not run tests in parallel if the tests: * Use Spring Framework's `@DirtiesContext` support. * Use Spring Framework's `@MockitoBean` or `@MockitoSpyBean` support. * Use Spring Boot's `@MockBean` or `@SpyBean` support. -* Use JUnit 4's `@FixMethodOrder` support or any testing framework feature - that is designed to ensure that test methods run in a particular order. Note, - however, that this does not apply if entire test classes are run in parallel. +* Use JUnit Jupiter's `@TestMethodOrder` support or any testing framework feature that is + designed to ensure that test methods run in a particular order. Note, however, that + this does not apply if entire test classes are run in parallel. * Change the state of shared services or systems such as a database, message broker, filesystem, and others. This applies to both embedded and external systems. diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc index 1ee5856ecf..71f9da8acd 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc @@ -1,172 +1,15 @@ [[testcontext-support-classes]] = TestContext Framework Support Classes -This section describes the various classes that support the Spring TestContext Framework. +This section describes the various classes that support the Spring TestContext Framework +in JUnit and TestNG. -[[testcontext-junit4-runner]] -== Spring JUnit 4 Runner - -The Spring TestContext Framework offers full integration with JUnit 4 through a custom -runner (supported on JUnit 4.12 or higher). By annotating test classes with -`@RunWith(SpringJUnit4ClassRunner.class)` or the shorter `@RunWith(SpringRunner.class)` -variant, developers can implement standard JUnit 4-based unit and integration tests and -simultaneously reap the benefits of the TestContext framework, such as support for -loading application contexts, dependency injection of test instances, transactional test -method execution, and so on. If you want to use the Spring TestContext Framework with an -alternative runner (such as JUnit 4's `Parameterized` runner) or third-party runners -(such as the `MockitoJUnitRunner`), you can, optionally, use -xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's support for JUnit rules] instead. - -The following code listing shows the minimal requirements for configuring a test class to -run with the custom Spring `Runner`: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes"] ----- - @RunWith(SpringRunner.class) - @TestExecutionListeners({}) - public class SimpleTest { - - @Test - public void testMethod() { - // test logic... - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes"] ----- - @RunWith(SpringRunner::class) - @TestExecutionListeners - class SimpleTest { - - @Test - fun testMethod() { - // test logic... - } - } ----- -====== - -In the preceding example, `@TestExecutionListeners` is configured with an empty list, to -disable the default listeners, which otherwise would require an `ApplicationContext` to -be configured through `@ContextConfiguration`. - -[[testcontext-junit4-rules]] -== Spring JUnit 4 Rules - -The `org.springframework.test.context.junit4.rules` package provides the following JUnit -4 rules (supported on JUnit 4.12 or higher): - -* `SpringClassRule` -* `SpringMethodRule` - -`SpringClassRule` is a JUnit `TestRule` that supports class-level features of the Spring -TestContext Framework, whereas `SpringMethodRule` is a JUnit `MethodRule` that supports -instance-level and method-level features of the Spring TestContext Framework. - -In contrast to the `SpringRunner`, Spring's rule-based JUnit support has the advantage of -being independent of any `org.junit.runner.Runner` implementation and can, therefore, be -combined with existing alternative runners (such as JUnit 4's `Parameterized`) or -third-party runners (such as the `MockitoJUnitRunner`). - -To support the full functionality of the TestContext framework, you must combine a -`SpringClassRule` with a `SpringMethodRule`. The following example shows the proper way -to declare these rules in an integration test: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes"] ----- - // Optionally specify a non-Spring Runner via @RunWith(...) - @ContextConfiguration - public class IntegrationTest { - - @ClassRule - public static final SpringClassRule springClassRule = new SpringClassRule(); - - @Rule - public final SpringMethodRule springMethodRule = new SpringMethodRule(); - - @Test - public void testMethod() { - // test logic... - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes"] ----- - // Optionally specify a non-Spring Runner via @RunWith(...) - @ContextConfiguration - class IntegrationTest { - - @Rule - val springMethodRule = SpringMethodRule() - - @Test - fun testMethod() { - // test logic... - } - - companion object { - @ClassRule - val springClassRule = SpringClassRule() - } - } ----- -====== - -[[testcontext-support-classes-junit4]] -== JUnit 4 Support Classes - -The `org.springframework.test.context.junit4` package provides the following support -classes for JUnit 4-based test cases (supported on JUnit 4.12 or higher): - -* `AbstractJUnit4SpringContextTests` -* `AbstractTransactionalJUnit4SpringContextTests` - -`AbstractJUnit4SpringContextTests` is an abstract base test class that integrates the -Spring TestContext Framework with explicit `ApplicationContext` testing support in a -JUnit 4 environment. When you extend `AbstractJUnit4SpringContextTests`, you can access a -`protected` `applicationContext` instance variable that you can use to perform explicit -bean lookups or to test the state of the context as a whole. - -`AbstractTransactionalJUnit4SpringContextTests` is an abstract transactional extension of -`AbstractJUnit4SpringContextTests` that adds some convenience functionality for JDBC -access. This class expects a `javax.sql.DataSource` bean and a -`PlatformTransactionManager` bean to be defined in the `ApplicationContext`. When you -extend `AbstractTransactionalJUnit4SpringContextTests`, you can access a `protected` -`jdbcTemplate` instance variable that you can use to run SQL statements to query the -database. You can use such queries to confirm database state both before and after -running database-related application code, and Spring ensures that such queries run in -the scope of the same transaction as the application code. When used in conjunction with -an ORM tool, be sure to avoid xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives]. -As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support], -`AbstractTransactionalJUnit4SpringContextTests` also provides convenience methods that -delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`. -Furthermore, `AbstractTransactionalJUnit4SpringContextTests` provides an -`executeSqlScript(..)` method for running SQL scripts against the configured `DataSource`. - -TIP: These classes are a convenience for extension. If you do not want your test classes -to be tied to a Spring-specific class hierarchy, you can configure your own custom test -classes by using `@RunWith(SpringRunner.class)` or xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit rules] -. [[testcontext-junit-jupiter-extension]] == SpringExtension for JUnit Jupiter The Spring TestContext Framework offers full integration with the JUnit Jupiter testing -framework, introduced in JUnit 5. By annotating test classes with +framework, originally introduced in JUnit 5. By annotating test classes with `@ExtendWith(SpringExtension.class)`, you can implement standard JUnit Jupiter-based unit and integration tests and simultaneously reap the benefits of the TestContext framework, such as support for loading application contexts, dependency injection of test instances, @@ -177,14 +20,17 @@ following features above and beyond the feature set that Spring supports for JUn TestNG: * Dependency injection for test constructors, test methods, and test lifecycle callback - methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency Injection with the `SpringExtension`] for further details. + methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency + Injection with the `SpringExtension`] for further details. * Powerful support for link:https://junit.org/junit5/docs/current/user-guide/#extensions-conditions[conditional test execution] based on SpEL expressions, environment variables, system properties, and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in - xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] for further details and examples. + xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] + for further details and examples. * Custom composed annotations that combine annotations from Spring and JUnit Jupiter. See the `@TransactionalDevTestConfig` and `@TransactionalIntegrationTest` examples in - xref:testing/annotations/integration-meta.adoc[Meta-Annotation Support for Testing] for further details. + xref:testing/annotations/integration-meta.adoc[Meta-Annotation Support for Testing] for + further details. The following code listing shows how to configure a test class to use the `SpringExtension` in conjunction with `@ContextConfiguration`: @@ -226,8 +72,8 @@ Kotlin:: ---- ====== -Since you can also use annotations in JUnit 5 as meta-annotations, Spring provides the -`@SpringJUnitConfig` and `@SpringJUnitWebConfig` composed annotations to simplify the +Since you can also use annotations in JUnit Jupiter as meta-annotations, Spring provides +the `@SpringJUnitConfig` and `@SpringJUnitWebConfig` composed annotations to simplify the configuration of the test `ApplicationContext` and JUnit Jupiter. The following example uses `@SpringJUnitConfig` to reduce the amount of configuration @@ -307,7 +153,8 @@ Kotlin:: ====== See the documentation for `@SpringJUnitConfig` and `@SpringJUnitWebConfig` in -xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] for further details. +xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] +for further details. [[testcontext-junit-jupiter-di]] === Dependency Injection with the `SpringExtension` @@ -318,10 +165,9 @@ extension API from JUnit Jupiter, which lets Spring provide dependency injection constructors, test methods, and test lifecycle callback methods. Specifically, the `SpringExtension` can inject dependencies from the test's -`ApplicationContext` into test constructors and methods that are annotated with -Spring's `@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`, -`@AfterAll`, `@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`, -and others. +`ApplicationContext` into test constructors and methods that are annotated with Spring's +`@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`, `@AfterAll`, +`@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and others. [[testcontext-junit-jupiter-di-constructor]] @@ -341,8 +187,9 @@ autowirable if one of the following conditions is met (in order of precedence). attribute set to `ALL`. * The default _test constructor autowire mode_ has been changed to `ALL`. -See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`] for details on the use of -`@TestConstructor` and how to change the global _test constructor autowire mode_. +See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`] +for details on the use of `@TestConstructor` and how to change the global _test +constructor autowire mode_. WARNING: If the constructor for a test class is considered to be _autowirable_, Spring assumes the responsibility for resolving arguments for all parameters in the constructor. @@ -407,8 +254,9 @@ Kotlin:: Note that this feature lets test dependencies be `final` and therefore immutable. If the `spring.test.constructor.autowire.mode` property is to `all` (see -xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]), we can omit the declaration of -`@Autowired` on the constructor in the previous example, resulting in the following. +xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]), +we can omit the declaration of `@Autowired` on the constructor in the previous example, +resulting in the following. [tabs] ====== @@ -553,17 +401,19 @@ honor `@NestedTestConfiguration` semantics. In order to allow development teams to change the default to `OVERRIDE` – for example, for compatibility with Spring Framework 5.0 through 5.2 – the default mode can be changed globally via a JVM system property or a `spring.properties` file in the root of the -classpath. See the xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration["Changing the default enclosing configuration inheritance mode"] - note for details. +classpath. See the +xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration["Changing the default enclosing configuration inheritance mode"] +note for details. Although the following "Hello World" example is very simplistic, it shows how to declare common configuration on a top-level class that is inherited by its `@Nested` test classes. In this particular example, only the `TestConfig` configuration class is inherited. Each nested test class provides its own set of active profiles, resulting in a distinct `ApplicationContext` for each nested test class (see -xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching] for details). Consult the list of -xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[supported annotations] to see -which annotations can be inherited in `@Nested` test classes. +xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching] for details). +Consult the list of +xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[supported annotations] +to see which annotations can be inherited in `@Nested` test classes. [tabs] ====== @@ -626,8 +476,198 @@ Kotlin:: ---- ====== + +[[testcontext-junit4-support]] +== JUnit 4 Support + +[[testcontext-junit4-runner]] +=== Spring JUnit 4 Runner + +[WARNING] +==== +JUnit 4 is officially in maintenance mode, and JUnit 4 support in Spring is deprecated +since Spring Framework 7.0 in favor of the +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] +and JUnit Jupiter. +==== + +The Spring TestContext Framework offers full integration with JUnit 4 through a custom +runner (supported on JUnit 4.12 or higher). By annotating test classes with +`@RunWith(SpringJUnit4ClassRunner.class)` or the shorter `@RunWith(SpringRunner.class)` +variant, developers can implement standard JUnit 4-based unit and integration tests and +simultaneously reap the benefits of the TestContext framework, such as support for +loading application contexts, dependency injection of test instances, transactional test +method execution, and so on. If you want to use the Spring TestContext Framework with an +alternative runner (such as JUnit 4's `Parameterized` runner) or third-party runners +(such as the `MockitoJUnitRunner`), you can, optionally, use +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's support for JUnit rules] +instead. + +The following code listing shows the minimal requirements for configuring a test class to +run with the custom Spring `Runner`: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @RunWith(SpringRunner.class) + @TestExecutionListeners({}) + public class SimpleTest { + + @Test + public void testMethod() { + // test logic... + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @RunWith(SpringRunner::class) + @TestExecutionListeners + class SimpleTest { + + @Test + fun testMethod() { + // test logic... + } + } +---- +====== + +In the preceding example, `@TestExecutionListeners` is configured with an empty list, to +disable the default listeners, which otherwise would require an `ApplicationContext` to +be configured through `@ContextConfiguration`. + +[[testcontext-junit4-rules]] +=== Spring JUnit 4 Rules + +[WARNING] +==== +JUnit 4 is officially in maintenance mode, and JUnit 4 support in Spring is deprecated +since Spring Framework 7.0 in favor of the +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] +and JUnit Jupiter. +==== + +The `org.springframework.test.context.junit4.rules` package provides the following JUnit +4 rules (supported on JUnit 4.12 or higher): + +* `SpringClassRule` +* `SpringMethodRule` + +`SpringClassRule` is a JUnit `TestRule` that supports class-level features of the Spring +TestContext Framework, whereas `SpringMethodRule` is a JUnit `MethodRule` that supports +instance-level and method-level features of the Spring TestContext Framework. + +In contrast to the `SpringRunner`, Spring's rule-based JUnit support has the advantage of +being independent of any `org.junit.runner.Runner` implementation and can, therefore, be +combined with existing alternative runners (such as JUnit 4's `Parameterized`) or +third-party runners (such as the `MockitoJUnitRunner`). + +To support the full functionality of the TestContext framework, you must combine a +`SpringClassRule` with a `SpringMethodRule`. The following example shows the proper way +to declare these rules in an integration test: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // Optionally specify a non-Spring Runner via @RunWith(...) + @ContextConfiguration + public class IntegrationTest { + + @ClassRule + public static final SpringClassRule springClassRule = new SpringClassRule(); + + @Rule + public final SpringMethodRule springMethodRule = new SpringMethodRule(); + + @Test + public void testMethod() { + // test logic... + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // Optionally specify a non-Spring Runner via @RunWith(...) + @ContextConfiguration + class IntegrationTest { + + @Rule + val springMethodRule = SpringMethodRule() + + @Test + fun testMethod() { + // test logic... + } + + companion object { + @ClassRule + val springClassRule = SpringClassRule() + } + } +---- +====== + +[[testcontext-support-classes-junit4]] +=== JUnit 4 Base Classes + +[WARNING] +==== +JUnit 4 is officially in maintenance mode, and JUnit 4 support in Spring is deprecated +since Spring Framework 7.0 in favor of the +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] +and JUnit Jupiter. +==== + +The `org.springframework.test.context.junit4` package provides the following support +classes for JUnit 4-based test cases (supported on JUnit 4.12 or higher): + +* `AbstractJUnit4SpringContextTests` +* `AbstractTransactionalJUnit4SpringContextTests` + +`AbstractJUnit4SpringContextTests` is an abstract base test class that integrates the +Spring TestContext Framework with explicit `ApplicationContext` testing support in a +JUnit 4 environment. When you extend `AbstractJUnit4SpringContextTests`, you can access a +`protected` `applicationContext` instance variable that you can use to perform explicit +bean lookups or to test the state of the context as a whole. + +`AbstractTransactionalJUnit4SpringContextTests` is an abstract transactional extension of +`AbstractJUnit4SpringContextTests` that adds some convenience functionality for JDBC +access. This class expects a `javax.sql.DataSource` bean and a +`PlatformTransactionManager` bean to be defined in the `ApplicationContext`. When you +extend `AbstractTransactionalJUnit4SpringContextTests`, you can access a `protected` +`jdbcTemplate` instance variable that you can use to run SQL statements to query the +database. You can use such queries to confirm database state both before and after +running database-related application code, and Spring ensures that such queries run in +the scope of the same transaction as the application code. When used in conjunction with +an ORM tool, be sure to avoid +xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives]. +As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support], +`AbstractTransactionalJUnit4SpringContextTests` also provides convenience methods that +delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`. +Furthermore, `AbstractTransactionalJUnit4SpringContextTests` provides an +`executeSqlScript(..)` method for running SQL scripts against the configured `DataSource`. + +TIP: These classes are a convenience for extension. If you do not want your test classes +to be tied to a Spring-specific class hierarchy, you can configure your own custom test +classes by using `@RunWith(SpringRunner.class)` or +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit rules]. + + [[testcontext-support-classes-testng]] -== TestNG Support Classes +== TestNG Support The `org.springframework.test.context.testng` package provides the following support classes for TestNG based test cases: @@ -650,7 +690,8 @@ extend `AbstractTransactionalTestNGSpringContextTests`, you can access a `protec database. You can use such queries to confirm database state both before and after running database-related application code, and Spring ensures that such queries run in the scope of the same transaction as the application code. When used in conjunction with -an ORM tool, be sure to avoid xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives]. +an ORM tool, be sure to avoid +xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives]. As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support], `AbstractTransactionalTestNGSpringContextTests` also provides convenience methods that delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`. diff --git a/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc b/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc index 2642be67ed..ae3bd4f610 100644 --- a/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc +++ b/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc @@ -265,6 +265,7 @@ Java:: client = WebTestClient.bindToController(new TestController()) .configureClient() .baseUrl("/test") + .apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build()) .build(); ---- @@ -275,6 +276,7 @@ Kotlin:: client = WebTestClient.bindToController(TestController()) .configureClient() .baseUrl("/test") + .apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build()) .build() ---- ====== @@ -396,7 +398,7 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - import org.springframework.test.web.reactive.server.expectBody + import org.springframework.test.web.reactive.server.expectBody client.get().uri("/persons/1") .exchange() diff --git a/framework-docs/modules/ROOT/pages/web/webflux-versioning.adoc b/framework-docs/modules/ROOT/pages/web/webflux-versioning.adoc new file mode 100644 index 0000000000..c05bac73c0 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webflux-versioning.adoc @@ -0,0 +1,85 @@ +[[webflux-versioning]] += API Versioning +:page-section-summary-toc: 1 + +[.small]#xref:web/webmvc-versioning.adoc[See equivalent in the Servlet stack]# + +Spring WebFlux supports API versioning. This section provides an overview of the support +and underlying strategies. + +Please, see also related content in: + +- Use xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[API Versions] +to map requests to annotated controller methods +- Configure API versioning in xref:web/webflux/config.adoc#webflux-config-api-version[WebFlux Config] + +TIP: API versioning is also supported on the client side in `RestClient`, `WebClient`, and +xref:integration/rest-clients.adoc#rest-http-interface[HTTP Service] clients, as well as +for testing with `WebTestClient`. + + + + +[[webflux-versioning-strategy]] +== ApiVersionStrategy +[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-strategy[See equivalent in the Servlet stack]# + +This strategy holds all application preferences about how to manage versioning. +It delegates to xref:#webflux-versioning-resolver[ApiVersionResolver] to resolve versions +from requests, and to xref:#webflux-versioning-parser[ApiVersionParser] to parse raw version +values into `Comparable`. It also helps to xref:#webflux-versioning-validation[validate] +request versions. + +NOTE: `ApiVersionStrategy` helps to map requests to `@RequestMapping` controller methods, +and is initialized by the WebFlux config. Typically, applications do not interact directly with it. + + + + +[[webflux-versioning-resolver]] +== ApiVersionResolver +[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-resolver[See equivalent in the Servlet stack]# + +This strategy resolves the API version from a request. The WebFlux config provides built-in +options to resolve from a header, a request parameter, or from the URL path. +You can also use a custom `ApiVersionResolver`. + + + + +[[webflux-versioning-parser]] +== ApiVersionParser +[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-parser[See equivalent in the Servlet stack]# + +This strategy helps to parse raw version values into `Comparable`, which helps to +compare, sort, and select versions. By default, the built-in `SemanticApiVersionParser` +parses a version into `major`, `minor`, and `patch` integer values. Minor and patch +values are set to 0 if not present. + + + + +[[webflux-versioning-validation]] +== Validation +[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-validation[See equivalent in the Servlet stack]# + +If a request version is not supported, `InvalidApiVersionException` is raised resulting +in a 400 response. By default, the list of supported versions is initialized from declared +versions in annotated controller mappings. You can add to that list, or set it explicitly +to a fixed set of versions (i.e. ignoring declared ones) through the MVC config. + +By default, a version is required when API versioning is enabled, but you can turn that +off in which case the highest available version is used. You can also specify a default +version. `MissingApiVersionException` is raised resulting in a 400 response when a +version is required but not present. + + + + +[[webflux-versioning-mapping]] +== Request Mapping +[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-mapping[See equivalent in the Servlet stack]# + +`ApiVersionStrategy` supports the mapping of requests to annotated controller methods. +See xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[API Versions] +for more details. \ No newline at end of file diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-body.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-body.adoc index bd52881b45..7ff92ff268 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-body.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-body.adoc @@ -306,34 +306,34 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -Resource resource = ... -Mono result = webClient - .post() - .uri("https://example.com") - .body(Flux.concat( - FormPartEvent.create("field", "field value"), - FilePartEvent.create("file", resource) - ), PartEvent.class) - .retrieve() - .bodyToMono(String.class); + Resource resource = ... + Mono result = webClient + .post() + .uri("https://example.com") + .body(Flux.concat( + FormPartEvent.create("field", "field value"), + FilePartEvent.create("file", resource) + ), PartEvent.class) + .retrieve() + .bodyToMono(String.class); ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -var resource: Resource = ... -var result: Mono = webClient - .post() - .uri("https://example.com") - .body( - Flux.concat( - FormPartEvent.create("field", "field value"), - FilePartEvent.create("file", resource) + var resource: Resource = ... + var result: Mono = webClient + .post() + .uri("https://example.com") + .body( + Flux.concat( + FormPartEvent.create("field", "field value"), + FilePartEvent.create("file", resource) + ) ) - ) - .retrieve() - .bodyToMono() + .retrieve() + .bodyToMono() ---- ====== diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-builder.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-builder.adoc index 53a2fc247c..c6ce0f086c 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-builder.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-builder.adoc @@ -1,7 +1,7 @@ [[webflux-client-builder]] = Configuration -The simplest way to create a `WebClient` is through one of the static factory methods: +The simplest way to create `WebClient` is through one of the static factory methods: * `WebClient.create()` * `WebClient.create(String baseUrl)` @@ -12,10 +12,12 @@ You can also use `WebClient.builder()` with further options: * `defaultUriVariables`: default values to use when expanding URI templates. * `defaultHeader`: Headers for every request. * `defaultCookie`: Cookies for every request. +* `defaultApiVersion`: API version for every request. * `defaultRequest`: `Consumer` to customize every request. * `filter`: Client filter for every request. * `exchangeStrategies`: HTTP message reader/writer customizations. * `clientConnector`: HTTP client library settings. +* `apiVersionInserter`: to insert API version values in the request * `observationRegistry`: the registry to use for enabling xref:integration/observability.adoc#http-client.webclient[Observability support]. * `observationConvention`: xref:integration/observability.adoc#config[an optional, custom convention to extract metadata] for recorded observations. @@ -390,29 +392,29 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - HttpClient httpClient = HttpClient.newBuilder() - .followRedirects(Redirect.NORMAL) - .connectTimeout(Duration.ofSeconds(20)) - .build(); + HttpClient httpClient = HttpClient.newBuilder() + .followRedirects(Redirect.NORMAL) + .connectTimeout(Duration.ofSeconds(20)) + .build(); - ClientHttpConnector connector = - new JdkClientHttpConnector(httpClient, new DefaultDataBufferFactory()); + ClientHttpConnector connector = + new JdkClientHttpConnector(httpClient, new DefaultDataBufferFactory()); - WebClient webClient = WebClient.builder().clientConnector(connector).build(); + WebClient webClient = WebClient.builder().clientConnector(connector).build(); ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val httpClient = HttpClient.newBuilder() - .followRedirects(Redirect.NORMAL) - .connectTimeout(Duration.ofSeconds(20)) - .build() + val httpClient = HttpClient.newBuilder() + .followRedirects(Redirect.NORMAL) + .connectTimeout(Duration.ofSeconds(20)) + .build() - val connector = JdkClientHttpConnector(httpClient, DefaultDataBufferFactory()) + val connector = JdkClientHttpConnector(httpClient, DefaultDataBufferFactory()) - val webClient = WebClient.builder().clientConnector(connector).build() + val webClient = WebClient.builder().clientConnector(connector).build() ---- ====== diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-filter.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-filter.adoc index d63ed06a9f..a2d4ad961f 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-filter.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-filter.adoc @@ -158,65 +158,65 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -public class MultipartExchangeFilterFunction implements ExchangeFilterFunction { - - @Override - public Mono filter(ClientRequest request, ExchangeFunction next) { - if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType()) - && (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) { - return next.exchange(ClientRequest.from(request).body((outputMessage, context) -> - request.body().insert(new BufferingDecorator(outputMessage), context)).build() - ); - } else { - return next.exchange(request); - } - } - - private static final class BufferingDecorator extends ClientHttpRequestDecorator { - - private BufferingDecorator(ClientHttpRequest delegate) { - super(delegate); - } - - @Override - public Mono writeWith(Publisher body) { - return DataBufferUtils.join(body).flatMap(buffer -> { - getHeaders().setContentLength(buffer.readableByteCount()); - return super.writeWith(Mono.just(buffer)); - }); - } - } -} + public class MultipartExchangeFilterFunction implements ExchangeFilterFunction { + + @Override + public Mono filter(ClientRequest request, ExchangeFunction next) { + if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType()) + && (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) { + return next.exchange(ClientRequest.from(request).body((outputMessage, context) -> + request.body().insert(new BufferingDecorator(outputMessage), context)).build() + ); + } else { + return next.exchange(request); + } + } + + private static final class BufferingDecorator extends ClientHttpRequestDecorator { + + private BufferingDecorator(ClientHttpRequest delegate) { + super(delegate); + } + + @Override + public Mono writeWith(Publisher body) { + return DataBufferUtils.join(body).flatMap(buffer -> { + getHeaders().setContentLength(buffer.readableByteCount()); + return super.writeWith(Mono.just(buffer)); + }); + } + } + } ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -class MultipartExchangeFilterFunction : ExchangeFilterFunction { - - override fun filter(request: ClientRequest, next: ExchangeFunction): Mono { - return if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType()) - && (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) { - next.exchange(ClientRequest.from(request) - .body { message, context -> request.body().insert(BufferingDecorator(message), context) } - .build()) - } - else { - next.exchange(request) - } - - } - - private class BufferingDecorator(delegate: ClientHttpRequest) : ClientHttpRequestDecorator(delegate) { - override fun writeWith(body: Publisher): Mono { - return DataBufferUtils.join(body) - .flatMap { - headers.contentLength = it.readableByteCount().toLong() - super.writeWith(Mono.just(it)) - } - } - } -} + class MultipartExchangeFilterFunction : ExchangeFilterFunction { + + override fun filter(request: ClientRequest, next: ExchangeFunction): Mono { + return if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType()) + && (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) { + next.exchange(ClientRequest.from(request) + .body { message, context -> request.body().insert(BufferingDecorator(message), context) } + .build()) + } + else { + next.exchange(request) + } + + } + + private class BufferingDecorator(delegate: ClientHttpRequest) : ClientHttpRequestDecorator(delegate) { + override fun writeWith(body: Publisher): Mono { + return DataBufferUtils.join(body) + .flatMap { + headers.contentLength = it.readableByteCount().toLong() + super.writeWith(Mono.just(it)) + } + } + } + } ---- -====== \ No newline at end of file +====== diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-testing.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-testing.adoc index febbb54982..f7a69c7dbb 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-testing.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-testing.adoc @@ -2,9 +2,16 @@ = Testing :page-section-summary-toc: 1 -To test code that uses the `WebClient`, you can use a mock web server, such as the -https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer]. To see an example -of its use, check out +To test code that uses the `WebClient`, you can use a mock web server, such as +https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer] or +https://wiremock.org/[WireMock]. Mock web servers accept requests over HTTP like a regular +server, and that means you can test with the same HTTP client that is also configured in +the same way as in production, which is important because there are often subtle +differences in the way different clients handle network I/O. Another advantage of mock +web servers is the ability to simulate specific network issues and conditions at the +transport level, in combination with the client used in production. + +For example use of MockWebServer, see {spring-framework-code}/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java[`WebClientIntegrationTests`] in the Spring Framework test suite or the https://github.com/square/okhttp/tree/master/samples/static-server[`static-server`] diff --git a/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc b/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc index 1e7f397c19..04ea4fa386 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc @@ -207,14 +207,14 @@ Kotlin:: class ExampleHandler : WebSocketHandler { override fun handle(session: WebSocketSession): Mono { - return session.receive() // <1> + return session.receive() // <1> .doOnNext { // ... // <2> } .concatMap { // ... // <3> } - .then() // <4> + .then() // <4> } } ---- @@ -268,16 +268,16 @@ Kotlin:: override fun handle(session: WebSocketSession): Mono { - val output = session.receive() // <1> + val output = session.receive() // <1> .doOnNext { // ... } .concatMap { // ... } - .map { session.textMessage("Echo $it") } // <2> + .map { session.textMessage("Echo $it") } // <2> - return session.send(output) // <3> + return session.send(output) // <3> } } ---- diff --git a/framework-docs/modules/ROOT/pages/web/webflux/config.adoc b/framework-docs/modules/ROOT/pages/web/webflux/config.adoc index c67851c129..f2a75ba801 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/config.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/config.adoc @@ -149,7 +149,7 @@ Java:: DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); registrar.setUseIsoFormat(true); registrar.registerFormatters(registry); - } + } } ---- @@ -686,6 +686,63 @@ reliance on it. +[[webflux-config-api-version]] +== API Version +[.small]#xref:web/webmvc/mvc-config/api-version.adoc[See equivalent in the Servlet stack]# + +To enable API versioning with a request header, use the following: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim"] +---- + @Configuration + public class WebConfiguration implements WebFluxConfigurer { + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer.useRequestHeader("X-API-Version"); + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim"] +---- + @Configuration + class WebConfiguration : WebMvcConfigurer { + + override fun configureApiVersioning(configurer: ApiVersionConfigurer) { + configurer.useRequestHeader("X-API-Version") + } + } +---- +====== + +Alternatively, the version can be resolved from a request parameter, from a path segment, +or through a custom `ApiVersionResolver`. + +TIP: When resolving from a path segment, consider configuring a path prefix once in +xref:web/webmvc/mvc-config/path-matching.adoc[Path Matching] options. + +Raw version values are parsed with `SemanticVersionParser` by default, but you can use +a custom xref:web/webflux-versioning.adoc#webflux-versioning-parser[ApiVersionParser]. + +"Supported" versions are transparently detected from versions declared in request mappings +for convenience, but you can also set the list of supported versions explicitly, and +ignore declared ones. Requests with a version that is not supported are rejected with an +`InvalidApiVersionException` resulting in a 400 response. + +Once API versioning is configured, you can begin to map requests to +xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[controller methods] +according to the request version. + + + + [[webflux-config-blocking-execution]] == Blocking Execution diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc index 24999a0e17..632377c7b9 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc @@ -62,7 +62,7 @@ Java:: ---- class Account { - private final String firstName; + private final String firstName; public Account(@BindParam("first-name") String firstName) { this.firstName = firstName; diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/multipart-forms.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/multipart-forms.adoc index 5616e8110b..ab6c1f647e 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/multipart-forms.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/multipart-forms.adoc @@ -19,7 +19,7 @@ Java:: private String name; - private MultipartFile file; + private FilePart file; // ... @@ -42,7 +42,7 @@ Kotlin:: ---- class MyForm( val name: String, - val file: MultipartFile) + val file: FilePart) @Controller class FileUploadController { diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/return-types.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/return-types.adoc index 22fe3570b6..450b9e9925 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/return-types.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/return-types.adoc @@ -34,11 +34,7 @@ Controllers can then return a `Flux>`; Reactor provides a dedicated oper | `HttpHeaders` | For returning a response with headers and no body. -| `ErrorResponse` -| To render an RFC 9457 error response with details in the body, - see xref:web/webflux/ann-rest-exceptions.adoc[Error Responses] - -| `ProblemDetail` +| `ErrorResponse`, `ProblemDetail` | To render an RFC 9457 error response with details in the body, see xref:web/webflux/ann-rest-exceptions.adoc[Error Responses] diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-modelattrib-methods.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-modelattrib-methods.adoc index 7d79ea1d46..f4a6110105 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-modelattrib-methods.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-modelattrib-methods.adoc @@ -93,8 +93,8 @@ Java:: ---- @ModelAttribute public void addAccount(@RequestParam String number) { - Mono accountMono = accountRepository.findAccount(number); - model.addAttribute("account", accountMono); + Mono accountMono = accountRepository.findAccount(number); + model.addAttribute("account", accountMono); } @PostMapping("/accounts") diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc index cbfcad7a7c..295a16a264 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc @@ -234,8 +234,8 @@ Kotlin:: -- URI path patterns can also have embedded `${...}` placeholders that are resolved on startup -through `PropertySourcesPlaceholderConfigurer` against local, system, environment, and -other property sources. You can use this to, for example, parameterize a base URL based on +by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and +other property sources. You can use this, for example, to parameterize a base URL based on some external configuration. NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support. @@ -408,6 +408,85 @@ Kotlin:: ====== +[[webflux-ann-requestmapping-version]] +== API Version +[.small]#xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[See equivalent in the Servlet stack]# + +There is no standard way to specify an API version, so you need to configure that first +through the xref:web/webflux/config.adoc#webflux-config-api-version[WebFlux Config] along with other +config options. This results in the creation of an +xref:web/webflux-versioning.adoc#webflux-versioning-strategy[ApiVersionStrategy] that in +supports request mapping. + +Once API versioning is enabled, you can begin to map requests with versions. +The `@RequestMapping` version attribute supports the following: + +- No value -- match any version +- Fixed version ("1.2") -- match the given version only +- Baseline version ("1.2+") -- match the given version and above + +If multiple controller methods have a version less than or equal to the request version, +the one closest to the request version is considered for mapping purposes, +in effect superseding the rest. + +To illustrate this, consider the following controller mappings: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @RestController + @RequestMapping("/account/{id}") + public class AccountController { + + @GetMapping // <1> + public Account getAccount() { + } + + @GetMapping(version = "1.1") // <2> + public Account getAccount1_1() { + } + + @GetMapping(version = "1.2+") // <3> + public Account getAccount1_2() { + } + + @GetMapping(version = "1.5") // <4> + public Account getAccount1_5() { + } + } +---- +<1> match any version +<2> match version 1.1 +<3> match version 1.2 and above +<4> match version 1.5 +====== + +For request with version `"1.3"`: + +- (1) matches as it matches any version +- (2) does not match +- (3) matches as it matches 1.2 and above, and is *chosen* as the highest match +- (4) is higher and does not match + +For request with version `"1.5"`: + +- (1) matches as it matches any version +- (2) does not match +- (3) matches as it matches 1.2 and above +- (4) matches and is *chosen* as the highest match + +A request with version `"1.6"` does not have a match. (1) and (3) do match, but are +superseded by (4), which does not match. In this scenario, `NotAcceptableApiVersionException` +is raised resulting in a 400 response. + +NOTE: The above assumes the request version is a "supported" versions. If not it would +fail xref:web/webflux-versioning.adoc#webflux-versioning-validation[Validation]. + + + [[webflux-ann-requestmapping-head-options]] == HTTP HEAD, OPTIONS diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-validation.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-validation.adoc index e22e07b94b..553b9d9dac 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-validation.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-validation.adoc @@ -104,21 +104,21 @@ Kotlin:: override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) { // ... - } + } override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) { // ... - } + } override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) { // ... - } + } // ... override fun other(result: ParameterValidationResult) { // ... - } + } }) ---- ====== diff --git a/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc b/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc index 6e980e5197..13e4c4b0d4 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc @@ -81,7 +81,7 @@ The following table describes server dependencies (also see |jetty-server, jetty-servlet |=== -The code snippets below show using the `HttpHandler` adapters with each server API: +The code snippets below show using the `HttpHandler` adapters with each server API. *Reactor Netty* [tabs] @@ -176,17 +176,16 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- HttpHandler handler = ... - Servlet servlet = new JettyHttpHandlerAdapter(handler); + JettyCoreHttpHandlerAdapter adapter = new JettyCoreHttpHandlerAdapter(handler); Server server = new Server(); - ServletContextHandler contextHandler = new ServletContextHandler(server, ""); - contextHandler.addServlet(new ServletHolder(servlet), "/"); - contextHandler.start(); + server.setHandler(adapter); ServerConnector connector = new ServerConnector(server); connector.setHost(host); connector.setPort(port); server.addConnector(connector); + server.start(); ---- @@ -195,27 +194,27 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- val handler: HttpHandler = ... - val servlet = JettyHttpHandlerAdapter(handler) + val adapter = JettyCoreHttpHandlerAdapter(handler) val server = Server() - val contextHandler = ServletContextHandler(server, "") - contextHandler.addServlet(ServletHolder(servlet), "/") - contextHandler.start(); + server.setHandler(adapter) val connector = ServerConnector(server) connector.host = host connector.port = port server.addConnector(connector) + server.start() ---- ====== -*Servlet Container* +TIP: In Spring Framework 6.2, `JettyHttpHandlerAdapter` was deprecated in favor of +`JettyCoreHttpHandlerAdapter`, which integrates directly with Jetty 12 APIs +without a Servlet layer. -To deploy as a WAR to any Servlet container, you can extend and include -{spring-framework-api}/web/server/adapter/AbstractReactiveWebInitializer.html[`AbstractReactiveWebInitializer`] -in the WAR. That class wraps an `HttpHandler` with `ServletHttpHandlerAdapter` and registers -that as a `Servlet`. +To deploy as a WAR to a Servlet container instead, use +{spring-framework-api}/web/server/adapter/AbstractReactiveWebInitializer.html[`AbstractReactiveWebInitializer`], +to adapt `HttpHandler` to a `Servlet` via `ServletHttpHandlerAdapter`. @@ -782,8 +781,8 @@ Java:: ---- WebClient webClient = WebClient.builder() .codecs(configurer -> { - CustomDecoder decoder = new CustomDecoder(); - configurer.customCodecs().registerWithDefaultConfig(decoder); + CustomDecoder decoder = new CustomDecoder(); + configurer.customCodecs().registerWithDefaultConfig(decoder); }) .build(); ---- @@ -794,10 +793,9 @@ Kotlin:: ---- val webClient = WebClient.builder() .codecs({ configurer -> - val decoder = CustomDecoder() - configurer.customCodecs().registerWithDefaultConfig(decoder) + val decoder = CustomDecoder() + configurer.customCodecs().registerWithDefaultConfig(decoder) }) .build() ---- ====== - diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc index d68a19b2a4..ca233a4076 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc @@ -276,7 +276,7 @@ ServerResponse.async(asyncResponse); ---- ====== -https://www.w3.org/TR/eventsource/[Server-Sent Events] can be provided via the +https://html.spec.whatwg.org/multipage/server-sent-events.html[Server-Sent Events] can be provided via the static `sse` method on `ServerResponse`. The builder provided by that method allows you to send Strings, or other objects as JSON. For example: @@ -781,7 +781,7 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - ClassPathResource index = new ClassPathResource("static/index.html"); + ClassPathResource index = new ClassPathResource("static/index.html"); List extensions = List.of("js", "css", "ico", "png", "jpg", "gif"); RequestPredicate spaPredicate = path("/api/**").or(path("/error")).negate(); RouterFunction redirectToIndex = route() @@ -793,7 +793,7 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val redirectToIndex = router { + val redirectToIndex = router { val index = ClassPathResource("static/index.html") val spaPredicate = !(path("/api/**") or path("/error")) resource(spaPredicate, index) @@ -812,16 +812,16 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - Resource location = new FileUrlResource("public-resources/"); - RouterFunction resources = RouterFunctions.resources("/resources/**", location); + Resource location = new FileUrlResource("public-resources/"); + RouterFunction resources = RouterFunctions.resources("/resources/**", location); ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val location = FileUrlResource("public-resources/") - val resources = router { resources("/resources/**", location) } + val location = FileUrlResource("public-resources/") + val resources = router { resources("/resources/**", location) } ---- ====== @@ -846,7 +846,7 @@ processing lifecycle and also (potentially) run side by side with annotated cont any are declared. It is also how functional endpoints are enabled by the Spring Boot Web starter. -The following example shows a WebFlux Java configuration: +The following example shows a WebMvc Java configuration: [tabs] ====== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-versioning.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-versioning.adoc new file mode 100644 index 0000000000..7ea6fad3ec --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webmvc-versioning.adoc @@ -0,0 +1,85 @@ +[[mvc-versioning]] += API Versioning +:page-section-summary-toc: 1 + +[.small]#xref:web/webflux-versioning.adoc[See equivalent in the Reactive stack]# + +Spring MVC supports API versioning. This section provides an overview of the support +and underlying strategies. + +Please, see also related content in: + +- Use xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[API Version] +to map requests to annotated controller methods +- Configure API versioning in xref:web/webmvc/mvc-config/api-version.adoc[MVC Config] + +TIP: API versioning is also supported on the client side in `RestClient`, `WebClient`, and +xref:integration/rest-clients.adoc#rest-http-interface[HTTP Service] clients, as well as +for testing with `WebTestClient`. + + + + +[[mvc-versioning-strategy]] +== ApiVersionStrategy +[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-strategy[See equivalent in the Reactive stack]# + +This strategy holds all application preferences about how to manage versioning. +It delegates to xref:#mvc-versioning-resolver[ApiVersionResolver] to resolve versions +from requests, and to xref:#mvc-versioning-parser[ApiVersionParser] to parse raw version +values into `Comparable`. It also helps to xref:#mvc-versioning-validation[validate] +request versions. + +NOTE: `ApiVersionStrategy` helps to map requests to `@RequestMapping` controller methods, +and is initialized by the MVC config. Typically, applications do not interact directly with it. + + + + +[[mvc-versioning-resolver]] +== ApiVersionResolver +[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-resolver[See equivalent in the Reactive stack]# + +This strategy resolves the API version from a request. The MVC config provides built-in +options to resolve from a header, from a request parameter, or from the URL path. +You can also use a custom `ApiVersionResolver`. + + + + +[[mvc-versioning-parser]] +== ApiVersionParser +[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-parser[See equivalent in the Reactive stack]# + +This strategy helps to parse raw version values into `Comparable`, which helps to +compare, sort, and select versions. By default, the built-in `SemanticApiVersionParser` +parses a version into `major`, `minor`, and `patch` integer values. Minor and patch +values are set to 0 if not present. + + + + +[[mvc-versioning-validation]] +== Validation +[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-validation[See equivalent in the Reactive stack]# + +If a request version is not supported, `InvalidApiVersionException` is raised resulting +in a 400 response. By default, the list of supported versions is initialized from declared +versions in annotated controller mappings. You can add to that list, or set it explicitly +to a fixed set of versions (i.e. ignoring declared ones) through the MVC config. + +By default, a version is required when API versioning is enabled, but you can turn that +off in which case the highest available version is used. You can also specify a default +version. `MissingApiVersionException` is raised resulting in a 400 response when a +version is required but not present. + + + + +[[mvc-versioning-mapping]] +== Request Mapping +[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-mapping[See equivalent in the Reactive stack]# + +`ApiVersionStrategy` supports the mapping of requests to annotated controller methods. +See xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[API Version] +for more details. \ No newline at end of file diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-async.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-async.adoc index 9e248d5f24..3f71ba4852 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-async.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-async.adoc @@ -4,11 +4,13 @@ Spring MVC has an extensive integration with Servlet asynchronous request xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-processing[processing]: -* xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-deferredresult[`DeferredResult`] and xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-callable[`Callable`] -return values in controller methods provide basic support for a single asynchronous -return value. +* xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-deferredresult[`DeferredResult`], +xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-callable[`Callable`], and +xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-webasynctask[`WebAsyncTask`] return values +in controller methods provide support for a single asynchronous return value. * Controllers can xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-http-streaming[stream] multiple values, including -xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-sse[SSE] and xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-output-stream[raw data]. +xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-sse[SSE] and +xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-output-stream[raw data]. * Controllers can use reactive clients and return xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[reactive types] for response handling. @@ -96,6 +98,47 @@ xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[config + +[[mvc-ann-async-webasynctask]] +== `WebAsyncTask` + +`WebAsyncTask` is comparable to using xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-callable[Callable] +but allows customizing additional settings such a request timeout value, and the +`AsyncTaskExecutor` to execute the `java.util.concurrent.Callable` with instead +of the defaults set up globally for Spring MVC. Below is an example of using `WebAsyncTask`: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @GetMapping("/callable") + WebAsyncTask handle() { + return new WebAsyncTask(20000L,()->{ + Thread.sleep(10000); //simulate long-running task + return "asynchronous request completed"; + }); + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- +@GetMapping("/callable") +fun handle(): WebAsyncTask { + return WebAsyncTask(20000L) { + Thread.sleep(10000) // simulate long-running task + "asynchronous request completed" + } +} +---- +====== + + + + [[mvc-ann-async-processing]] == Processing @@ -281,7 +324,7 @@ invokes the configured exception resolvers and completes the request. === SSE `SseEmitter` (a subclass of `ResponseBodyEmitter`) provides support for -https://www.w3.org/TR/eventsource/[Server-Sent Events], where events sent from the server +https://html.spec.whatwg.org/multipage/server-sent-events.html[Server-Sent Events], where events sent from the server are formatted according to the W3C SSE specification. To produce an SSE stream from a controller, return `SseEmitter`, as the following example shows: @@ -390,7 +433,7 @@ reactive types from the controller method. Reactive return values are handled as follows: * A single-value promise is adapted to, similar to using `DeferredResult`. Examples -include `Mono` (Reactor) or `Single` (RxJava). +include `CompletionStage` (JDK), Mono` (Reactor), and `Single` (RxJava). * A multi-value stream with a streaming media type (such as `application/x-ndjson` or `text/event-stream`) is adapted to, similar to using `ResponseBodyEmitter` or `SseEmitter`. Examples include `Flux` (Reactor) or `Observable` (RxJava). diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/api-version.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/api-version.adoc new file mode 100644 index 0000000000..88ffa09a6a --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/api-version.adoc @@ -0,0 +1,26 @@ +[[mvc-config-api-version]] += API Version + +[.small]#xref:web/webflux/config.adoc#webflux-config-api-version[See equivalent in the Reactive stack]# + +To enable API versioning with a request header, use the following: + +include-code::./WebConfiguration[tag=snippet,indent=0] + +Alternatively, the version can be resolved from a request parameter, from a path segment, +or through a custom `ApiVersionResolver`. + +TIP: When resolving from a path segment, consider configuring a path prefix once in +xref:web/webmvc/mvc-config/path-matching.adoc[Path Matching] options. + +Raw version values are parsed with `SemanticVersionParser` by default, but you can use +a custom xref:web/webmvc-versioning.adoc#mvc-versioning-parser[ApiVersionParser]. + +"Supported" versions are transparently detected from versions declared in request mappings +for convenience, but you can also set the list of supported versions explicitly, and +ignore declared ones. Requests with a version that is not supported are rejected with an +`InvalidApiVersionException` resulting in a 400 response. + +Once API versioning is configured, you can begin to map requests to +xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[controller methods] +according to the request version. \ No newline at end of file diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-advice.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-advice.adoc index 403db0bbf2..f4af7ac1b4 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-advice.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-advice.adoc @@ -10,18 +10,20 @@ to any controller. Moreover, as of 5.3, `@ExceptionHandler` methods in `@Control can be used to handle exceptions from any `@Controller` or any other handler. `@ControllerAdvice` is meta-annotated with `@Component` and therefore can be registered as -a Spring bean through xref:core/beans/java/instantiating-container.adoc#beans-java-instantiating-container-scan[component scanning] -. `@RestControllerAdvice` is meta-annotated with `@ControllerAdvice` -and `@ResponseBody`, and that means `@ExceptionHandler` methods will have their return -value rendered via response body message conversion, rather than via HTML views. +a Spring bean through xref:core/beans/java/instantiating-container.adoc#beans-java-instantiating-container-scan[component scanning]. + +`@RestControllerAdvice` is a shortcut annotation that combines `@ControllerAdvice` +with `@ResponseBody`, in effect simply an `@ControllerAdvice` whose exception handler +methods render to the response body. On startup, `RequestMappingHandlerMapping` and `ExceptionHandlerExceptionResolver` detect controller advice beans and apply them at runtime. Global `@ExceptionHandler` methods, from an `@ControllerAdvice`, are applied _after_ local ones, from the `@Controller`. By contrast, global `@ModelAttribute` and `@InitBinder` methods are applied _before_ local ones. -The `@ControllerAdvice` annotation has attributes that let you narrow the set of controllers -and handlers that they apply to. For example: +By default, both `@ControllerAdvice` and `@RestControllerAdvice` apply to any controller, +including `@Controller` and `@RestController`. Use attributes of the annotation to narrow +the set of controllers and handlers that they apply to. For example: [tabs] ====== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-exceptionhandler.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-exceptionhandler.adoc index e13037ded8..3a0f94e572 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-exceptionhandler.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-exceptionhandler.adoc @@ -177,13 +177,9 @@ the content negotiation during the error handling phase will decide which conten be converted through `HttpMessageConverter` instances and written to the response. See xref:web/webmvc/mvc-controller/ann-methods/responseentity.adoc[ResponseEntity]. -| `ErrorResponse` +| `ErrorResponse`, `ProblemDetail` | To render an RFC 9457 error response with details in the body, -see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] - -| `ProblemDetail` -| To render an RFC 9457 error response with details in the body, -see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] + see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] | `String` | A view name to be resolved with `ViewResolver` implementations and used together with the diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc index 7a04b5ba7f..d9808acc05 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc @@ -97,7 +97,7 @@ Java:: ---- class Account { - private final String firstName; + private final String firstName; public Account(@BindParam("first-name") String firstName) { this.firstName = firstName; @@ -243,7 +243,7 @@ Kotlin:: ====== If there is no `BindingResult` parameter after the `@ModelAttribute`, then -`MethodArgumentNotValueException` is raised with the validation errors. However, if method +a `MethodArgumentNotValidException` is raised with the validation errors. However, if method validation applies because other parameters have `@jakarta.validation.Constraint` annotations, then `HandlerMethodValidationException` is raised instead. For more details, see the section xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation]. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/return-types.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/return-types.adoc index 557de2db3c..bd6aa862ec 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/return-types.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/return-types.adoc @@ -22,11 +22,7 @@ supported for all return values. | `HttpHeaders` | For returning a response with headers and no body. -| `ErrorResponse` -| To render an RFC 9457 error response with details in the body, - see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] - -| `ProblemDetail` +| `ErrorResponse`, `ProblemDetail` | To render an RFC 9457 error response with details in the body, see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc index 5ed0bfa381..cfd4cbb281 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc @@ -429,6 +429,86 @@ xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-co instead. +[[mvc-ann-requestmapping-version]] +== API Version +[.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[See equivalent in the Reactive stack]# + +There is no standard way to specify an API version, so you need to configure that first +through the xref:web/webmvc/mvc-config/api-version.adoc[MVC Config] along with other +config options. This results in the creation of an +xref:web/webmvc-versioning.adoc#mvc-versioning-strategy[ApiVersionStrategy] that in turn +supports request mapping. + +Once API versioning is enabled, you can begin to map requests with versions. +The `@RequestMapping` version attribute supports the following: + +- No value -- match any version +- Fixed version ("1.2") -- match the given version only +- Baseline version ("1.2+") -- match the given version and above + +If multiple controller methods have a version less than or equal to the request version, +the one closest to the request version is considered for mapping purposes, +in effect superseding the rest. + +To illustrate this, consider the following controller mappings: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @RestController + @RequestMapping("/account/{id}") + public class AccountController { + + @GetMapping // <1> + public Account getAccount() { + } + + @GetMapping(version = "1.1") // <2> + public Account getAccount1_1() { + } + + @GetMapping(version = "1.2+") // <3> + public Account getAccount1_2() { + } + + @GetMapping(version = "1.5") // <4> + public Account getAccount1_5() { + } + } +---- +<1> match any version +<2> match version 1.1 +<3> match version 1.2 and above +<4> match version 1.5 +====== + +For request with version `"1.3"`: + +- (1) matches as it matches any version +- (2) does not match +- (3) matches as it matches 1.2 and above, and is *chosen* as the highest match +- (4) is higher and does not match + +For request with version `"1.5"`: + +- (1) matches as it matches any version +- (2) does not match +- (3) matches as it matches 1.2 and above +- (4) matches and is *chosen* as the highest match + +A request with version `"1.6"` does not have a match. (1) and (3) do match, but are +superseded by (4), which does not match. In this scenario, `NotAcceptableApiVersionException` +is raised resulting in a 400 response. + +NOTE: The above assumes the request version is a "supported" versions. If not it would +fail xref:web/webmvc-versioning.adoc#mvc-versioning-validation[Validation]. + + + + [[mvc-ann-requestmapping-head-options]] == HTTP HEAD, OPTIONS [.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-head-options[See equivalent in the Reactive stack]# diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-validation.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-validation.adoc index 34cf05e99d..99ddf8635e 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-validation.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-validation.adoc @@ -73,12 +73,12 @@ Java:: @Override public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) { - // ... + // ... } @Override public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) { - // ... + // ... } @Override @@ -88,7 +88,7 @@ Java:: @Override public void other(ParameterValidationResult result) { - // ... + // ... } }); ---- @@ -103,22 +103,22 @@ Kotlin:: ex.visitResults(object : HandlerMethodValidationException.Visitor { override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) { - // ... - } + // ... + } override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) { - // ... - } + // ... + } override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) { - // ... - } + // ... + } // ... override fun other(result: ParameterValidationResult) { - // ... - } + // ... + } }) ---- ====== diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc index 4301ba9708..831b1ff8df 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc @@ -22,11 +22,11 @@ The following example code is based on it: [source,javascript,indent=0,subs="verbatim,quotes"] ---- const stompClient = new StompJs.Client({ - brokerURL: 'ws://domain.com/portfolio', - onConnect: () => { - // ... - } - }); + brokerURL: 'ws://domain.com/portfolio', + onConnect: () => { + // ... + } + }); ---- Alternatively, if you connect through SockJS, you can enable the @@ -47,5 +47,3 @@ interactive web application] -- a getting started guide. * https://github.com/rstoyanchev/spring-websocket-portfolio[Stock Portfolio] -- a sample application. - - diff --git a/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc b/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc index a56c0d1893..fa1de23973 100644 --- a/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc +++ b/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc @@ -38,7 +38,7 @@ to inform the server that the original port was `443`. ==== X-Forwarded-Proto While not standard, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto[`X-Forwarded-Proto: (https|http)`] -is a de-facto standard header that is used to communicate the original protocol (for example, https / https) +is a de-facto standard header that is used to communicate the original protocol (for example, https / http) to a downstream server. For example, if a request of `https://example.com/resource` is sent to a proxy which forwards the request to `http://localhost:8080/resource`, then a header of `X-Forwarded-Proto: https` can be sent to inform the server that the original protocol was `https`. @@ -119,4 +119,4 @@ https://example.com/api/app1/{path} -> http://localhost:8080/app1/{path} In this case, the proxy has a prefix of `/api/app1` and the server has a prefix of `/app1`. The proxy can send `X-Forwarded-Prefix: /api/app1` to have the original prefix -`/api/app1` override the server prefix `/app1`. \ No newline at end of file +`/api/app1` override the server prefix `/app1`. diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/converter/AccountControllerIntegrationTests.java b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/converter/AccountControllerIntegrationTests.java index 63326e903a..7555aebc8e 100644 --- a/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/converter/AccountControllerIntegrationTests.java +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/converter/AccountControllerIntegrationTests.java @@ -25,6 +25,7 @@ import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; import org.springframework.test.web.servlet.assertj.MockMvcTester; import org.springframework.web.context.WebApplicationContext; +@SuppressWarnings("removal") // tag::snippet[] @SpringJUnitWebConfig(ApplicationWebConfiguration.class) class AccountControllerIntegrationTests { diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.java new file mode 100644 index 0000000000..a69e95e856 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.java @@ -0,0 +1,32 @@ +/* + * 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.docs.web.webmvc.mvcconfig.mvcconfigapiversion; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer.useRequestHeader("X-API-Version"); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java index 019a99a270..3517b3275e 100644 --- a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java @@ -28,6 +28,7 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +@SuppressWarnings("removal") // tag::snippet[] @Configuration public class WebConfiguration implements WebMvcConfigurer { diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.java index 0d949748a3..1c1122713e 100644 --- a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.java @@ -23,6 +23,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; import org.springframework.web.servlet.view.json.MappingJackson2JsonView; +@SuppressWarnings("removal") // tag::snippet[] @Configuration public class FreeMarkerConfiguration implements WebMvcConfigurer { diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.java index c4d27f555e..57592d5614 100644 --- a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.java @@ -21,6 +21,7 @@ import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.view.json.MappingJackson2JsonView; +@SuppressWarnings("removal") // tag::snippet[] @Configuration public class WebConfiguration implements WebMvcConfigurer { diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/converter/AccountControllerIntegrationTests.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/converter/AccountControllerIntegrationTests.kt index 523d728941..196d6a63db 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/converter/AccountControllerIntegrationTests.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/converter/AccountControllerIntegrationTests.kt @@ -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. @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION") + package org.springframework.docs.testing.mockmvc.assertj.mockmvctestersetup.converter import org.springframework.beans.factory.annotation.Autowired diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.kt new file mode 100644 index 0000000000..474ff85a6d --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.kt @@ -0,0 +1,31 @@ +/* + * 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.docs.web.webmvc.mvcconfig.mvcconfigapiversion + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun configureApiVersioning(configurer: ApiVersionConfigurer) { + configurer.useRequestHeader("X-API-Version") + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt index 12c197a46f..c1993d1208 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigmessageconverters import com.fasterxml.jackson.module.paramnames.ParameterNamesModule diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.kt index 55acaa63cb..5e300f7222 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers import org.springframework.context.annotation.Bean diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.kt index 472ecf25bf..1b60cb562b 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION") + package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers import org.springframework.context.annotation.Configuration diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 8a083146d9..c662dedf78 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -7,20 +7,21 @@ javaPlatform { } dependencies { - api(platform("com.fasterxml.jackson:jackson-bom:2.18.3")) - api(platform("io.micrometer:micrometer-bom:1.14.5")) - api(platform("io.netty:netty-bom:4.1.119.Final")) - api(platform("io.projectreactor:reactor-bom:2025.0.0-M1")) + api(platform("com.fasterxml.jackson:jackson-bom:2.18.4")) + api(platform("io.micrometer:micrometer-bom:1.15.1")) + api(platform("io.netty:netty-bom:4.2.2.Final")) + api(platform("io.projectreactor:reactor-bom:2025.0.0-M4")) api(platform("io.rsocket:rsocket-bom:1.1.5")) - api(platform("org.apache.groovy:groovy-bom:4.0.26")) + api(platform("org.apache.groovy:groovy-bom:4.0.27")) api(platform("org.apache.logging.log4j:log4j-bom:3.0.0-beta3")) api(platform("org.assertj:assertj-bom:3.27.3")) - api(platform("org.eclipse.jetty:jetty-bom:12.1.0.alpha1")) - api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.0.alpha1")) - api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.1")) - api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.8.0")) - api(platform("org.junit:junit-bom:5.12.1")) - api(platform("org.mockito:mockito-bom:5.16.1")) + api(platform("org.eclipse.jetty:jetty-bom:12.1.0.beta0")) + api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.0.beta0")) + api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2")) + api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.8.1")) + api(platform("org.junit:junit-bom:5.13.1")) + api(platform("org.mockito:mockito-bom:5.18.0")) + api(platform("tools.jackson:jackson-bom:3.0.0-rc5")) constraints { api("com.fasterxml:aalto-xml:1.3.2") @@ -29,8 +30,8 @@ dependencies { api("com.github.librepdf:openpdf:1.3.43") api("com.google.code.findbugs:findbugs:3.0.1") api("com.google.code.findbugs:jsr305:3.0.2") - api("com.google.code.gson:gson:2.12.1") - api("com.google.protobuf:protobuf-java-util:4.30.0") + api("com.google.code.gson:gson:2.13.0") + api("com.google.protobuf:protobuf-java-util:4.30.2") api("com.h2database:h2:2.3.232") api("com.jayway.jsonpath:json-path:2.9.0") api("com.networknt:json-schema-validator:1.5.3") @@ -44,9 +45,8 @@ dependencies { api("com.thoughtworks.qdox:qdox:2.2.0") api("com.thoughtworks.xstream:xstream:1.4.21") api("commons-io:commons-io:2.15.0") - api("commons-logging:commons-logging:1.3.4") + api("commons-logging:commons-logging:1.3.5") api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2") - api("io.micrometer:context-propagation:1.1.1") api("io.mockk:mockk:1.13.4") api("io.projectreactor.tools:blockhound:1.0.8.RELEASE") api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE") @@ -97,16 +97,16 @@ dependencies { api("org.apache.derby:derby:10.16.1.1") api("org.apache.derby:derbyclient:10.16.1.1") api("org.apache.derby:derbytools:10.16.1.1") - api("org.apache.httpcomponents.client5:httpclient5:5.4.2") - api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.3") + api("org.apache.httpcomponents.client5:httpclient5:5.5") + 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:11.0.1") - api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.1") - api("org.apache.tomcat:tomcat-util:11.0.1") - api("org.apache.tomcat:tomcat-websocket:11.0.1") - api("org.aspectj:aspectjrt:1.9.23") - api("org.aspectj:aspectjtools:1.9.23") - api("org.aspectj:aspectjweaver:1.9.23") + api("org.apache.tomcat.embed:tomcat-embed-core:11.0.7") + api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.7") + api("org.apache.tomcat:tomcat-util:11.0.7") + api("org.apache.tomcat:tomcat-websocket:11.0.7") + api("org.aspectj:aspectjrt:1.9.24") + api("org.aspectj:aspectjtools:1.9.24") + api("org.aspectj:aspectjweaver:1.9.24") api("org.awaitility:awaitility:4.3.0") api("org.bouncycastle:bcpkix-jdk18on:1.72") api("org.codehaus.jettison:jettison:1.5.4") @@ -124,11 +124,12 @@ dependencies { api("org.glassfish:jakarta.el:4.0.2") api("org.graalvm.sdk:graal-sdk:22.3.1") api("org.hamcrest:hamcrest:3.0") - api("org.hibernate.orm:hibernate-core:7.0.0.Beta4") - api("org.hibernate.validator:hibernate-validator:9.0.0.CR1") + api("org.hibernate.orm:hibernate-core:7.0.0.Final") + api("org.hibernate.validator:hibernate-validator:9.0.0.Final") api("org.hsqldb:hsqldb:2.7.4") api("org.htmlunit:htmlunit:4.10.0") api("org.javamoney:moneta:1.4.4") + api("org.jboss.logging:jboss-logging:3.6.1.Final") api("org.jruby:jruby:9.4.12.0") api("org.jspecify:jspecify:1.0.0") api("org.junit.support:testng-engine:1.0.5") diff --git a/gradle.properties b/gradle.properties index 54f2e67903..a50cbe9423 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m org.gradle.parallel=true -kotlinVersion=2.1.20 +kotlinVersion=2.2.0-RC2 kotlin.jvm.target.validation.mode=ignore kotlin.stdlib.default.dependency=false diff --git a/gradle/ide.gradle b/gradle/ide.gradle index 36a5c02951..56865ee11b 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -73,7 +73,7 @@ eclipse.classpath.file.whenMerged { // within Eclipse. Consequently, Java 21 features managed via the // me.champeau.mrjar plugin cannot be built or tested within Eclipse. eclipse.classpath.file.whenMerged { classpath -> - classpath.entries.removeAll { it.path =~ /src\/(main|test)\/java21/ } + classpath.entries.removeAll { it.path =~ /src\/(main|test)\/java(21|24)/ } } // Remove classpath entries for non-existent libraries added by the me.champeau.mrjar diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index 5dfa395fe2..076a67b192 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -6,15 +6,13 @@ apply plugin: 'org.springframework.build.optional-dependencies' // apply plugin: 'io.github.goooler.shadow' apply plugin: 'me.champeau.jmh' apply from: "$rootDir/gradle/publications.gradle" -apply plugin: 'net.ltgt.errorprone' +apply plugin: "io.spring.nullability" dependencies { jmh 'org.openjdk.jmh:jmh-core:1.37' jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37' jmh 'org.openjdk.jmh:jmh-generator-bytecode:1.37' jmh 'net.sf.jopt-simple:jopt-simple' - errorprone 'com.uber.nullaway:nullaway:0.12.4' - errorprone 'com.google.errorprone:error_prone_core:2.36.0' } pluginManager.withPlugin("kotlin") { @@ -69,20 +67,32 @@ normalization { javadoc { description = "Generates project-level javadoc for use in -javadoc jar" + failOnError = true + options { + encoding = "UTF-8" + memberLevel = JavadocMemberLevel.PROTECTED + author = true + header = project.name + use = true + links(project.ext.javadocLinks) + setOutputLevel(JavadocOutputLevel.QUIET) + // Check for 'syntax' during linting. Note that the global + // 'framework-api:javadoc' task checks for 'reference' in addition + // to 'syntax'. + addBooleanOption("Xdoclint:syntax,-reference", true) + // Change modularity mismatch from warn to info. + // See https://github.com/spring-projects/spring-framework/issues/27497 + addStringOption("-link-modularity-mismatch", "info") + // With the javadoc tool on Java 24, it appears that the 'reference' + // group is always active and the '-reference' flag is not honored. + // Thus, we do NOT fail the build on Javadoc warnings due to + // cross-module @see and @link references which are only reachable + // when running the global 'framework-api:javadoc' task. + addBooleanOption('Werror', false) + } - options.encoding = "UTF-8" - options.memberLevel = JavadocMemberLevel.PROTECTED - options.author = true - options.header = project.name - options.use = true - options.links(project.ext.javadocLinks) - options.setOutputLevel(JavadocOutputLevel.QUIET) - // Check for syntax during linting. - options.addBooleanOption("Xdoclint:syntax", true) - - // Suppress warnings due to cross-module @see and @link references. - // Note that global 'api' task does display all warnings, and - // checks for 'reference' on top of 'syntax'. + // Attempt to suppress warnings due to cross-module @see and @link references. + // Note that the global 'framework-api:javadoc' task displays all warnings. logging.captureStandardError LogLevel.INFO logging.captureStandardOutput LogLevel.INFO // suppress "## warnings" message } @@ -114,15 +124,3 @@ publishing { components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { skip() } components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() } -tasks.withType(JavaCompile).configureEach { - options.errorprone { - disableAllChecks = true - option("NullAway:OnlyNullMarked", "true") - option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract") - option("NullAway:JSpecifyMode", "true") - } -} -tasks.compileJava { - // The check defaults to a warning, bump it up to an error for the main sources - options.errorprone.error("NullAway") -} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 9bbc975c74..1b33c55baa 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b1c8..ff23a68d70 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.13-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/gradlew b/gradlew index faf93008b7..23d15a9367 100755 --- a/gradlew +++ b/gradlew @@ -114,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9b42019c79..5eed7ee845 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java b/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java index b416fc3edf..a7aec02b83 100644 --- a/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java @@ -158,8 +158,7 @@ class AdvisorAutoProxyCreatorIntegrationTests { try { rb.echoException(new ServletException()); } - catch (ServletException ex) { - + catch (ServletException ignored) { } assertThat(txMan.commits).as("Transaction counts match").isEqualTo(1); } @@ -272,7 +271,7 @@ class OrderedTxCheckAdvisor extends StaticMethodMatcherPointcutAdvisor implement TransactionInterceptor.currentTransactionStatus(); throw new RuntimeException("Shouldn't have a transaction"); } - catch (NoTransactionException ex) { + catch (NoTransactionException ignored) { // this is Ok } } diff --git a/integration-tests/src/test/java/org/springframework/core/env/PropertyPlaceholderConfigurerEnvironmentIntegrationTests.java b/integration-tests/src/test/java/org/springframework/core/env/PropertyPlaceholderConfigurerEnvironmentIntegrationTests.java index 248000ce7b..f482ffd50f 100644 --- a/integration-tests/src/test/java/org/springframework/core/env/PropertyPlaceholderConfigurerEnvironmentIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/core/env/PropertyPlaceholderConfigurerEnvironmentIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,7 +25,7 @@ import static org.springframework.beans.factory.support.BeanDefinitionBuilder.ro class PropertyPlaceholderConfigurerEnvironmentIntegrationTests { @Test - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "removal"}) void test() { GenericApplicationContext ctx = new GenericApplicationContext(); ctx.registerBeanDefinition("ppc", diff --git a/settings.gradle b/settings.gradle index e9fe33a314..496091ee58 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,5 @@ plugins { id "io.spring.develocity.conventions" version "0.0.22" - id "org.gradle.toolchains.foojay-resolver-convention" version "0.9.0" } include "spring-aop" diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java index b9c657af0d..b1b196243b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.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. @@ -109,11 +109,11 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut private @Nullable BeanFactory beanFactory; - private transient @Nullable ClassLoader pointcutClassLoader; + private transient volatile @Nullable ClassLoader pointcutClassLoader; - private transient @Nullable PointcutExpression pointcutExpression; + private transient volatile @Nullable PointcutExpression pointcutExpression; - private transient boolean pointcutParsingFailed = false; + private transient volatile boolean pointcutParsingFailed; /** @@ -193,11 +193,14 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut * Lazily build the underlying AspectJ pointcut expression. */ private PointcutExpression obtainPointcutExpression() { - if (this.pointcutExpression == null) { - this.pointcutClassLoader = determinePointcutClassLoader(); - this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader); + PointcutExpression pointcutExpression = this.pointcutExpression; + if (pointcutExpression == null) { + ClassLoader pointcutClassLoader = determinePointcutClassLoader(); + pointcutExpression = buildPointcutExpression(pointcutClassLoader); + this.pointcutClassLoader = pointcutClassLoader; + this.pointcutExpression = pointcutExpression; } - return this.pointcutExpression; + return pointcutExpression; } /** @@ -460,40 +463,24 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut } private ShadowMatch getShadowMatch(Method targetMethod, Method originalMethod) { - ShadowMatch shadowMatch = ShadowMatchUtils.getShadowMatch(this, targetMethod); + ShadowMatchKey key = new ShadowMatchKey(this, targetMethod); + ShadowMatch shadowMatch = ShadowMatchUtils.getShadowMatch(key); if (shadowMatch == null) { - PointcutExpression fallbackExpression = null; - Method methodToMatch = targetMethod; - try { + PointcutExpression pointcutExpression = obtainPointcutExpression(); + synchronized (pointcutExpression) { + shadowMatch = ShadowMatchUtils.getShadowMatch(key); + if (shadowMatch != null) { + return shadowMatch; + } + PointcutExpression fallbackExpression = null; + Method methodToMatch = targetMethod; try { - shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch); - } - catch (ReflectionWorldException ex) { - // Failed to introspect target method, probably because it has been loaded - // in a special ClassLoader. Let's try the declaring ClassLoader instead... try { - fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass()); - if (fallbackExpression != null) { - shadowMatch = fallbackExpression.matchesMethodExecution(methodToMatch); - } - } - catch (ReflectionWorldException ex2) { - fallbackExpression = null; - } - } - if (targetMethod != originalMethod && (shadowMatch == null || - (Proxy.isProxyClass(targetMethod.getDeclaringClass()) && - (shadowMatch.neverMatches() || containsAnnotationPointcut())))) { - // Fall back to the plain original method in case of no resolvable match or a - // negative match on a proxy class (which doesn't carry any annotations on its - // redeclared methods), as well as for annotation pointcuts. - methodToMatch = originalMethod; - try { - shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch); + shadowMatch = pointcutExpression.matchesMethodExecution(methodToMatch); } catch (ReflectionWorldException ex) { - // Could neither introspect the target class nor the proxy class -> - // let's try the original method's declaring class before we give up... + // Failed to introspect target method, probably because it has been loaded + // in a special ClassLoader. Let's try the declaring ClassLoader instead... try { fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass()); if (fallbackExpression != null) { @@ -504,21 +491,45 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut fallbackExpression = null; } } + if (targetMethod != originalMethod && (shadowMatch == null || + (Proxy.isProxyClass(targetMethod.getDeclaringClass()) && + (shadowMatch.neverMatches() || containsAnnotationPointcut())))) { + // Fall back to the plain original method in case of no resolvable match or a + // negative match on a proxy class (which doesn't carry any annotations on its + // redeclared methods), as well as for annotation pointcuts. + methodToMatch = originalMethod; + try { + shadowMatch = pointcutExpression.matchesMethodExecution(methodToMatch); + } + catch (ReflectionWorldException ex) { + // Could neither introspect the target class nor the proxy class -> + // let's try the original method's declaring class before we give up... + try { + fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass()); + if (fallbackExpression != null) { + shadowMatch = fallbackExpression.matchesMethodExecution(methodToMatch); + } + } + catch (ReflectionWorldException ex2) { + fallbackExpression = null; + } + } + } } + catch (Throwable ex) { + // Possibly AspectJ 1.8.10 encountering an invalid signature + logger.debug("PointcutExpression matching rejected target method", ex); + fallbackExpression = null; + } + if (shadowMatch == null) { + shadowMatch = new ShadowMatchImpl(org.aspectj.util.FuzzyBoolean.NO, null, null, null); + } + else if (shadowMatch.maybeMatches() && fallbackExpression != null) { + shadowMatch = new DefensiveShadowMatch(shadowMatch, + fallbackExpression.matchesMethodExecution(methodToMatch)); + } + shadowMatch = ShadowMatchUtils.setShadowMatch(key, shadowMatch); } - catch (Throwable ex) { - // Possibly AspectJ 1.8.10 encountering an invalid signature - logger.debug("PointcutExpression matching rejected target method", ex); - fallbackExpression = null; - } - if (shadowMatch == null) { - shadowMatch = new ShadowMatchImpl(org.aspectj.util.FuzzyBoolean.NO, null, null, null); - } - else if (shadowMatch.maybeMatches() && fallbackExpression != null) { - shadowMatch = new DefensiveShadowMatch(shadowMatch, - fallbackExpression.matchesMethodExecution(methodToMatch)); - } - shadowMatch = ShadowMatchUtils.setShadowMatch(this, targetMethod, shadowMatch); } return shadowMatch; } @@ -616,14 +627,14 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut @Override @SuppressWarnings("rawtypes") - @Deprecated + @Deprecated(since = "4.0") // deprecated by AspectJ public boolean couldMatchJoinPointsInType(Class someClass) { return (contextMatch(someClass) == FuzzyBoolean.YES); } @Override @SuppressWarnings("rawtypes") - @Deprecated + @Deprecated(since = "4.0") // deprecated by AspectJ public boolean couldMatchJoinPointsInType(Class someClass, MatchingContext context) { return (contextMatch(someClass) == FuzzyBoolean.YES); } @@ -713,4 +724,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut } } + + private record ShadowMatchKey(AspectJExpressionPointcut expression, Method method) { + } + } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPoint.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPoint.java index 0b2eb35584..d65035de7c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPoint.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPoint.java @@ -319,7 +319,7 @@ public class MethodInvocationProceedingJoinPoint implements ProceedingJoinPoint, } @Override - @Deprecated + @Deprecated(since = "4.0") // deprecated by AspectJ public int getColumn() { throw new UnsupportedOperationException(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/ShadowMatchUtils.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/ShadowMatchUtils.java index 3c26b4abc5..5e45c6d7d9 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/ShadowMatchUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/ShadowMatchUtils.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,24 +16,46 @@ package org.springframework.aop.aspectj; -import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.aspectj.weaver.tools.ShadowMatch; import org.jspecify.annotations.Nullable; -import org.springframework.aop.support.ExpressionPointcut; - /** * Internal {@link ShadowMatch} utilities. * * @author Stephane Nicoll + * @author Juergen Hoeller * @since 6.2 */ public abstract class ShadowMatchUtils { - private static final Map shadowMatchCache = new ConcurrentHashMap<>(256); + private static final Map shadowMatchCache = new ConcurrentHashMap<>(256); + + + /** + * Find a {@link ShadowMatch} for the specified key. + * @param key the key to use + * @return the {@code ShadowMatch} to use for the specified key, + * or {@code null} if none found + */ + static @Nullable ShadowMatch getShadowMatch(Object key) { + return shadowMatchCache.get(key); + } + + /** + * Associate the {@link ShadowMatch} with the specified key. + * If an entry already exists, the given {@code shadowMatch} is ignored. + * @param key the key to use + * @param shadowMatch the shadow match to use for this key + * if none already exists + * @return the shadow match to use for the specified key + */ + static ShadowMatch setShadowMatch(Object key, ShadowMatch shadowMatch) { + ShadowMatch existing = shadowMatchCache.putIfAbsent(key, shadowMatch); + return (existing != null ? existing : shadowMatch); + } /** * Clear the cache of computed {@link ShadowMatch} instances. @@ -42,33 +64,4 @@ public abstract class ShadowMatchUtils { shadowMatchCache.clear(); } - /** - * Return the {@link ShadowMatch} for the specified {@link ExpressionPointcut} - * and {@link Method} or {@code null} if none is found. - * @param expression the expression - * @param method the method - * @return the {@code ShadowMatch} to use for the specified expression and method - */ - static @Nullable ShadowMatch getShadowMatch(ExpressionPointcut expression, Method method) { - return shadowMatchCache.get(new Key(expression, method)); - } - - /** - * Associate the {@link ShadowMatch} to the specified {@link ExpressionPointcut} - * and method. If an entry already exists, the given {@code shadowMatch} is - * ignored. - * @param expression the expression - * @param method the method - * @param shadowMatch the shadow match to use for this expression and method - * if none already exists - * @return the shadow match to use for the specified expression and method - */ - static ShadowMatch setShadowMatch(ExpressionPointcut expression, Method method, ShadowMatch shadowMatch) { - ShadowMatch existing = shadowMatchCache.putIfAbsent(new Key(expression, method), shadowMatch); - return (existing != null ? existing : shadowMatch); - } - - - private record Key(ExpressionPointcut expression, Method method) {} - } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java index bbe4eea5f0..be8aa13e3e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java @@ -110,7 +110,7 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto * Create a new {@code ReflectiveAspectJAdvisorFactory}, propagating the given * {@link BeanFactory} to the created {@link AspectJExpressionPointcut} instances, * for bean pointcut handling as well as consistent {@link ClassLoader} resolution. - * @param beanFactory the BeanFactory to propagate (may be {@code null}} + * @param beanFactory the BeanFactory to propagate (may be {@code null}) * @since 4.3.6 * @see AspectJExpressionPointcut#setBeanFactory * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBeanClassLoader() 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 447aa65082..b770c9ecda 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 @@ -36,6 +36,7 @@ import org.springframework.aop.AopInvocationException; 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.GeneratorStrategy; @@ -203,7 +204,7 @@ class CglibAopProxy implements AopProxy, Serializable { enhancer.setSuperclass(proxySuperClass); enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised)); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(true); + enhancer.setAttemptLoad(enhancer.getUseCache() && AotDetector.useGeneratedArtifacts()); enhancer.setStrategy(KotlinDetector.isKotlinType(proxySuperClass) ? new ClassLoaderAwareGeneratorStrategy(classLoader) : new ClassLoaderAwareGeneratorStrategy(classLoader, undeclaredThrowableStrategy) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java index 8ab713d01e..a9a3f88a91 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -112,10 +112,10 @@ public class BeanNameAutoProxyCreator extends AbstractAutoProxyCreator { boolean isFactoryBean = FactoryBean.class.isAssignableFrom(beanClass); for (String mappedName : this.beanNames) { if (isFactoryBean) { - if (!mappedName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) { + if (mappedName.isEmpty() || mappedName.charAt(0) != BeanFactory.FACTORY_BEAN_PREFIX_CHAR) { continue; } - mappedName = mappedName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length()); + mappedName = mappedName.substring(1); // length of '&' } if (isMatch(beanName, mappedName)) { return true; diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/AbstractProxyExceptionHandlingTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/AbstractProxyExceptionHandlingTests.java index f25b17a10b..bba53793cc 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/AbstractProxyExceptionHandlingTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/AbstractProxyExceptionHandlingTests.java @@ -29,7 +29,6 @@ import org.mockito.Mockito; import org.mockito.stubbing.Answer; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.BDDMockito.willAnswer; import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.mock; @@ -69,7 +68,12 @@ abstract class AbstractProxyExceptionHandlingTests { private void invokeProxy() { - throwableSeenByCaller = catchThrowable(() -> Objects.requireNonNull(proxy).doSomething()); + try { + Objects.requireNonNull(proxy).doSomething(); + } + catch (Throwable throwable) { + throwableSeenByCaller = throwable; + } } @SuppressWarnings("SameParameterValue") diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MethodCounter.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MethodCounter.java index 4551a8f3f1..093686261a 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MethodCounter.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MethodCounter.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. @@ -24,7 +24,7 @@ import java.util.Map; import org.jspecify.annotations.Nullable; /** - * Abstract superclass for counting advices etc. + * Abstract superclass for counting advice, etc. * * @author Rod Johnson * @author Chris Beams @@ -62,7 +62,7 @@ public class MethodCounter implements Serializable { */ @Override public boolean equals(@Nullable Object other) { - return (other != null && other.getClass() == this.getClass()); + return (other != null && getClass() == other.getClass()); } @Override diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java index 0da9c8fd52..984218b36a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -33,6 +33,7 @@ import java.util.Map; import java.util.Set; import kotlin.jvm.JvmClassMappingKt; +import kotlin.jvm.internal.DefaultConstructorMarker; import kotlin.reflect.KClass; import kotlin.reflect.KFunction; import kotlin.reflect.KParameter; @@ -94,10 +95,9 @@ public abstract class BeanUtils { * @return the new instance * @throws BeanInstantiationException if the bean cannot be instantiated * @see Class#newInstance() - * @deprecated as of Spring 5.0, following the deprecation of - * {@link Class#newInstance()} in JDK 9 + * @deprecated following the deprecation of {@link Class#newInstance()} in JDK 9 */ - @Deprecated + @Deprecated(since = "5.0") public static T instantiate(Class clazz) throws BeanInstantiationException { Assert.notNull(clazz, "Class must not be null"); if (clazz.isInterface()) { @@ -659,7 +659,9 @@ public abstract class BeanUtils { ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class); @Nullable String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(ctor)); Assert.state(paramNames != null, () -> "Cannot resolve parameter names for constructor " + ctor); - Assert.state(paramNames.length == ctor.getParameterCount(), + int parameterCount = (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasDefaultConstructorMarker(ctor) ? + ctor.getParameterCount() - 1 : ctor.getParameterCount()); + Assert.state(paramNames.length == parameterCount, () -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor); return paramNames; } @@ -928,6 +930,11 @@ public abstract class BeanUtils { } return kotlinConstructor.callBy(argParameters); } + + public static boolean hasDefaultConstructorMarker(Constructor ctor) { + int parameterCount = ctor.getParameterCount(); + return parameterCount > 0 && ctor.getParameters()[parameterCount -1].getType() == DefaultConstructorMarker.class; + } } } 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 c7b0cdc7cc..d84e123b80 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 @@ import org.jspecify.annotations.Nullable; * 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/BeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java index 8e113a729f..27f7959762 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.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. @@ -125,9 +125,16 @@ public interface BeanFactory { * beans created by the FactoryBean. For example, if the bean named * {@code myJndiObject} is a FactoryBean, getting {@code &myJndiObject} * will return the factory, not the instance returned by the factory. + * @see #FACTORY_BEAN_PREFIX_CHAR */ String FACTORY_BEAN_PREFIX = "&"; + /** + * Character variant of {@link #FACTORY_BEAN_PREFIX}. + * @since 6.2.6 + */ + char FACTORY_BEAN_PREFIX_CHAR = '&'; + /** * Return an instance, which may be shared or independent, of the specified bean. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java index 2e9580b6c6..3f6921f22e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java @@ -73,7 +73,7 @@ public abstract class BeanFactoryUtils { * @see BeanFactory#FACTORY_BEAN_PREFIX */ public static boolean isFactoryDereference(@Nullable String name) { - return (name != null && name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)); + return (name != null && !name.isEmpty() && name.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR); } /** @@ -85,14 +85,14 @@ public abstract class BeanFactoryUtils { */ public static String transformedBeanName(String name) { Assert.notNull(name, "'name' must not be null"); - if (!name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) { + if (name.isEmpty() || name.charAt(0) != BeanFactory.FACTORY_BEAN_PREFIX_CHAR) { return name; } return transformedBeanNameCache.computeIfAbsent(name, beanName -> { do { - beanName = beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length()); + beanName = beanName.substring(1); // length of '&' } - while (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)); + while (beanName.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR); return beanName; }); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistry.java index 5a81d6f8e6..b5eeec43cf 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistry.java @@ -55,9 +55,8 @@ public interface BeanRegistry { void registerAlias(String name, String alias); /** - * Register a bean from the given bean class, which will be instantiated - * using the related {@link BeanUtils#getResolvableConstructor resolvable constructor} - * if any. + * Register a bean from the given bean class, which will be instantiated using the + * related {@link BeanUtils#getResolvableConstructor resolvable constructor} if any. * @param beanClass the class of the bean * @return the generated bean name */ @@ -65,10 +64,9 @@ public interface BeanRegistry { /** * Register a bean from the given bean class, customizing it with the customizer - * callback. The bean will be instantiated using the supplier that can be - * configured in the customizer callback, or will be tentatively instantiated - * with its {@link BeanUtils#getResolvableConstructor resolvable constructor} - * otherwise. + * callback. The bean will be instantiated using the supplier that can be configured + * in the customizer callback, or will be tentatively instantiated with its + * {@link BeanUtils#getResolvableConstructor resolvable constructor} otherwise. * @param beanClass the class of the bean * @param customizer callback to customize other bean properties than the name * @return the generated bean name @@ -76,9 +74,8 @@ public interface BeanRegistry { String registerBean(Class beanClass, Consumer> customizer); /** - * Register a bean from the given bean class, which will be instantiated - * using the related {@link BeanUtils#getResolvableConstructor resolvable constructor} - * if any. + * Register a bean from the given bean class, which will be instantiated using the + * related {@link BeanUtils#getResolvableConstructor resolvable constructor} if any. * @param name the name of the bean * @param beanClass the class of the bean */ @@ -86,8 +83,8 @@ public interface BeanRegistry { /** * Register a bean from the given bean class, customizing it with the customizer - * callback. The bean will be instantiated using the supplier that can be - * configured in the customizer callback, or will be tentatively instantiated with its + * callback. The bean will be instantiated using the supplier that can be configured + * in the customizer callback, or will be tentatively instantiated with its * {@link BeanUtils#getResolvableConstructor resolvable constructor} otherwise. * @param name the name of the bean * @param beanClass the class of the bean @@ -122,8 +119,8 @@ public interface BeanRegistry { Spec fallback(); /** - * Hint that this bean has an infrastructure role, meaning it has no - * relevance to the end-user. + * Hint that this bean has an infrastructure role, meaning it has no relevance + * to the end-user. * @see BeanDefinition#setRole(int) * @see BeanDefinition#ROLE_INFRASTRUCTURE */ @@ -136,8 +133,7 @@ public interface BeanRegistry { Spec lazyInit(); /** - * Configure this bean as not a candidate for getting autowired into some - * other bean. + * Configure this bean as not a candidate for getting autowired into another bean. * @see BeanDefinition#setAutowireCandidate(boolean) */ Spec notAutowirable(); @@ -184,6 +180,7 @@ public interface BeanRegistry { Spec targetType(ResolvableType type); } + /** * Context available from the bean instance supplier designed to give access * to bean dependencies. @@ -191,10 +188,8 @@ public interface BeanRegistry { interface SupplierContext { /** - * Return the bean instance that uniquely matches the given object type, - * if any. - * @param requiredType type the bean must match; can be an interface or - * superclass + * Return the bean instance that uniquely matches the given object type, if any. + * @param requiredType type the bean must match; can be an interface or superclass * @return an instance of the single bean matching the required type * @see BeanFactory#getBean(String) */ @@ -240,4 +235,5 @@ public interface BeanRegistry { */ ObjectProvider beanProvider(ResolvableType requiredType); } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java index df8ad91c32..7387226831 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java @@ -208,8 +208,8 @@ public abstract class BeanFactoryAnnotationUtils { } } } - catch (NoSuchBeanDefinitionException ex) { - // Ignore - can't compare qualifiers for a manually registered singleton object + catch (NoSuchBeanDefinitionException ignored) { + // can't compare qualifiers for a manually registered singleton object } } return false; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredArgumentsCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredArgumentsCodeGenerator.java index 0afc2419a8..c5c863df73 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredArgumentsCodeGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredArgumentsCodeGenerator.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. @@ -68,10 +68,10 @@ public class AutowiredArgumentsCodeGenerator { for (int i = startIndex; i < parameterTypes.length; i++) { code.add(i > startIndex ? ", " : ""); if (!ambiguous) { - code.add("$L.get($L)", variableName, i - startIndex); + code.add("$L.get($L)", variableName, i); } else { - code.add("$L.get($L, $T.class)", variableName, i - startIndex, parameterTypes[i]); + code.add("$L.get($L, $T.class)", variableName, i, parameterTypes[i]); } } return code.build(); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java index 6aca2880b7..e983260eeb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java @@ -38,7 +38,6 @@ import org.jspecify.annotations.Nullable; import org.springframework.aot.generate.GeneratedMethods; import org.springframework.aot.generate.ValueCodeGenerator; import org.springframework.aot.generate.ValueCodeGenerator.Delegate; -import org.springframework.aot.generate.ValueCodeGeneratorDelegates; import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; @@ -103,12 +102,12 @@ class BeanDefinitionPropertiesCodeGenerator { this.hints = hints; this.attributeFilter = attributeFilter; - List allDelegates = new ArrayList<>(); - allDelegates.add((valueCodeGenerator, value) -> customValueCodeGenerator.apply(PropertyNamesStack.peek(), value)); - allDelegates.addAll(additionalDelegates); - allDelegates.addAll(BeanDefinitionPropertyValueCodeGeneratorDelegates.INSTANCES); - allDelegates.addAll(ValueCodeGeneratorDelegates.INSTANCES); - this.valueCodeGenerator = ValueCodeGenerator.with(allDelegates).scoped(generatedMethods); + List customDelegates = new ArrayList<>(); + customDelegates.add((valueCodeGenerator, value) -> + customValueCodeGenerator.apply(PropertyNamesStack.peek(), value)); + customDelegates.addAll(additionalDelegates); + this.valueCodeGenerator = BeanDefinitionPropertyValueCodeGeneratorDelegates + .createValueCodeGenerator(generatedMethods, customDelegates); } @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1128 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 e07558a810..c1d4108b7a 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-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.beans.factory.aot; +import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -46,7 +47,7 @@ import org.springframework.javapoet.CodeBlock; * @author Stephane Nicoll * @since 6.1.2 */ -abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { +public abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { /** * A list of {@link Delegate} implementations for the following common bean @@ -73,6 +74,26 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { ); + /** + * Create a {@link ValueCodeGenerator} instance with both these + * {@link #INSTANCES delegate} and the {@link ValueCodeGeneratorDelegates#INSTANCES + * core delegates}. + * @param generatedMethods the {@link GeneratedMethods} to use + * @param customDelegates additional delegates that should be considered first + * @return a configured value code generator + * @since 7.0 + * @see ValueCodeGenerator#add(List) + */ + public static ValueCodeGenerator createValueCodeGenerator( + GeneratedMethods generatedMethods, List customDelegates) { + List allDelegates = new ArrayList<>(); + allDelegates.addAll(customDelegates); + allDelegates.addAll(INSTANCES); + allDelegates.addAll(ValueCodeGeneratorDelegates.INSTANCES); + return ValueCodeGenerator.with(allDelegates).scoped(generatedMethods); + } + + /** * {@link Delegate} for {@link ManagedList} types. */ @@ -155,6 +176,8 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { .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/config/AutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowireCapableBeanFactory.java index 46dff4eab4..48ba5e61ea 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowireCapableBeanFactory.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. @@ -97,10 +97,10 @@ public interface AutowireCapableBeanFactory extends BeanFactory { * Constant that indicates determining an appropriate autowire strategy * through introspection of the bean class. * @see #autowire - * @deprecated as of Spring 3.0: If you are using mixed autowiring strategies, - * prefer annotation-based autowiring for clearer demarcation of autowiring needs. + * @deprecated If you are using mixed autowiring strategies, prefer + * annotation-based autowiring for clearer demarcation of autowiring needs. */ - @Deprecated + @Deprecated(since = "3.0") int AUTOWIRE_AUTODETECT = 4; /** @@ -188,7 +188,7 @@ public interface AutowireCapableBeanFactory extends BeanFactory { * @see #AUTOWIRE_BY_NAME * @see #AUTOWIRE_BY_TYPE * @see #AUTOWIRE_CONSTRUCTOR - * @deprecated as of 6.1, in favor of {@link #createBean(Class)} + * @deprecated in favor of {@link #createBean(Class)} */ @Deprecated(since = "6.1") Object createBean(Class beanClass, int autowireMode, boolean dependencyCheck) throws BeansException; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java index 41c8a815da..884d871893 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java @@ -19,21 +19,18 @@ package org.springframework.beans.factory.config; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; -import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.Map; import java.util.Optional; -import kotlin.reflect.KProperty; -import kotlin.reflect.jvm.ReflectJvmMapping; import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.InjectionPoint; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; -import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; +import org.springframework.core.Nullness; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; import org.springframework.core.convert.TypeDescriptor; @@ -162,28 +159,13 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable } if (this.field != null) { - return !(this.field.getType() == Optional.class || hasNullableAnnotation() || - (KotlinDetector.isKotlinType(this.field.getDeclaringClass()) && KotlinDelegate.isNullable(this.field))); + return !(this.field.getType() == Optional.class || Nullness.forField(this.field) == Nullness.NULLABLE); } else { return !obtainMethodParameter().isOptional(); } } - /** - * Check whether the underlying field is annotated with any variant of a - * {@code Nullable} annotation, for example, {@code jakarta.annotation.Nullable} or - * {@code edu.umd.cs.findbugs.annotations.Nullable}. - */ - private boolean hasNullableAnnotation() { - for (Annotation ann : getAnnotations()) { - if ("Nullable".equals(ann.annotationType().getSimpleName())) { - return true; - } - } - return false; - } - /** * Return whether this dependency is 'eager' in the sense of * eagerly resolving potential target beans for type matching. @@ -445,19 +427,4 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable } } - - /** - * Inner class to avoid a hard dependency on Kotlin at runtime. - */ - private static class KotlinDelegate { - - /** - * Check whether the specified {@link Field} represents a nullable Kotlin type or not. - */ - public static boolean isNullable(Field field) { - KProperty property = ReflectJvmMapping.getKotlinProperty(field); - return (property != null && property.getReturnType().isMarkedNullable()); - } - } - } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java index 6380f164bb..5ff44380d8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.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. @@ -22,7 +22,9 @@ import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanNameAware; +import org.springframework.core.env.AbstractPropertyResolver; import org.springframework.util.StringValueResolver; +import org.springframework.util.SystemPropertyUtils; /** * Abstract base class for property resource configurers that resolve placeholders @@ -38,16 +40,16 @@ import org.springframework.util.StringValueResolver; * *

  * <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
- *   <property name="driverClassName" value="${driver}" />
- *   <property name="url" value="jdbc:${dbname}" />
+ *   <property name="driverClassName" value="${jdbc.driver}" />
+ *   <property name="url" value="jdbc:${jdbc.dbname}" />
  * </bean>
  * 
* * Example properties file: * *
- * driver=com.mysql.jdbc.Driver
- * dbname=mysql:mydb
+ * jdbc.driver=com.mysql.jdbc.Driver + * jdbc.dbname=mysql:mydb * * Annotated bean definitions may take advantage of property replacement using * the {@link org.springframework.beans.factory.annotation.Value @Value} annotation: @@ -80,11 +82,12 @@ import org.springframework.util.StringValueResolver; *

Example XML property with default value: * *

- *   <property name="url" value="jdbc:${dbname:defaultdb}" />
+ *   <property name="url" value="jdbc:${jdbc.dbname:defaultdb}" />
  * 
* * @author Chris Beams * @author Juergen Hoeller + * @author Sam Brannen * @since 3.1 * @see PropertyPlaceholderConfigurer * @see org.springframework.context.support.PropertySourcesPlaceholderConfigurer @@ -93,16 +96,21 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi implements BeanNameAware, BeanFactoryAware { /** Default placeholder prefix: {@value}. */ - public static final String DEFAULT_PLACEHOLDER_PREFIX = "${"; + public static final String DEFAULT_PLACEHOLDER_PREFIX = SystemPropertyUtils.PLACEHOLDER_PREFIX; /** Default placeholder suffix: {@value}. */ - public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}"; + public static final String DEFAULT_PLACEHOLDER_SUFFIX = SystemPropertyUtils.PLACEHOLDER_SUFFIX; /** Default value separator: {@value}. */ - public static final String DEFAULT_VALUE_SEPARATOR = ":"; + public static final String DEFAULT_VALUE_SEPARATOR = SystemPropertyUtils.VALUE_SEPARATOR; + + /** + * Default escape character: {@code '\'}. + * @since 6.2 + * @see AbstractPropertyResolver#getDefaultEscapeCharacter() + */ + public static final Character DEFAULT_ESCAPE_CHARACTER = SystemPropertyUtils.ESCAPE_CHARACTER; - /** Default escape character: {@code '\'}. */ - public static final Character DEFAULT_ESCAPE_CHARACTER = '\\'; /** Defaults to {@value #DEFAULT_PLACEHOLDER_PREFIX}. */ protected String placeholderPrefix = DEFAULT_PLACEHOLDER_PREFIX; @@ -113,8 +121,10 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi /** Defaults to {@value #DEFAULT_VALUE_SEPARATOR}. */ protected @Nullable String valueSeparator = DEFAULT_VALUE_SEPARATOR; - /** Defaults to {@link #DEFAULT_ESCAPE_CHARACTER}. */ - protected @Nullable Character escapeCharacter = DEFAULT_ESCAPE_CHARACTER; + /** + * The default is determined by {@link AbstractPropertyResolver#getDefaultEscapeCharacter()}. + */ + protected @Nullable Character escapeCharacter = AbstractPropertyResolver.getDefaultEscapeCharacter(); protected boolean trimValues = false; @@ -129,7 +139,7 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi /** * Set the prefix that a placeholder string starts with. - * The default is {@value #DEFAULT_PLACEHOLDER_PREFIX}. + *

The default is {@value #DEFAULT_PLACEHOLDER_PREFIX}. */ public void setPlaceholderPrefix(String placeholderPrefix) { this.placeholderPrefix = placeholderPrefix; @@ -137,31 +147,32 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi /** * Set the suffix that a placeholder string ends with. - * The default is {@value #DEFAULT_PLACEHOLDER_SUFFIX}. + *

The default is {@value #DEFAULT_PLACEHOLDER_SUFFIX}. */ public void setPlaceholderSuffix(String placeholderSuffix) { this.placeholderSuffix = placeholderSuffix; } /** - * Specify the separating character between the placeholder variable - * and the associated default value, or {@code null} if no such - * special character should be processed as a value separator. - * The default is {@value #DEFAULT_VALUE_SEPARATOR}. + * Specify the separating character between the placeholder variable and the + * associated default value, or {@code null} if no such special character + * should be processed as a value separator. + *

The default is {@value #DEFAULT_VALUE_SEPARATOR}. */ public void setValueSeparator(@Nullable String valueSeparator) { this.valueSeparator = valueSeparator; } /** - * Specify the escape character to use to ignore placeholder prefix - * or value separator, or {@code null} if no escaping should take - * place. - *

Default is {@link #DEFAULT_ESCAPE_CHARACTER}. + * Set the escape character to use to ignore the + * {@linkplain #setPlaceholderPrefix(String) placeholder prefix} and the + * {@linkplain #setValueSeparator(String) value separator}, or {@code null} + * if no escaping should take place. + *

The default is determined by {@link AbstractPropertyResolver#getDefaultEscapeCharacter()}. * @since 6.2 */ - public void setEscapeCharacter(@Nullable Character escsEscapeCharacter) { - this.escapeCharacter = escsEscapeCharacter; + public void setEscapeCharacter(@Nullable Character escapeCharacter) { + this.escapeCharacter = escapeCharacter; } /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PreferencesPlaceholderConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PreferencesPlaceholderConfigurer.java index e100e9e314..f4a28b3e14 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PreferencesPlaceholderConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PreferencesPlaceholderConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,8 +26,8 @@ import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.InitializingBean; /** - * Subclass of PropertyPlaceholderConfigurer that supports JDK 1.4's - * Preferences API ({@code java.util.prefs}). + * Subclass of {@link PropertyPlaceholderConfigurer} that supports JDK 1.4's + * {@link Preferences} API. * *

Tries to resolve placeholders as keys first in the user preferences, * then in the system preferences, then in this configurer's properties. @@ -43,9 +43,10 @@ import org.springframework.beans.factory.InitializingBean; * @see #setSystemTreePath * @see #setUserTreePath * @see java.util.prefs.Preferences - * @deprecated as of 5.2, along with {@link PropertyPlaceholderConfigurer} + * @deprecated as of 5.2, along with {@link PropertyPlaceholderConfigurer}; to be removed in 8.0 */ -@Deprecated +@Deprecated(since = "5.2", forRemoval = true) +@SuppressWarnings({"deprecation", "removal"}) public class PreferencesPlaceholderConfigurer extends PropertyPlaceholderConfigurer implements InitializingBean { private @Nullable String systemTreePath; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java index 840a34e762..0a26f20419 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.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,12 +35,14 @@ import org.springframework.beans.factory.BeanInitializationException; * * Example properties file: * - *

dataSource.driverClassName=com.mysql.jdbc.Driver
+ * 
+ * dataSource.driverClassName=com.mysql.jdbc.Driver
  * dataSource.url=jdbc:mysql:mydb
* - * In contrast to PropertyPlaceholderConfigurer, the original definition can have default - * values or no values at all for such bean properties. If an overriding properties file does - * not have an entry for a certain bean property, the default context definition is used. + *

In contrast to {@link PropertyPlaceholderConfigurer}, the original definition + * can have default values or no values at all for such bean properties. If an + * overriding properties file does not have an entry for a certain bean property, + * the default context definition is used. * *

Note that the context definition is not aware of being overridden; * so this is not immediately obvious when looking at the XML definition file. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java index 77d430073e..81d5eb24df 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.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. @@ -52,11 +52,13 @@ import org.springframework.util.StringValueResolver; * @see #setSystemPropertiesModeName * @see PlaceholderConfigurerSupport * @see PropertyOverrideConfigurer - * @deprecated as of 5.2; use {@code org.springframework.context.support.PropertySourcesPlaceholderConfigurer} - * instead which is more flexible through taking advantage of the {@link org.springframework.core.env.Environment} - * and {@link org.springframework.core.env.PropertySource} mechanisms. + * @deprecated as of 5.2, to be removed in 8.0; + * use {@code org.springframework.context.support.PropertySourcesPlaceholderConfigurer} + * instead which is more flexible through taking advantage of the + * {@link org.springframework.core.env.Environment} and + * {@link org.springframework.core.env.PropertySource} mechanisms. */ -@Deprecated +@Deprecated(since = "5.2", forRemoval = true) public class PropertyPlaceholderConfigurer extends PlaceholderConfigurerSupport { /** Never check system properties. */ diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index 0fcaacd6c1..bdd1e9f836 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -143,8 +143,10 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac private final Set> ignoredDependencyTypes = new HashSet<>(); /** - * Dependency interfaces to ignore on dependency check and autowire, as Set of - * Class objects. By default, only the BeanFactory interface is ignored. + * Dependency interfaces to ignore on dependency check and autowire, as a Set + * of Class objects. + *

By default, the {@code BeanNameAware}, {@code BeanFactoryAware}, and + * {@code BeanClassLoaderAware} interfaces are ignored. */ private final Set> ignoredDependencyInterfaces = new HashSet<>(); @@ -283,11 +285,15 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac /** * Ignore the given dependency interface for autowiring. *

This will typically be used by application contexts to register - * dependencies that are resolved in other ways, like BeanFactory through - * BeanFactoryAware or ApplicationContext through ApplicationContextAware. - *

By default, only the BeanFactoryAware interface is ignored. + * dependencies that are resolved in other ways, like {@code BeanFactory} + * through {@code BeanFactoryAware} or {@code ApplicationContext} through + * {@code ApplicationContextAware}. + *

By default, the {@code BeanNameAware}, {@code BeanFactoryAware}, and + * {@code BeanClassLoaderAware} interfaces are ignored. * For further types to ignore, invoke this method for each type. + * @see org.springframework.beans.factory.BeanNameAware * @see org.springframework.beans.factory.BeanFactoryAware + * @see org.springframework.beans.factory.BeanClassLoaderAware * @see org.springframework.context.ApplicationContextAware */ public void ignoreDependencyInterface(Class ifc) { @@ -357,7 +363,7 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac // Specialized methods for fine-grained control over the bean lifecycle //------------------------------------------------------------------------- - @Deprecated + @Deprecated(since = "6.1") @Override public Object createBean(Class beanClass, int autowireMode, boolean dependencyCheck) throws BeansException { // Use non-singleton bean definition, to avoid registering bean as dependent bean. @@ -984,9 +990,17 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac * that we couldn't obtain a shortcut FactoryBean instance */ private @Nullable FactoryBean getSingletonFactoryBeanForTypeCheck(String beanName, RootBeanDefinition mbd) { - boolean locked = this.singletonLock.tryLock(); - if (!locked) { - return null; + Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock(); + if (lockFlag == null) { + this.singletonLock.lock(); + } + else { + boolean locked = (lockFlag && this.singletonLock.tryLock()); + if (!locked) { + // Avoid shortcut FactoryBean instance but allow for subsequent type-based resolution. + resolveBeanClass(mbd, beanName); + return null; + } } try { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java index 2a8a44e326..c584eb56c2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.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. @@ -95,10 +95,10 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess * Constant that indicates determining an appropriate autowire strategy * through introspection of the bean class. * @see #setAutowireMode - * @deprecated as of Spring 3.0: If you are using mixed autowiring strategies, - * use annotation-based autowiring for clearer demarcation of autowiring needs. + * @deprecated If you are using mixed autowiring strategies, use + * annotation-based autowiring for clearer demarcation of autowiring needs. */ - @Deprecated + @Deprecated(since = "3.0") public static final int AUTOWIRE_AUTODETECT = AutowireCapableBeanFactory.AUTOWIRE_AUTODETECT; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java index 692571f5f0..181705b16a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java @@ -762,16 +762,16 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp public String[] getAliases(String name) { String beanName = transformedBeanName(name); List aliases = new ArrayList<>(); - boolean factoryPrefix = name.startsWith(FACTORY_BEAN_PREFIX); + boolean hasFactoryPrefix = (!name.isEmpty() && name.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR); String fullBeanName = beanName; - if (factoryPrefix) { + if (hasFactoryPrefix) { fullBeanName = FACTORY_BEAN_PREFIX + beanName; } if (!fullBeanName.equals(name)) { aliases.add(fullBeanName); } String[] retrievedAliases = super.getAliases(beanName); - String prefix = (factoryPrefix ? FACTORY_BEAN_PREFIX : ""); + String prefix = (hasFactoryPrefix ? FACTORY_BEAN_PREFIX : ""); for (String retrievedAlias : retrievedAliases) { String alias = prefix + retrievedAlias; if (!alias.equals(name)) { @@ -1137,7 +1137,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp public BeanDefinition getMergedBeanDefinition(String name) throws BeansException { String beanName = transformedBeanName(name); // Efficiently check whether bean definition exists in this factory. - if (!containsBeanDefinition(beanName) && getParentBeanFactory() instanceof ConfigurableBeanFactory parent) { + if (getParentBeanFactory() instanceof ConfigurableBeanFactory parent && !containsBeanDefinition(beanName)) { return parent.getMergedBeanDefinition(beanName); } // Resolve merged bean definition locally. @@ -1276,7 +1276,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp */ protected String originalBeanName(String name) { String beanName = transformedBeanName(name); - if (name.startsWith(FACTORY_BEAN_PREFIX)) { + if (!name.isEmpty() && name.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR) { beanName = FACTORY_BEAN_PREFIX + beanName; } return beanName; @@ -1458,7 +1458,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp // Cache the merged bean definition for the time being // (it might still get re-merged later on in order to pick up metadata changes) if (containingBd == null && (isCacheBeanMetadata() || isBeanEligibleForMetadataCaching(beanName))) { - this.mergedBeanDefinitions.put(beanName, mbd); + cacheMergedBeanDefinition(mbd, beanName); } } if (previous != null) { @@ -1487,6 +1487,18 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp } } + /** + * Cache the given merged bean definition. + *

Subclasses can override this to derive additional cached state + * from the final post-processed bean definition. + * @param mbd the merged bean definition to cache + * @param beanName the name of the bean + * @since 6.2.6 + */ + protected void cacheMergedBeanDefinition(RootBeanDefinition mbd, String beanName) { + this.mergedBeanDefinitions.put(beanName, mbd); + } + /** * Check the given merged bean definition, * potentially throwing validation exceptions. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReader.java index b6eeb3ae23..2fe442ef73 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReader.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. @@ -30,10 +30,6 @@ import org.springframework.core.io.ResourceLoader; * load and register methods for bean definitions, specific to * their bean definition format. * - *

Note that a bean definition reader does not have to implement - * this interface. It only serves as a suggestion for bean definition - * readers that want to follow standard naming conventions. - * * @author Juergen Hoeller * @since 1.1 * @see org.springframework.core.io.Resource 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 416e7b29f9..baa679b599 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-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. @@ -23,6 +23,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; +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 CglibSubclassingInstantiationStrategy extends SimpleInstantiationSt 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)); @@ -279,7 +280,6 @@ public class CglibSubclassingInstantiationStrategy extends SimpleInstantiationSt public @Nullable 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 processReturnType(method, 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 8aa4231646..54a4930535 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 @@ -130,16 +130,19 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable { /** - * System property that instructs Spring to enforce string locking during bean creation, + * System property that instructs Spring to enforce strict locking during bean creation, * rather than the mix of strict and lenient locking that 6.2 applies by default. Setting * this flag to "true" restores 6.1.x style locking in the entire pre-instantiation phase. + *

By default, the factory infers strict locking from the encountered thread names: + * If additional threads have names that match the thread prefix of the main bootstrap thread, + * they are considered external (multiple external bootstrap threads calling into the factory) + * and therefore have strict locking applied to them. This inference can be turned off through + * explicitly setting this flag to "false" rather than leaving it unspecified. * @since 6.2.6 * @see #preInstantiateSingletons() */ public static final String STRICT_LOCKING_PROPERTY_NAME = "spring.locking.strict"; - private static final boolean lenientLockingAllowed = !SpringProperties.getFlag(STRICT_LOCKING_PROPERTY_NAME); - private static @Nullable Class jakartaInjectProviderClass; static { @@ -158,6 +161,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto private static final Map> serializableFactories = new ConcurrentHashMap<>(8); + /** Whether strict locking is enforced or relaxed in this factory. */ + private final @Nullable Boolean strictLocking = SpringProperties.checkFlag(STRICT_LOCKING_PROPERTY_NAME); + /** Optional id for this factory, for serialization purposes. */ private @Nullable String serializationId; @@ -205,7 +211,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto /** Whether bean definition metadata may be cached for all beans. */ private volatile boolean configurationFrozen; - private volatile boolean preInstantiationPhase; + /** Name prefix of main thread: only set during pre-instantiation phase. */ + private volatile @Nullable String mainThreadPrefix; private final NamedThreadLocal preInstantiationThread = new NamedThreadLocal<>("Pre-instantiation thread marker"); @@ -626,10 +633,15 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto } } else { - if (includeNonSingletons || isNonLazyDecorated || - (allowFactoryBeanInit && isSingleton(beanName, mbd, dbd))) { + if (includeNonSingletons || isNonLazyDecorated) { matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit); } + else if (allowFactoryBeanInit) { + // Type check before singleton check, avoiding FactoryBean instantiation + // for early FactoryBean.isSingleton() calls on non-matching beans. + matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit) && + isSingleton(beanName, mbd, dbd); + } if (!matchFound) { // In case of FactoryBean, try to match FactoryBean instance itself next. beanName = FACTORY_BEAN_PREFIX + beanName; @@ -1004,6 +1016,14 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto return super.obtainInstanceFromSupplier(supplier, beanName, mbd); } + @Override + protected void cacheMergedBeanDefinition(RootBeanDefinition mbd, String beanName) { + super.cacheMergedBeanDefinition(mbd, beanName); + if (mbd.isPrimary()) { + this.primaryBeanNames.add(beanName); + } + } + @Override protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object @Nullable [] args) { super.checkMergedBeanDefinition(mbd, beanName, args); @@ -1016,7 +1036,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto } } else { - // Bean intended to be initialized in main bootstrap thread + // Bean intended to be initialized in main bootstrap thread. if (this.preInstantiationThread.get() == PreInstantiation.BACKGROUND) { throw new BeanCurrentlyInCreationException(beanName, "Bean marked for mainline initialization " + "but requested in background thread - enforce early instantiation in mainline thread " + @@ -1027,8 +1047,40 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto @Override protected @Nullable Boolean isCurrentThreadAllowedToHoldSingletonLock() { - return (lenientLockingAllowed && this.preInstantiationPhase ? - this.preInstantiationThread.get() != PreInstantiation.BACKGROUND : null); + String mainThreadPrefix = this.mainThreadPrefix; + if (mainThreadPrefix != null) { + // We only differentiate in the preInstantiateSingletons phase, using + // the volatile mainThreadPrefix field as an indicator for that phase. + + PreInstantiation preInstantiation = this.preInstantiationThread.get(); + if (preInstantiation != null) { + // A Spring-managed bootstrap thread: + // MAIN is allowed to lock (true) or even forced to lock (null), + // BACKGROUND is never allowed to lock (false). + return switch (preInstantiation) { + case MAIN -> (Boolean.TRUE.equals(this.strictLocking) ? null : true); + case BACKGROUND -> false; + }; + } + + // Not a Spring-managed bootstrap thread... + if (Boolean.FALSE.equals(this.strictLocking)) { + // Explicitly configured to use lenient locking wherever possible. + return true; + } + else if (this.strictLocking == null) { + // No explicit locking configuration -> infer appropriate locking. + if (!getThreadNamePrefix().equals(mainThreadPrefix)) { + // An unmanaged thread (assumed to be application-internal) with lenient locking, + // and not part of the same thread pool that provided the main bootstrap thread + // (excluding scenarios where we are hit by multiple external bootstrap threads). + return true; + } + } + } + + // Traditional behavior: forced to always hold a full lock. + return null; } @Override @@ -1044,8 +1096,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto // Trigger initialization of all non-lazy singleton beans... List> futures = new ArrayList<>(); - this.preInstantiationPhase = true; this.preInstantiationThread.set(PreInstantiation.MAIN); + this.mainThreadPrefix = getThreadNamePrefix(); try { for (String beanName : beanNames) { RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); @@ -1058,8 +1110,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto } } finally { + this.mainThreadPrefix = null; this.preInstantiationThread.remove(); - this.preInstantiationPhase = false; } if (!futures.isEmpty()) { @@ -1152,6 +1204,12 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto } } + private static String getThreadNamePrefix() { + String name = Thread.currentThread().getName(); + int numberSeparator = name.lastIndexOf('-'); + return (numberSeparator >= 0 ? name.substring(0, numberSeparator) : name); + } + //--------------------------------------------------------------------- // Implementation of BeanDefinitionRegistry interface @@ -1678,7 +1736,6 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto throw new BeanNotOfRequiredTypeException(name, type, candidate.getClass()); } return result; - } private @Nullable Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable String beanName, @@ -1910,7 +1967,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto else if (containsSingleton(candidateName) || (descriptor instanceof StreamDependencyDescriptor streamDescriptor && streamDescriptor.isOrdered())) { Object beanInstance = descriptor.resolveCandidate(candidateName, requiredType, this); - candidates.put(candidateName, (beanInstance instanceof NullBean ? null : beanInstance)); + candidates.put(candidateName, beanInstance); } else { candidates.put(candidateName, getType(candidateName)); @@ -2154,7 +2211,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto * i.e. whether the candidate points back to the original bean or to a factory method * on the original bean. */ - @Contract("null, _ -> false;_, null -> false;") + @Contract("null, _ -> false; _, null -> false;") private boolean isSelfReference(@Nullable String beanName, @Nullable String candidateName) { return (beanName != null && candidateName != null && (beanName.equals(candidateName) || (containsBeanDefinition(candidateName) && 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 f7036864cc..b412589b6e 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 @@ -17,6 +17,7 @@ package org.springframework.beans.factory.support; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; @@ -111,6 +112,12 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements /** Names of beans that are currently in lenient creation. */ private final Set singletonsInLenientCreation = new HashSet<>(); + /** Map from one creation thread waiting on a lenient creation thread. */ + private final Map lenientWaitingThreads = new HashMap<>(); + + /** Map from bean name to actual creation thread for currently created beans. */ + private final Map currentCreationThreads = new ConcurrentHashMap<>(); + /** Flag that indicates whether we're currently within destroySingletons. */ private volatile boolean singletonsCurrentlyInDestruction = false; @@ -248,9 +255,11 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements public Object getSingleton(String beanName, ObjectFactory singletonFactory) { Assert.notNull(beanName, "Bean name must not be null"); + Thread currentThread = Thread.currentThread(); Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock(); boolean acquireLock = !Boolean.FALSE.equals(lockFlag); boolean locked = (acquireLock && this.singletonLock.tryLock()); + try { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { @@ -260,13 +269,15 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements // Fallback as of 6.2: process given singleton bean outside of singleton lock. // Thread-safe exposure is still guaranteed, there is just a risk of collisions // when triggering creation of other beans as dependencies of the current bean. - if (logger.isInfoEnabled()) { - logger.info("Creating singleton bean '" + beanName + "' in thread \"" + - Thread.currentThread().getName() + "\" while other thread holds " + - "singleton lock for other beans " + this.singletonsCurrentlyInCreation); - } this.lenientCreationLock.lock(); try { + if (logger.isInfoEnabled()) { + Set lockedBeans = new HashSet<>(this.singletonsCurrentlyInCreation); + lockedBeans.removeAll(this.singletonsInLenientCreation); + logger.info("Obtaining singleton bean '" + beanName + "' in thread \"" + + currentThread.getName() + "\" while other thread holds singleton " + + "lock for other beans " + lockedBeans); + } this.singletonsInLenientCreation.add(beanName); } finally { @@ -302,14 +313,27 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements this.lenientCreationLock.lock(); try { while ((singletonObject = this.singletonObjects.get(beanName)) == null) { + Thread otherThread = this.currentCreationThreads.get(beanName); + if (otherThread != null && (otherThread == currentThread || + checkDependentWaitingThreads(otherThread, currentThread))) { + throw ex; + } if (!this.singletonsInLenientCreation.contains(beanName)) { break; } + if (otherThread != null) { + this.lenientWaitingThreads.put(currentThread, otherThread); + } try { this.lenientCreationFinished.await(); } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); + currentThread.interrupt(); + } + finally { + if (otherThread != null) { + this.lenientWaitingThreads.remove(currentThread); + } } } } @@ -342,7 +366,13 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements // Leniently created singleton object could have appeared in the meantime. singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { - singletonObject = singletonFactory.getObject(); + this.currentCreationThreads.put(beanName, currentThread); + try { + singletonObject = singletonFactory.getObject(); + } + finally { + this.currentCreationThreads.remove(beanName); + } newSingleton = true; } } @@ -391,6 +421,8 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements this.lenientCreationLock.lock(); try { this.singletonsInLenientCreation.remove(beanName); + this.lenientWaitingThreads.entrySet().removeIf( + entry -> entry.getValue() == currentThread); this.lenientCreationFinished.signalAll(); } finally { @@ -399,14 +431,28 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements } } + private boolean checkDependentWaitingThreads(Thread waitingThread, Thread candidateThread) { + Thread threadToCheck = waitingThread; + while ((threadToCheck = this.lenientWaitingThreads.get(threadToCheck)) != null) { + if (threadToCheck == candidateThread) { + return true; + } + } + return false; + } + /** * Determine whether the current thread is allowed to hold the singleton lock. - *

By default, any thread may acquire and hold the singleton lock, except - * background threads from {@link DefaultListableBeanFactory#setBootstrapExecutor}. - * @return {@code false} if the current thread is explicitly not allowed to hold - * the lock, {@code true} if it is explicitly allowed to hold the lock but also - * accepts lenient fallback behavior, or {@code null} if there is no specific - * indication (traditional behavior: always holding a full lock) + *

By default, all threads are forced to hold a full lock through {@code null}. + * {@link DefaultListableBeanFactory} overrides this to specifically handle its + * threads during the pre-instantiation phase: {@code true} for the main thread, + * {@code false} for managed background threads, and configuration-dependent + * behavior for unmanaged threads. + * @return {@code true} if the current thread is explicitly allowed to hold the + * lock but also accepts lenient fallback behavior, {@code false} if it is + * explicitly not allowed to hold the lock and therefore forced to use lenient + * fallback behavior, or {@code null} if there is no specific indication + * (traditional behavior: forced to always hold a full lock) * @since 6.2 */ protected @Nullable Boolean isCurrentThreadAllowedToHoldSingletonLock() { @@ -704,12 +750,19 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements // For an individual destruction, remove the registered instance now. // As of 6.2, this happens after the current bean's destruction step, // allowing for late bean retrieval by on-demand suppliers etc. - this.singletonLock.lock(); - try { + if (this.currentCreationThreads.get(beanName) == Thread.currentThread()) { + // Local remove after failed creation step -> without singleton lock + // since bean creation may have happened leniently without any lock. removeSingleton(beanName); } - finally { - this.singletonLock.unlock(); + else { + this.singletonLock.lock(); + try { + removeSingleton(beanName); + } + finally { + this.singletonLock.unlock(); + } } } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java index da0199f0d1..dcec9ee5c1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.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. @@ -117,7 +117,15 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg */ protected Object getObjectFromFactoryBean(FactoryBean factory, String beanName, boolean shouldPostProcess) { if (factory.isSingleton() && containsSingleton(beanName)) { - this.singletonLock.lock(); + Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock(); + boolean locked; + if (lockFlag == null) { + this.singletonLock.lock(); + locked = true; + } + else { + locked = (lockFlag && this.singletonLock.tryLock()); + } try { Object object = this.factoryBeanObjectCache.get(beanName); if (object == null) { @@ -130,11 +138,13 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg } else { if (shouldPostProcess) { - if (isSingletonCurrentlyInCreation(beanName)) { - // Temporarily return non-post-processed object, not storing it yet - return object; + if (locked) { + if (isSingletonCurrentlyInCreation(beanName)) { + // Temporarily return non-post-processed object, not storing it yet + return object; + } + beforeSingletonCreation(beanName); } - beforeSingletonCreation(beanName); try { object = postProcessObjectFromFactoryBean(object, beanName); } @@ -143,7 +153,9 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg "Post-processing of FactoryBean's singleton object failed", ex); } finally { - afterSingletonCreation(beanName); + if (locked) { + afterSingletonCreation(beanName); + } } } if (containsSingleton(beanName)) { @@ -154,7 +166,9 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg return object; } finally { - this.singletonLock.unlock(); + if (locked) { + this.singletonLock.unlock(); + } } } else { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java index 5f1efdc2d0..5501fe22ca 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.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. @@ -87,7 +87,7 @@ public class MethodOverrides { /** * Return the override for the given method, if any. - * @param method method to check for overrides for + * @param method the method to check for overrides for * @return the method override, or {@code null} if none */ public @Nullable MethodOverride getOverride(Method method) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodReplacer.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodReplacer.java index e4e5df879e..e8d844db7d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodReplacer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodReplacer.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. @@ -35,7 +35,7 @@ public interface MethodReplacer { * @param obj the instance we're reimplementing the method for * @param method the method to reimplement * @param args arguments to the method - * @return return value for the method + * @return the return value for the method */ Object reimplement(Object obj, Method method, Object[] args) throws Throwable; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java index 40a65d4e70..66e122c384 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.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. @@ -75,10 +75,10 @@ import org.springframework.util.StringUtils; * @author Rob Harrop * @since 26.11.2003 * @see DefaultListableBeanFactory - * @deprecated as of 5.3, in favor of Spring's common bean definition formats - * and/or custom reader implementations + * @deprecated in favor of Spring's common bean definition formats and/or + * custom reader implementations */ -@Deprecated +@Deprecated(since = "5.3") public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader { /** @@ -404,10 +404,10 @@ public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader /** * Get all property values, given a prefix (which will be stripped) * and add the bean they define to the factory with the given name. - * @param beanName name of the bean to define + * @param beanName the name of the bean to define * @param map a Map containing string pairs - * @param prefix prefix of each entry, which will be stripped - * @param resourceDescription description of the resource that the + * @param prefix the prefix of each entry, which will be stripped + * @param resourceDescription the description of the resource that the * Map came from (for logging purposes) * @throws BeansException if the bean definition could not be parsed or registered */ diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharacterEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharacterEditor.java index 1458345191..873c694657 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharacterEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharacterEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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.beans.propertyeditors; import java.beans.PropertyEditorSupport; +import java.util.HexFormat; import org.jspecify.annotations.Nullable; @@ -97,13 +98,12 @@ public class CharacterEditor extends PropertyEditorSupport { return (value != null ? value.toString() : ""); } - - private boolean isUnicodeCharacterSequence(String sequence) { + private static boolean isUnicodeCharacterSequence(String sequence) { return (sequence.startsWith(UNICODE_PREFIX) && sequence.length() == UNICODE_LENGTH); } private void setAsUnicode(String text) { - int code = Integer.parseInt(text.substring(UNICODE_PREFIX.length()), 16); + int code = HexFormat.fromHexDigits(text, UNICODE_PREFIX.length(), text.length()); setValue((char) code); } diff --git a/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanRegistrarDsl.kt b/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanRegistrarDsl.kt index 8daf689cf4..cb58909459 100644 --- a/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanRegistrarDsl.kt +++ b/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanRegistrarDsl.kt @@ -111,8 +111,8 @@ open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): Bean } /** - * Register a bean from the given bean class, which will be instantiated - * using the related [resolvable constructor] + * Register a bean of type [T] which will be instantiated using the + * related [resolvable constructor] * [org.springframework.beans.BeanUtils.getResolvableConstructor] if any. * @param T the bean type * @param name the name of the bean @@ -177,8 +177,8 @@ open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): Bean } /** - * Register a bean from the given bean class, which will be instantiated - * using the related [resolvable constructor] + * Register a bean of type [T] which will be instantiated using the + * related [resolvable constructor] * [org.springframework.beans.BeanUtils.getResolvableConstructor] * if any. * @param T the bean type @@ -243,8 +243,8 @@ open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): Bean } /** - * Register a bean from the given bean class, which will be instantiated - * using the provided [supplier]. + * Register a bean of type [T] which will be instantiated using the + * provided [supplier]. * @param T the bean type * @param name the name of the bean * @param autowirable set whether this bean is a candidate for getting @@ -302,7 +302,7 @@ open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): Bean it.prototype() } it.supplier { - SupplierContextDsl(it).supplier() + SupplierContextDsl(it, env).supplier() } val resolvableType = ResolvableType.forType(object: ParameterizedTypeReference() {}); if (resolvableType.hasGenerics()) { @@ -323,8 +323,8 @@ open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): Bean prototype: Boolean = false, crossinline supplier: (SupplierContextDsl.() -> T)): String { /** - * Register a bean from the given bean class, which will be instantiated - * using the provided [supplier]. + * Register a bean of type [T] which will be instantiated using the + * provided [supplier]. * @param T the bean type * @param autowirable set whether this bean is a candidate for getting * autowired into some other bean @@ -370,7 +370,7 @@ open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): Bean it.prototype() } it.supplier { - SupplierContextDsl(it).supplier() + SupplierContextDsl(it, env).supplier() } val resolvableType = ResolvableType.forType(object: ParameterizedTypeReference() {}); if (resolvableType.hasGenerics()) { @@ -380,13 +380,701 @@ open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): Bean return registry.registerBean(T::class.java, customizer) } + // Function with 0 parameter + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f]. + * @param T the bean type + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: () -> T, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke() + } + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f]. + * @param T the bean type + * @param name the name of the bean + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: () -> T, + name: String, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke() + } + + // Function with 1 parameter + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A) -> T, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean()) + } + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param name the name of the bean + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A) -> T, + name: String, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean()) + } + + // Function with 2 parameters + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A, B) -> T, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean(), bean()) + } + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param name the name of the bean + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A, B) -> T, + name: String, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean(), bean()) + } + + // Function with 3 parameters + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A, B, C) -> T, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean(), bean(), bean()) + } + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param name the name of the bean + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A, B, C) -> T, + name: String, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean(), bean(), bean()) + } + + // Function with 4 parameters + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A, B, C, D) -> T, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean(), bean(), bean(), bean()) + } + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param name the name of the bean + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A, B, C, D) -> T, + name: String, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean(), bean(), bean(), bean()) + } + + // Function with 5 parameters + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A, B, C, D, E) -> T, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean(), bean(), bean(), bean(), bean()) + } + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param name the name of the bean + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A, B, C, D, E) -> T, + name: String, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean(), bean(), bean(), bean(), bean()) + } + + // Function with 6 parameters + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A, B, C, D, E, F) -> T, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean(), bean(), bean(), bean(), bean(), bean()) + } + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param name the name of the bean + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A, B, C, D, E, F) -> T, + name: String, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean(), bean(), bean(), bean(), bean(), bean()) + } + + // Function with 7 parameters + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A, B, C, D, E, F, G) -> T, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean(), bean(), bean(), bean(), bean(), bean(), bean()) + } + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param name the name of the bean + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A, B, C, D, E, F, G) -> T, + name: String, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean(), bean(), bean(), bean(), bean(), bean(), bean()) + } + + // Function with 8 parameters + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A, B, C, D, E, F, G, H) -> T, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean(), bean(), bean(), bean(), bean(), bean(), bean(), bean()) + } + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param name the name of the bean + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A, B, C, D, E, F, G, H) -> T, + name: String, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean(), bean(), bean(), bean(), bean(), bean(), bean(), bean()) + } + + // Function with 9 parameters + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A, B, C, D, E, F, G, H, I) -> T, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean(), bean(), bean(), bean(), bean(), bean(), bean(), bean(), bean()) + } + + /** + * Register a bean of type [T] which will be instantiated by invoking the + * provided [function][f] with its parameters autowired by type. + * @param T the bean type + * @param name the name of the bean + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean( + crossinline f: (A, B, C, D, E, F, G, H, I) -> T, + name: String, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) = + registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { + f.invoke(bean(), bean(), bean(), bean(), bean(), bean(), bean(), bean(), bean()) + } + /** * Context available from the bean instance supplier designed to give access * to bean dependencies. */ @BeanRegistrarDslMarker - open class SupplierContextDsl(@PublishedApi internal val context: SupplierContext) { + open class SupplierContextDsl(@PublishedApi internal val context: SupplierContext, val env: Environment) { /** * Return the bean instance that uniquely matches the given object type, 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 4f8e0d9abf..1f6b9f2414 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 @@ -77,7 +77,6 @@ import org.springframework.beans.testfixture.beans.NestedTestBean; import org.springframework.beans.testfixture.beans.SideEffectBean; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.beans.testfixture.beans.factory.DummyFactory; -import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; @@ -263,6 +262,32 @@ class DefaultListableBeanFactoryTests { assertThat(DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isFalse(); } + @Test + void nonInitializedFactoryBeanIgnoredByEagerTypeMatching() { + RootBeanDefinition bd = new RootBeanDefinition(DummyFactory.class); + bd.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, String.class); + lbf.registerBeanDefinition("x1", bd); + + assertBeanNamesForType(TestBean.class, false, true); + assertThat(lbf.getBeanNamesForAnnotation(SuppressWarnings.class)).isEmpty(); + + assertThat(lbf.containsSingleton("x1")).isFalse(); + assertThat(lbf.containsBean("x1")).isTrue(); + assertThat(lbf.containsBean("&x1")).isTrue(); + assertThat(lbf.isSingleton("x1")).isTrue(); + assertThat(lbf.isSingleton("&x1")).isTrue(); + assertThat(lbf.isPrototype("x1")).isFalse(); + assertThat(lbf.isPrototype("&x1")).isFalse(); + assertThat(lbf.isTypeMatch("x1", TestBean.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", TestBean.class)).isFalse(); + assertThat(lbf.isTypeMatch("&x1", DummyFactory.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClass(DummyFactory.class))).isTrue(); + assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClassWithGenerics(FactoryBean.class, Object.class))).isTrue(); + assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClassWithGenerics(FactoryBean.class, String.class))).isFalse(); + assertThat(lbf.getType("x1")).isEqualTo(TestBean.class); + assertThat(lbf.getType("&x1")).isEqualTo(DummyFactory.class); + } + @Test void initializedFactoryBeanFoundByNonEagerTypeMatching() { Properties p = new Properties(); @@ -1393,7 +1418,6 @@ class DefaultListableBeanFactoryTests { lbf.registerBeanDefinition("rod", bd); RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); lbf.registerBeanDefinition("rod2", bd2); - lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); assertThatExceptionOfType(UnsatisfiedDependencyException.class) .isThrownBy(() -> lbf.autowire(ConstructorDependency.class, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false)) @@ -1464,7 +1488,6 @@ class DefaultListableBeanFactoryTests { RootBeanDefinition bd = new RootBeanDefinition(ConstructorDependenciesBean.class); bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); lbf.registerBeanDefinition("bean", bd); - lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); ConstructorDependenciesBean bean = lbf.getBean(ConstructorDependenciesBean.class); Object spouse1 = lbf.getBean("spouse1"); @@ -1482,7 +1505,6 @@ class DefaultListableBeanFactoryTests { bd.setAttribute(GenericBeanDefinition.PREFERRED_CONSTRUCTORS_ATTRIBUTE, ConstructorDependenciesBean.class.getConstructors()); lbf.registerBeanDefinition("bean", bd); - lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); ConstructorDependenciesBean bean = lbf.getBean(ConstructorDependenciesBean.class); Object spouse1 = lbf.getBean("spouse1"); @@ -1500,7 +1522,6 @@ class DefaultListableBeanFactoryTests { bd.setAttribute(GenericBeanDefinition.PREFERRED_CONSTRUCTORS_ATTRIBUTE, ConstructorDependenciesBean.class.getConstructor(TestBean.class)); lbf.registerBeanDefinition("bean", bd); - lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); ConstructorDependenciesBean bean = lbf.getBean(ConstructorDependenciesBean.class); Object spouse = lbf.getBean("spouse1"); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanLookupTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanLookupTests.java index a8546f3748..9e8ba7b709 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanLookupTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanLookupTests.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. @@ -33,13 +33,14 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Chris Beams */ class FactoryBeanLookupTests { - private BeanFactory beanFactory; + + private final BeanFactory beanFactory = new DefaultListableBeanFactory(); + @BeforeEach void setUp() { - beanFactory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader((BeanDefinitionRegistry) beanFactory).loadBeanDefinitions( - new ClassPathResource("FactoryBeanLookupTests-context.xml", this.getClass())); + new ClassPathResource("FactoryBeanLookupTests-context.xml", getClass())); } @Test @@ -71,6 +72,7 @@ class FactoryBeanLookupTests { Foo foo = beanFactory.getBean("fooFactory", Foo.class); assertThat(foo).isNotNull(); } + } class FooFactoryBean extends AbstractFactoryBean { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java index e88e982c62..03c9e1139a 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java @@ -960,6 +960,33 @@ class AutowiredAnnotationBeanPostProcessorTests { @Test void constructorResourceInjectionWithNoCandidatesAndNoFallback() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorWithoutFallbackBean.class)); + + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> bf.getBean("annotatedBean")) + .satisfies(methodParameterDeclaredOn(ConstructorWithoutFallbackBean.class)); + } + + @Test + void constructorResourceInjectionWithCandidateAndNoFallback() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorWithoutFallbackBean.class)); + RootBeanDefinition tb = new RootBeanDefinition(NullFactoryMethods.class); + tb.setFactoryMethodName("createTestBean"); + bf.registerBeanDefinition("testBean", tb); + + bf.getBean("testBean"); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> bf.getBean("annotatedBean")) + .satisfies(methodParameterDeclaredOn(ConstructorWithoutFallbackBean.class)); + } + + @Test + void constructorResourceInjectionWithNameMatchingCandidateAndNoFallback() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorWithoutFallbackBean.class)); + RootBeanDefinition tb = new RootBeanDefinition(NullFactoryMethods.class); + tb.setFactoryMethodName("createTestBean"); + bf.registerBeanDefinition("testBean3", tb); + + bf.getBean("testBean3"); assertThatExceptionOfType(UnsatisfiedDependencyException.class) .isThrownBy(() -> bf.getBean("annotatedBean")) .satisfies(methodParameterDeclaredOn(ConstructorWithoutFallbackBean.class)); @@ -1193,6 +1220,7 @@ class AutowiredAnnotationBeanPostProcessorTests { @Test void singleConstructorInjectionWithMissingDependency() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SingleConstructorOptionalCollectionBean.class)); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) .isThrownBy(() -> bf.getBean("annotatedBean")); } @@ -1203,6 +1231,7 @@ class AutowiredAnnotationBeanPostProcessorTests { RootBeanDefinition tb = new RootBeanDefinition(NullFactoryMethods.class); tb.setFactoryMethodName("createTestBean"); bf.registerBeanDefinition("testBean", tb); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) .isThrownBy(() -> bf.getBean("annotatedBean")); } @@ -1727,6 +1756,40 @@ class AutowiredAnnotationBeanPostProcessorTests { tb2.setFactoryMethodName("newTestBean2"); tb2.setLazyInit(true); bf.registerBeanDefinition("testBean2", tb2); + bf.registerAlias("testBean2", "testBean"); + + ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class); + TestBean testBean1 = bf.getBean("testBean1", TestBean.class); + assertThat(bean.getTestBean()).isSameAs(testBean1); + assertThat(bean.getOptionalTestBean()).isSameAs(testBean1); + assertThat(bean.consumeOptionalTestBean()).isSameAs(testBean1); + assertThat(bean.getUniqueTestBean()).isSameAs(testBean1); + assertThat(bean.consumeUniqueTestBean()).isSameAs(testBean1); + assertThat(bf.containsSingleton("testBean2")).isFalse(); + + TestBean testBean2 = bf.getBean("testBean2", TestBean.class); + assertThat(bean.iterateTestBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.forEachTestBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.streamTestBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.streamTestBeansInOrder()).containsExactly(testBean2, testBean1); + assertThat(bean.allTestBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.allTestBeansInOrder()).containsExactly(testBean2, testBean1); + assertThat(bean.allSingletonBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.allSingletonBeansInOrder()).containsExactly(testBean2, testBean1); + } + + @Test + void objectProviderInjectionWithLateMarkedTargetPrimary() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectProviderInjectionBean.class)); + RootBeanDefinition tb1 = new RootBeanDefinition(TestBeanFactory.class); + tb1.setFactoryMethodName("newTestBean1"); + bf.registerBeanDefinition("testBean1", tb1); + RootBeanDefinition tb2 = new RootBeanDefinition(TestBeanFactory.class); + tb2.setFactoryMethodName("newTestBean2"); + tb2.setLazyInit(true); + bf.registerBeanDefinition("testBean2", tb2); + bf.registerAlias("testBean2", "testBean"); + tb1.setPrimary(true); ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class); TestBean testBean1 = bf.getBean("testBean1", TestBean.class); @@ -2394,7 +2457,7 @@ class AutowiredAnnotationBeanPostProcessorTests { } @Test - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "rawtypes" }) void genericsBasedConstructorInjectionWithNonTypedTarget() { RootBeanDefinition bd = new RootBeanDefinition(RepositoryConstructorInjectionBean.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); @@ -2674,22 +2737,22 @@ class AutowiredAnnotationBeanPostProcessorTests { public static class ResourceInjectionBean { @Autowired(required = false) - private TestBean testBean; + private @Nullable TestBean testBean; - TestBean testBean2; + @Nullable TestBean testBean2; @Autowired - public void setTestBean2(TestBean testBean2) { + public void setTestBean2(@Nullable TestBean testBean2) { Assert.state(this.testBean != null, "Wrong initialization order"); Assert.state(this.testBean2 == null, "Already called"); this.testBean2 = testBean2; } - public TestBean getTestBean() { + public @Nullable TestBean getTestBean() { return this.testBean; } - public TestBean getTestBean2() { + public @Nullable TestBean getTestBean2() { return this.testBean2; } } @@ -2698,13 +2761,13 @@ class AutowiredAnnotationBeanPostProcessorTests { static class NonPublicResourceInjectionBean extends ResourceInjectionBean { @Autowired - public final ITestBean testBean3 = null; + public final @Nullable ITestBean testBean3 = null; - private T nestedTestBean; + private @Nullable T nestedTestBean; - private ITestBean testBean4; + private @Nullable ITestBean testBean4; - protected BeanFactory beanFactory; + protected @Nullable BeanFactory beanFactory; public boolean baseInjected = false; @@ -2713,18 +2776,18 @@ class AutowiredAnnotationBeanPostProcessorTests { @Override @Autowired - public void setTestBean2(TestBean testBean2) { + public void setTestBean2(@Nullable TestBean testBean2) { this.testBean2 = testBean2; } @Autowired - private void inject(ITestBean testBean4, T nestedTestBean) { + private void inject(@Nullable ITestBean testBean4, @Nullable T nestedTestBean) { this.testBean4 = testBean4; this.nestedTestBean = nestedTestBean; } @Autowired - private void inject(ITestBean testBean4) { + private void inject(@Nullable ITestBean testBean4) { this.baseInjected = true; } @@ -2734,11 +2797,11 @@ class AutowiredAnnotationBeanPostProcessorTests { this.beanFactory = beanFactory; } - public ITestBean getTestBean3() { + public @Nullable ITestBean getTestBean3() { return this.testBean3; } - public ITestBean getTestBean4() { + public @Nullable ITestBean getTestBean4() { return this.testBean4; } @@ -3026,7 +3089,6 @@ class AutowiredAnnotationBeanPostProcessorTests { protected ITestBean testBean3; - @Autowired(required = false) public ConstructorWithoutFallbackBean(ITestBean testBean3) { this.testBean3 = testBean3; } @@ -3041,7 +3103,6 @@ class AutowiredAnnotationBeanPostProcessorTests { protected ITestBean testBean3; - @Autowired(required = false) public ConstructorWithNullableArgument(@Nullable ITestBean testBean3) { this.testBean3 = testBean3; } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/AutowiredArgumentsCodeGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/AutowiredArgumentsCodeGeneratorTests.java index 7f35baca32..e53314ac63 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/AutowiredArgumentsCodeGeneratorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/AutowiredArgumentsCodeGeneratorTests.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. @@ -67,7 +67,7 @@ class AutowiredArgumentsCodeGeneratorTests { AutowiredArgumentsCodeGenerator generator = new AutowiredArgumentsCodeGenerator( Outer.Nested.class, constructor); assertThat(generator.generateCode(constructor.getParameterTypes(), 1)) - .hasToString("args.get(0), args.get(1)"); + .hasToString("args.get(1), args.get(2)"); } @Test 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 0dafc56c1a..d37bd4b6ae 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,9 +18,11 @@ package org.springframework.beans.factory.aot; 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; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -28,7 +30,6 @@ import java.util.List; 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 +55,7 @@ import org.springframework.core.testfixture.aot.generate.value.ExampleClass; 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; @@ -71,9 +72,8 @@ import static org.assertj.core.api.Assertions.assertThat; class BeanDefinitionPropertyValueCodeGeneratorDelegatesTests { private static ValueCodeGenerator createValueCodeGenerator(GeneratedClass generatedClass) { - return ValueCodeGenerator.with(BeanDefinitionPropertyValueCodeGeneratorDelegates.INSTANCES) - .add(ValueCodeGeneratorDelegates.INSTANCES) - .scoped(generatedClass.getMethods()); + return BeanDefinitionPropertyValueCodeGeneratorDelegates.createValueCodeGenerator( + generatedClass.getMethods(), Collections.emptyList()); } private void compile(Object value, BiConsumer result) { @@ -83,14 +83,23 @@ class BeanDefinitionPropertyValueCodeGeneratorDelegatesTests { 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/aot/BeanInstanceSupplierTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java index d90498df77..cae3523900 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.support.AnnotationConsumer; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.springframework.beans.factory.BeanCurrentlyInCreationException; import org.springframework.beans.factory.ObjectProvider; @@ -668,7 +669,7 @@ class BeanInstanceSupplierTests { } @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) { return this.source.provideArguments(context); } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java index 0c4baa84a0..cddfb5dd2b 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.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. @@ -130,11 +130,13 @@ class InstanceSupplierCodeGeneratorTests { @Test void generateWhenHasConstructorWithInnerClassAndParameter() { BeanDefinition beanDefinition = new RootBeanDefinition(EnvironmentAwareComponent.class); + StandardEnvironment environment = new StandardEnvironment(); this.beanFactory.registerSingleton("configuration", new InnerComponentConfiguration()); - this.beanFactory.registerSingleton("environment", new StandardEnvironment()); + this.beanFactory.registerSingleton("environment", environment); compile(beanDefinition, (instanceSupplier, compiled) -> { Object bean = getBean(beanDefinition, instanceSupplier); assertThat(bean).isInstanceOf(EnvironmentAwareComponent.class); + assertThat(bean).hasFieldOrPropertyWithValue("environment", environment); assertThat(compiled.getSourceFile()).contains( "getBeanFactory().getBean(InnerComponentConfiguration.class).new EnvironmentAwareComponent("); }); @@ -158,11 +160,13 @@ class InstanceSupplierCodeGeneratorTests { @Test void generateWhenHasNonPublicConstructorWithInnerClassAndParameter() { BeanDefinition beanDefinition = new RootBeanDefinition(EnvironmentAwareComponentWithoutPublicConstructor.class); + StandardEnvironment environment = new StandardEnvironment(); this.beanFactory.registerSingleton("configuration", new InnerComponentConfiguration()); - this.beanFactory.registerSingleton("environment", new StandardEnvironment()); + this.beanFactory.registerSingleton("environment", environment); compile(beanDefinition, (instanceSupplier, compiled) -> { Object bean = getBean(beanDefinition, instanceSupplier); assertThat(bean).isInstanceOf(EnvironmentAwareComponentWithoutPublicConstructor.class); + assertThat(bean).hasFieldOrPropertyWithValue("environment", environment); assertThat(compiled.getSourceFile()).doesNotContain( "getBeanFactory().getBean(InnerComponentConfiguration.class)"); }); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurerTests.java index 01d387a95a..b80bc5ec92 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurerTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurerTests.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. @@ -44,7 +44,7 @@ import static org.springframework.beans.factory.support.BeanDefinitionReaderUtil * @author Chris Beams * @author Sam Brannen */ -@SuppressWarnings("deprecation") +@SuppressWarnings({"deprecation", "removal"}) class PropertyPlaceholderConfigurerTests { private static final String P1 = "p1"; @@ -84,7 +84,7 @@ class PropertyPlaceholderConfigurerTests { .getBeanDefinition()); PropertyPlaceholderConfigurer pc = new PropertyPlaceholderConfigurer(); - Resource resource = new ClassPathResource("PropertyPlaceholderConfigurerTests.properties", this.getClass()); + Resource resource = new ClassPathResource("PropertyPlaceholderConfigurerTests.properties", getClass()); pc.setLocation(resource); pc.postProcessBeanFactory(bf); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyResourceConfigurerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyResourceConfigurerTests.java index 5c764fa87c..23a878332d 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyResourceConfigurerTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyResourceConfigurerTests.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. @@ -59,7 +59,7 @@ import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifie * @since 02.10.2003 * @see PropertyPlaceholderConfigurerTests */ -@SuppressWarnings("deprecation") +@SuppressWarnings({"deprecation", "removal"}) class PropertyResourceConfigurerTests { static { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategyTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategyTests.java index d5b6ad2dff..c97f88c612 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategyTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategyTests.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. @@ -25,8 +25,8 @@ import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; /** * Tests for {@link CglibSubclassingInstantiationStrategy}. diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/DuplicateBeanIdTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/DuplicateBeanIdTests.java index 8544568823..db16b7062e 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/DuplicateBeanIdTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/DuplicateBeanIdTests.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. @@ -46,7 +46,7 @@ class DuplicateBeanIdTests { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf); assertThatException().as("duplicate ids in same nesting level").isThrownBy(() -> - reader.loadBeanDefinitions(new ClassPathResource("DuplicateBeanIdTests-sameLevel-context.xml", this.getClass()))); + reader.loadBeanDefinitions(new ClassPathResource("DuplicateBeanIdTests-sameLevel-context.xml", getClass()))); } @Test @@ -54,7 +54,7 @@ class DuplicateBeanIdTests { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setAllowBeanDefinitionOverriding(true); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf); - reader.loadBeanDefinitions(new ClassPathResource("DuplicateBeanIdTests-multiLevel-context.xml", this.getClass())); + reader.loadBeanDefinitions(new ClassPathResource("DuplicateBeanIdTests-multiLevel-context.xml", getClass())); TestBean testBean = bf.getBean(TestBean.class); // there should be only one assertThat(testBean.getName()).isEqualTo("nested"); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests.java index 4efa9b5dd3..5474e81d57 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ class NestedBeansElementAttributeRecursionTests { void defaultLazyInit() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( - new ClassPathResource("NestedBeansElementAttributeRecursionTests-lazy-context.xml", this.getClass())); + new ClassPathResource("NestedBeansElementAttributeRecursionTests-lazy-context.xml", getClass())); assertLazyInits(bf); } @@ -47,7 +47,7 @@ class NestedBeansElementAttributeRecursionTests { XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(bf); xmlBeanDefinitionReader.setValidating(false); xmlBeanDefinitionReader.loadBeanDefinitions( - new ClassPathResource("NestedBeansElementAttributeRecursionTests-lazy-context.xml", this.getClass())); + new ClassPathResource("NestedBeansElementAttributeRecursionTests-lazy-context.xml", getClass())); assertLazyInits(bf); } @@ -70,7 +70,7 @@ class NestedBeansElementAttributeRecursionTests { void defaultMerge() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( - new ClassPathResource("NestedBeansElementAttributeRecursionTests-merge-context.xml", this.getClass())); + new ClassPathResource("NestedBeansElementAttributeRecursionTests-merge-context.xml", getClass())); assertMerge(bf); } @@ -81,7 +81,7 @@ class NestedBeansElementAttributeRecursionTests { XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(bf); xmlBeanDefinitionReader.setValidating(false); xmlBeanDefinitionReader.loadBeanDefinitions( - new ClassPathResource("NestedBeansElementAttributeRecursionTests-merge-context.xml", this.getClass())); + new ClassPathResource("NestedBeansElementAttributeRecursionTests-merge-context.xml", getClass())); assertMerge(bf); } @@ -109,7 +109,7 @@ class NestedBeansElementAttributeRecursionTests { void defaultAutowireCandidates() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( - new ClassPathResource("NestedBeansElementAttributeRecursionTests-autowire-candidates-context.xml", this.getClass())); + new ClassPathResource("NestedBeansElementAttributeRecursionTests-autowire-candidates-context.xml", getClass())); assertAutowireCandidates(bf); } @@ -120,7 +120,7 @@ class NestedBeansElementAttributeRecursionTests { XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(bf); xmlBeanDefinitionReader.setValidating(false); xmlBeanDefinitionReader.loadBeanDefinitions( - new ClassPathResource("NestedBeansElementAttributeRecursionTests-autowire-candidates-context.xml", this.getClass())); + new ClassPathResource("NestedBeansElementAttributeRecursionTests-autowire-candidates-context.xml", getClass())); assertAutowireCandidates(bf); } @@ -149,7 +149,7 @@ class NestedBeansElementAttributeRecursionTests { void initMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( - new ClassPathResource("NestedBeansElementAttributeRecursionTests-init-destroy-context.xml", this.getClass())); + new ClassPathResource("NestedBeansElementAttributeRecursionTests-init-destroy-context.xml", getClass())); InitDestroyBean beanA = bf.getBean("beanA", InitDestroyBean.class); InitDestroyBean beanB = bf.getBean("beanB", InitDestroyBean.class); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementTests.java index 38e90b3181..03ce5b9bd1 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementTests.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. @@ -26,15 +26,14 @@ import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; - /** * Tests for new nested beans element support in Spring XML * * @author Chris Beams */ class NestedBeansElementTests { - private final Resource XML = - new ClassPathResource("NestedBeansElementTests-context.xml", this.getClass()); + + private final Resource XML = new ClassPathResource("NestedBeansElementTests-context.xml", getClass()); @Test void getBean_withoutActiveProfile() { diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt index 5b8378a495..eb992aa5c7 100644 --- a/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt +++ b/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt @@ -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. @@ -93,7 +93,6 @@ class BeanUtilsKotlinTests { @Test fun `Instantiate value class`() { val constructor = BeanUtils.findPrimaryConstructor(ValueClass::class.java)!! - assertThat(constructor).isNotNull() val value = "Hello value class!" val instance = BeanUtils.instantiateClass(constructor, value) assertThat(instance).isEqualTo(ValueClass(value)) @@ -102,7 +101,6 @@ class BeanUtilsKotlinTests { @Test fun `Instantiate value class with multiple constructors`() { val constructor = BeanUtils.findPrimaryConstructor(ValueClassWithMultipleConstructors::class.java)!! - assertThat(constructor).isNotNull() val value = "Hello value class!" val instance = BeanUtils.instantiateClass(constructor, value) assertThat(instance).isEqualTo(ValueClassWithMultipleConstructors(value)) @@ -111,7 +109,6 @@ class BeanUtilsKotlinTests { @Test fun `Instantiate class with value class parameter`() { val constructor = BeanUtils.findPrimaryConstructor(ConstructorWithValueClass::class.java)!! - assertThat(constructor).isNotNull() val value = ValueClass("Hello value class!") val instance = BeanUtils.instantiateClass(constructor, value) assertThat(instance).isEqualTo(ConstructorWithValueClass(value)) @@ -120,7 +117,6 @@ class BeanUtilsKotlinTests { @Test fun `Instantiate class with nullable value class parameter`() { val constructor = BeanUtils.findPrimaryConstructor(ConstructorWithNullableValueClass::class.java)!! - assertThat(constructor).isNotNull() val value = ValueClass("Hello value class!") var instance = BeanUtils.instantiateClass(constructor, value) assertThat(instance).isEqualTo(ConstructorWithNullableValueClass(value)) @@ -131,7 +127,6 @@ class BeanUtilsKotlinTests { @Test fun `Instantiate primitive value class`() { val constructor = BeanUtils.findPrimaryConstructor(PrimitiveValueClass::class.java)!! - assertThat(constructor).isNotNull() val value = 0 val instance = BeanUtils.instantiateClass(constructor, value) assertThat(instance).isEqualTo(PrimitiveValueClass(value)) @@ -140,7 +135,6 @@ class BeanUtilsKotlinTests { @Test fun `Instantiate class with primitive value class parameter`() { val constructor = BeanUtils.findPrimaryConstructor(ConstructorWithPrimitiveValueClass::class.java)!! - assertThat(constructor).isNotNull() val value = PrimitiveValueClass(0) val instance = BeanUtils.instantiateClass(constructor, value) assertThat(instance).isEqualTo(ConstructorWithPrimitiveValueClass(value)) @@ -149,7 +143,6 @@ class BeanUtilsKotlinTests { @Test fun `Instantiate class with nullable primitive value class parameter`() { val constructor = BeanUtils.findPrimaryConstructor(ConstructorWithNullablePrimitiveValueClass::class.java)!! - assertThat(constructor).isNotNull() val value = PrimitiveValueClass(0) var instance = BeanUtils.instantiateClass(constructor, value) assertThat(instance).isEqualTo(ConstructorWithNullablePrimitiveValueClass(value)) @@ -157,6 +150,48 @@ class BeanUtilsKotlinTests { assertThat(instance).isEqualTo(ConstructorWithNullablePrimitiveValueClass(null)) } + @Test + fun `Get parameter names with Foo`() { + val ctor = BeanUtils.findPrimaryConstructor(Foo::class.java)!! + val names = BeanUtils.getParameterNames(ctor) + assertThat(names).containsExactly("param1", "param2") + } + + @Test + fun `Get parameter names filters out DefaultConstructorMarker with ConstructorWithValueClass`() { + val ctor = BeanUtils.findPrimaryConstructor(ConstructorWithValueClass::class.java)!! + val names = BeanUtils.getParameterNames(ctor) + assertThat(names).containsExactly("value") + } + + @Test + fun `getParameterNames filters out DefaultConstructorMarker with ConstructorWithNullableValueClass`() { + val ctor = BeanUtils.findPrimaryConstructor(ConstructorWithNullableValueClass::class.java)!! + val names = BeanUtils.getParameterNames(ctor) + assertThat(names).containsExactly("value") + } + + @Test + fun `getParameterNames filters out DefaultConstructorMarker with ConstructorWithPrimitiveValueClass`() { + val ctor = BeanUtils.findPrimaryConstructor(ConstructorWithPrimitiveValueClass::class.java)!! + val names = BeanUtils.getParameterNames(ctor) + assertThat(names).containsExactly("value") + } + + @Test + fun `getParameterNames filters out DefaultConstructorMarker with ConstructorWithNullablePrimitiveValueClass`() { + val ctor = BeanUtils.findPrimaryConstructor(ConstructorWithNullablePrimitiveValueClass::class.java)!! + val names = BeanUtils.getParameterNames(ctor) + assertThat(names).containsExactly("value") + } + + @Test + fun `getParameterNames with ClassWithZeroParameterCtor`() { + val ctor = BeanUtils.findPrimaryConstructor(ClassWithZeroParameterCtor::class.java)!! + val names = BeanUtils.getParameterNames(ctor) + assertThat(names).isEmpty() + } + class Foo(val param1: String, val param2: Int) @@ -216,4 +251,6 @@ class BeanUtilsKotlinTests { data class ConstructorWithNullablePrimitiveValueClass(val value: PrimitiveValueClass?) + class ClassWithZeroParameterCtor() + } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/InnerComponentConfiguration.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/InnerComponentConfiguration.java index 6f0b03c83e..042b1fe028 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/InnerComponentConfiguration.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/InnerComponentConfiguration.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. @@ -28,7 +28,10 @@ public class InnerComponentConfiguration { public class EnvironmentAwareComponent { + final Environment environment; + public EnvironmentAwareComponent(Environment environment) { + this.environment = environment; } } @@ -40,7 +43,10 @@ public class InnerComponentConfiguration { public class EnvironmentAwareComponentWithoutPublicConstructor { + final Environment environment; + EnvironmentAwareComponentWithoutPublicConstructor(Environment environment) { + this.environment = environment; } } diff --git a/spring-context/spring-context.gradle b/spring-context/spring-context.gradle index a19b2f314d..5c50407bf9 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/cache/interceptor/AbstractCacheInvoker.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java index d8a3cc5675..5b3fe916e1 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.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. @@ -104,7 +104,7 @@ public abstract class AbstractCacheInvoker { return valueLoader.call(); } catch (Exception ex2) { - throw new RuntimeException(ex2); + throw new Cache.ValueRetrievalException(key, valueLoader, ex); } } } @@ -122,16 +122,12 @@ public abstract class AbstractCacheInvoker { try { return cache.retrieve(key); } - catch (Cache.ValueRetrievalException ex) { - throw ex; - } catch (RuntimeException ex) { getErrorHandler().handleCacheGetError(ex, cache, key); return null; } } - /** * Execute {@link Cache#retrieve(Object, Supplier)} on the specified * {@link Cache} and invoke the error handler if an exception occurs. @@ -144,9 +140,6 @@ public abstract class AbstractCacheInvoker { try { return cache.retrieve(key, valueLoader); } - catch (Cache.ValueRetrievalException ex) { - throw ex; - } catch (RuntimeException ex) { getErrorHandler().handleCacheGetError(ex, cache, key); return valueLoader.get(); diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java index a006a38e1a..876f772797 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java @@ -26,6 +26,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import org.apache.commons.logging.Log; @@ -440,13 +441,40 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker return cacheHit; } + @SuppressWarnings({ "unchecked", "rawtypes" }) private @Nullable Object executeSynchronized(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next(); if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) { Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT); Cache cache = context.getCaches().iterator().next(); if (CompletableFuture.class.isAssignableFrom(method.getReturnType())) { - return doRetrieve(cache, key, () -> (CompletableFuture) invokeOperation(invoker)); + AtomicBoolean invokeFailure = new AtomicBoolean(false); + CompletableFuture result = doRetrieve(cache, key, + () -> { + CompletableFuture invokeResult = ((CompletableFuture) invokeOperation(invoker)); + if (invokeResult == null) { + return null; + } + return invokeResult.exceptionallyCompose(ex -> { + invokeFailure.set(true); + return CompletableFuture.failedFuture(ex); + }); + }); + return result.exceptionallyCompose(ex -> { + if (!(ex instanceof RuntimeException rex)) { + return CompletableFuture.failedFuture(ex); + } + try { + getErrorHandler().handleCacheGetError(rex, cache, key); + if (invokeFailure.get()) { + return CompletableFuture.failedFuture(ex); + } + return (CompletableFuture) invokeOperation(invoker); + } + catch (Throwable ex2) { + return CompletableFuture.failedFuture(ex2); + } + }); } if (this.reactiveCachingHandler != null) { Object returnValue = this.reactiveCachingHandler.executeSynchronized(invoker, method, cache, key); @@ -505,9 +533,17 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker if (CompletableFuture.class.isAssignableFrom(context.getMethod().getReturnType())) { CompletableFuture result = doRetrieve(cache, key); if (result != null) { - return result.exceptionally(ex -> { - getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key); - return null; + return result.exceptionallyCompose(ex -> { + if (!(ex instanceof RuntimeException rex)) { + return CompletableFuture.failedFuture(ex); + } + try { + getErrorHandler().handleCacheGetError(rex, cache, key); + return CompletableFuture.completedFuture(null); + } + catch (Throwable ex2) { + return CompletableFuture.failedFuture(ex2); + } }).thenCompose(value -> (CompletableFuture) evaluate( (value != null ? CompletableFuture.completedFuture(unwrapCacheValue(value)) : null), invoker, method, contexts)); @@ -1075,31 +1111,71 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker private final ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); + @SuppressWarnings({"rawtypes", "unchecked"}) public @Nullable Object executeSynchronized(CacheOperationInvoker invoker, Method method, Cache cache, Object key) { + AtomicBoolean invokeFailure = new AtomicBoolean(false); ReactiveAdapter adapter = this.registry.getAdapter(method.getReturnType()); if (adapter != null) { if (adapter.isMultiValue()) { // Flux or similar return adapter.fromPublisher(Flux.from(Mono.fromFuture( - cache.retrieve(key, - () -> Flux.from(adapter.toPublisher(invokeOperation(invoker))).collectList().toFuture()))) - .flatMap(Flux::fromIterable)); + doRetrieve(cache, key, + () -> Flux.from(adapter.toPublisher(invokeOperation(invoker))).collectList().doOnError(ex -> invokeFailure.set(true)).toFuture()))) + .flatMap(Flux::fromIterable) + .onErrorResume(RuntimeException.class, ex -> { + try { + getErrorHandler().handleCacheGetError(ex, cache, key); + if (invokeFailure.get()) { + return Flux.error(ex); + } + return Flux.from(adapter.toPublisher(invokeOperation(invoker))); + } + catch (RuntimeException exception) { + return Flux.error(exception); + } + })); } else { // Mono or similar return adapter.fromPublisher(Mono.fromFuture( - cache.retrieve(key, - () -> Mono.from(adapter.toPublisher(invokeOperation(invoker))).toFuture()))); + doRetrieve(cache, key, + () -> Mono.from(adapter.toPublisher(invokeOperation(invoker))).doOnError(ex -> invokeFailure.set(true)).toFuture())) + .onErrorResume(RuntimeException.class, ex -> { + try { + getErrorHandler().handleCacheGetError(ex, cache, key); + if (invokeFailure.get()) { + return Mono.error(ex); + } + return Mono.from(adapter.toPublisher(invokeOperation(invoker))); + } + catch (RuntimeException exception) { + return Mono.error(exception); + } + })); } } if (KotlinDetector.isSuspendingFunction(method)) { - return Mono.fromFuture(cache.retrieve(key, () -> { - Mono mono = ((Mono) invokeOperation(invoker)); - if (mono == null) { + return Mono.fromFuture(doRetrieve(cache, key, () -> { + Mono mono = (Mono) invokeOperation(invoker); + if (mono != null) { + mono = mono.doOnError(ex -> invokeFailure.set(true)); + } + else { mono = Mono.empty(); } return mono.toFuture(); - })); + })).onErrorResume(RuntimeException.class, ex -> { + try { + getErrorHandler().handleCacheGetError(ex, cache, key); + if (invokeFailure.get()) { + return Mono.error(ex); + } + return (Mono) invokeOperation(invoker); + } + catch (RuntimeException exception) { + return Mono.error(exception); + } + }); } return NOT_HANDLED; } @@ -1113,7 +1189,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker return NOT_HANDLED; } - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings({"rawtypes", "unchecked"}) public @Nullable Object findInCaches(CacheOperationContext context, Cache cache, Object key, CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { 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 72776fe44a..5e2e827d6e 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 @@ -16,6 +16,7 @@ package org.springframework.context.annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; @@ -28,6 +29,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.BeanRegistrar; import org.springframework.beans.factory.ListableBeanFactory; @@ -290,8 +292,8 @@ class ConfigurationClassBeanDefinitionReader { } if (logger.isTraceEnabled()) { - logger.trace(String.format("Registering bean definition for @Bean method %s.%s()", - configClass.getMetadata().getClassName(), beanName)); + logger.trace("Registering bean definition for @Bean method %s.%s()" + .formatted(configClass.getMetadata().getClassName(), beanName)); } this.registry.registerBeanDefinition(beanName, beanDefToRegister); } @@ -351,9 +353,8 @@ class ConfigurationClassBeanDefinitionReader { "@Bean definition illegally overridden by existing bean definition: " + existingBeanDef); } if (logger.isDebugEnabled()) { - logger.debug(String.format("Skipping bean definition for %s: a definition for bean '%s' " + - "already exists. This top-level bean definition is considered as an override.", - beanMethod, beanName)); + logger.debug("Skipping bean definition for %s: a definition for bean '%s' already exists. " + + "This top-level bean definition is considered as an override.".formatted(beanMethod, beanName)); } return true; } @@ -379,9 +380,11 @@ class ConfigurationClassBeanDefinitionReader { BeanDefinitionReader reader = readerInstanceCache.get(readerClass); if (reader == null) { try { + Constructor constructor = + readerClass.getDeclaredConstructor(BeanDefinitionRegistry.class); // Instantiate the specified BeanDefinitionReader - reader = readerClass.getConstructor(BeanDefinitionRegistry.class).newInstance(this.registry); - // Delegate the current ResourceLoader to it if possible + reader = BeanUtils.instantiateClass(constructor, this.registry); + // Delegate the current ResourceLoader and Environment to it if possible if (reader instanceof AbstractBeanDefinitionReader abdr) { abdr.setResourceLoader(this.resourceLoader); abdr.setEnvironment(this.environment); @@ -390,11 +393,9 @@ class ConfigurationClassBeanDefinitionReader { } catch (Throwable ex) { throw new IllegalStateException( - "Could not instantiate BeanDefinitionReader class [" + readerClass.getName() + "]"); + "Could not instantiate BeanDefinitionReader class [" + readerClass.getName() + "]", ex); } } - - // TODO SPR-6310: qualify relative path locations as done in AbstractContextLoader.modifyLocations reader.loadBeanDefinitions(resource); }); } 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 a00c03d151..beb6fcd7de 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -16,6 +16,7 @@ package org.springframework.context.annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -27,6 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; 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; @@ -115,6 +117,12 @@ class ConfigurationClassEnhancer { 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); @@ -131,6 +139,32 @@ class ConfigurationClassEnhancer { } } + /** + * 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. */ @@ -138,26 +172,22 @@ class ConfigurationClassEnhancer { 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. @@ -339,9 +369,9 @@ class ConfigurationClassEnhancer { // proxy that intercepts calls to getObject() and returns any cached bean instance. // This ensures that the semantics of calling a FactoryBean from within @Bean methods // is the same as that of referring to a FactoryBean within XML. See SPR-6602. - if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX + beanName) && - factoryContainsBean(beanFactory, beanName)) { - Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName); + String factoryBeanName = BeanFactory.FACTORY_BEAN_PREFIX + beanName; + if (factoryContainsBean(beanFactory, factoryBeanName) && factoryContainsBean(beanFactory, beanName)) { + Object factoryBean = beanFactory.getBean(factoryBeanName); if (factoryBean instanceof ScopedProxyFactoryBean) { // Scoped proxy factory beans are a special case and should not be further proxied } @@ -545,7 +575,7 @@ class ConfigurationClassEnhancer { 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 659047e892..1f88fbea9f 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 @@ -98,6 +98,7 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Sam Brannen * @author Stephane Nicoll + * @author Daeho Kwon * @since 3.0 * @see ConfigurationClassBeanDefinitionReader */ @@ -180,8 +181,9 @@ class ConfigurationClassParser { } // Downgrade to lite (no enhancement) in case of no instance-level @Bean methods. - if (!configClass.hasNonStaticBeanMethods() && ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals( - bd.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE))) { + if (!configClass.getMetadata().isAbstract() && !configClass.hasNonStaticBeanMethods() && + ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals( + bd.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE))) { bd.setAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE, ConfigurationClassUtils.CONFIGURATION_CLASS_LITE); } @@ -548,15 +550,22 @@ class ConfigurationClassParser { *

For example, it is common for a {@code @Configuration} class to declare direct * {@code @Import}s in addition to meta-imports originating from an {@code @Enable} * annotation. + *

As of Spring Framework 7.0, {@code @Import} annotations declared on interfaces + * implemented by the configuration class are also considered. This allows imports to + * be triggered indirectly via marker interfaces or shared base interfaces. * @param sourceClass the class to search * @param imports the imports collected so far - * @param visited used to track visited classes to prevent infinite recursion + * @param visited used to track visited classes and interfaces to prevent infinite + * recursion * @throws IOException if there is any problem reading metadata from the named class */ private void collectImports(SourceClass sourceClass, Set imports, Set visited) throws IOException { if (visited.add(sourceClass)) { + for (SourceClass ifc : sourceClass.getInterfaces()) { + collectImports(ifc, imports, visited); + } for (SourceClass annotation : sourceClass.getAnnotations()) { String annName = annotation.getMetadata().getClassName(); if (!annName.equals(Import.class.getName())) { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Import.java b/spring-context/src/main/java/org/springframework/context/annotation/Import.java index c1275801fc..7d26dc6f8b 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Import.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Import.java @@ -29,9 +29,11 @@ import org.springframework.beans.factory.BeanRegistrar; * {@link Configuration @Configuration} classes. * *

Provides functionality equivalent to the {@code } element in Spring XML. - * Allows for importing {@code @Configuration} classes, {@link ImportSelector} and - * {@link ImportBeanDefinitionRegistrar} implementations, as well as regular component - * classes (as of 4.2; analogous to {@link AnnotationConfigApplicationContext#register}). + * + *

Allows for importing {@code @Configuration} classes, {@link ImportSelector}, + * {@link ImportBeanDefinitionRegistrar}, and {@link BeanRegistrar} implementations, + * as well as regular component classes (analogous to + * {@link AnnotationConfigApplicationContext#register}). * *

{@code @Bean} definitions declared in imported {@code @Configuration} classes should be * accessed by using {@link org.springframework.beans.factory.annotation.Autowired @Autowired} @@ -39,7 +41,17 @@ import org.springframework.beans.factory.BeanRegistrar; * declaring the bean can be autowired. The latter approach allows for explicit, IDE-friendly * navigation between {@code @Configuration} class methods. * - *

May be declared at the class level or as a meta-annotation. + *

May be declared directly at the class level or as a meta-annotation. + * {@code @Import} annotations declared directly at the class level are processed + * after {@code @Import} annotations declared as meta-annotations, which allows + * directly declared imports to override beans registered via {@code @Import} + * meta-annotations. + * + *

As of Spring Framework 7.0, {@code @Import} annotations declared on interfaces + * implemented by {@code @Configuration} classes are also supported. Locally declared + * {@code @Import} annotations are processed after {@code @Import} annotations on + * interfaces, which allows local imports to override beans registered via + * {@code @Import} annotations inherited from interfaces. * *

If XML or other non-{@code @Configuration} bean definition resources need to be * imported, use the {@link ImportResource @ImportResource} annotation instead. @@ -59,7 +71,7 @@ public @interface Import { /** * {@link Configuration @Configuration}, {@link ImportSelector}, - * {@link ImportBeanDefinitionRegistrar}, {@link BeanRegistrar} or regular + * {@link ImportBeanDefinitionRegistrar}, {@link BeanRegistrar}, or regular * component classes to import. */ Class[] value(); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportResource.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportResource.java index da1cb97b8e..dbb1c6047d 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ImportResource.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 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,14 +29,15 @@ import org.springframework.core.annotation.AliasFor; * Indicates one or more resources containing bean definitions to import. * *

Like {@link Import @Import}, this annotation provides functionality similar to - * the {@code } element in Spring XML. It is typically used when designing - * {@link Configuration @Configuration} classes to be bootstrapped by an - * {@link AnnotationConfigApplicationContext}, but where some XML functionality such - * as namespaces is still necessary. + * the {@code } element in Spring XML configuration. It is typically used + * when designing {@link Configuration @Configuration} classes to be bootstrapped by + * an {@link AnnotationConfigApplicationContext}, but where some XML functionality + * such as namespaces is still necessary. * - *

By default, arguments to the {@link #value} attribute will be processed using a + *

By default, arguments to the {@link #locations() locations} or {@link #value() value} + * attribute will be processed using a * {@link org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader GroovyBeanDefinitionReader} - * if ending in {@code ".groovy"}; otherwise, an + * for resource locations ending in {@code ".groovy"}; otherwise, an * {@link org.springframework.beans.factory.xml.XmlBeanDefinitionReader XmlBeanDefinitionReader} * will be used to parse Spring {@code } XML files. Optionally, the {@link #reader} * attribute may be declared, allowing the user to choose a custom {@link BeanDefinitionReader} @@ -77,12 +78,19 @@ public @interface ImportResource { /** * {@link BeanDefinitionReader} implementation to use when processing - * resources specified via the {@link #value} attribute. + * resources specified via the {@link #locations() locations} or + * {@link #value() value} attribute. + *

The configured {@code BeanDefinitionReader} type must declare a + * constructor that accepts a single + * {@link org.springframework.beans.factory.support.BeanDefinitionRegistry + * BeanDefinitionRegistry} argument. *

By default, the reader will be adapted to the resource path specified: * {@code ".groovy"} files will be processed with a - * {@link org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader GroovyBeanDefinitionReader}; - * whereas, all other resources will be processed with an - * {@link org.springframework.beans.factory.xml.XmlBeanDefinitionReader XmlBeanDefinitionReader}. + * {@link org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader + * GroovyBeanDefinitionReader}; whereas, all other resources will be processed + * with an {@link org.springframework.beans.factory.xml.XmlBeanDefinitionReader + * XmlBeanDefinitionReader}. + * @see #locations * @see #value */ Class reader() default BeanDefinitionReader.class; diff --git a/spring-context/src/main/java/org/springframework/context/aot/AbstractAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/AbstractAotProcessor.java index d7cce932a3..2d60fd612d 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/AbstractAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/aot/AbstractAotProcessor.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.context.aot; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Path; import org.jspecify.annotations.Nullable; @@ -103,7 +104,7 @@ public abstract class AbstractAotProcessor { FileSystemUtils.deleteRecursively(path); } catch (IOException ex) { - throw new RuntimeException("Failed to delete existing output in '" + path + "'"); + throw new UncheckedIOException("Failed to delete existing output in '" + path + "'", ex); } } } diff --git a/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextAotGenerator.java b/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextAotGenerator.java index 0331b145c1..eb5c736faf 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextAotGenerator.java +++ b/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextAotGenerator.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. @@ -50,6 +50,7 @@ public class ApplicationContextAotGenerator { */ public ClassName processAheadOfTime(GenericApplicationContext applicationContext, GenerationContext generationContext) { + return withCglibClassHandler(new CglibClassHandler(generationContext), () -> { applicationContext.refreshForAotProcessing(generationContext.getRuntimeHints()); ApplicationContextInitializationCodeGenerator codeGenerator = diff --git a/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java index 16fbee9bb3..746e23cfd6 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.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. @@ -80,8 +80,9 @@ public abstract class ContextAotProcessor extends AbstractAotProcessor[] classes; @@ -113,9 +114,9 @@ public class ReflectiveProcessorAotContributionBuilder { RuntimeHints runtimeHints = generationContext.getRuntimeHints(); registrar.registerRuntimeHints(runtimeHints, this.classes); } - } + private static class ReflectiveClassPathScanner extends ClassPathScanningCandidateComponentProvider { private final @Nullable ClassLoader classLoader; diff --git a/spring-context/src/main/java/org/springframework/context/config/PropertyPlaceholderBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/config/PropertyPlaceholderBeanDefinitionParser.java index 72504ae805..277ee04402 100644 --- a/spring-context/src/main/java/org/springframework/context/config/PropertyPlaceholderBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/context/config/PropertyPlaceholderBeanDefinitionParser.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. @@ -39,7 +39,7 @@ class PropertyPlaceholderBeanDefinitionParser extends AbstractPropertyLoadingBea @Override - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "removal"}) protected Class getBeanClass(Element element) { // The default value of system-properties-mode is 'ENVIRONMENT'. This value // indicates that resolution of placeholders against system properties is a diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java index 45dafd462d..310576f4cc 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.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. @@ -156,9 +156,9 @@ public abstract class AbstractResourceBasedMessageSource extends AbstractMessage * Return whether to fall back to the system Locale if no files for a specific * Locale have been found. * @since 4.3 - * @deprecated as of 5.2.2, in favor of {@link #getDefaultLocale()} + * @deprecated in favor of {@link #getDefaultLocale()} */ - @Deprecated + @Deprecated(since = "5.2.2") protected boolean isFallbackToSystemLocale() { return this.fallbackToSystemLocale; } 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 73e1b73507..fca08a5113 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. @@ -26,9 +26,12 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; @@ -52,6 +55,7 @@ import org.springframework.core.NativeDetector; import org.springframework.core.SpringProperties; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; /** * Spring's default implementation of the {@link LifecycleProcessor} strategy. @@ -61,12 +65,23 @@ import org.springframework.util.ClassUtils; * 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. + * + *

As of 6.2, this processor can be configured with custom timeouts for specific + * shutdown phases, applied to {@link SmartLifecycle#stop(Runnable)} implementations. + * As of 6.2.6, there is also support for the concurrent startup of specific phases + * with individual timeouts, triggering the {@link SmartLifecycle#start()} callbacks + * of all associated beans asynchronously and then waiting for all of them to return, + * as an alternative to the default sequential startup of beans without a timeout. * * @author Mark Fisher * @author Juergen Hoeller * @author Sebastien Deleuze * @since 3.0 + * @see SmartLifecycle#getPhase() + * @see #setConcurrentStartupForPhase + * @see #setTimeoutForShutdownPhase */ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactoryAware { @@ -102,6 +117,8 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor private final Log logger = LogFactory.getLog(getClass()); + private final Map concurrentStartupForPhases = new ConcurrentHashMap<>(); + private final Map timeoutsForShutdownPhases = new ConcurrentHashMap<>(); private volatile long timeoutPerShutdownPhase = 10000; @@ -127,20 +144,59 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor } + /** + * Switch to concurrent startup for each given phase (group of {@link SmartLifecycle} + * beans with the same 'phase' value) with corresponding timeouts. + *

Note: By default, the startup for every phase will be sequential without + * a timeout. Calling this setter with timeouts for the given phases switches to a + * mode where the beans in these phases will be started concurrently, cancelling + * the startup if the corresponding timeout is not met for any of these phases. + *

For an actual concurrent startup, a bootstrap {@code Executor} needs to be + * set for the application context, typically through a "bootstrapExecutor" bean. + * @param phasesWithTimeouts a map of phase values (matching + * {@link SmartLifecycle#getPhase()}) and corresponding timeout values + * (in milliseconds) + * @since 6.2.6 + * @see SmartLifecycle#getPhase() + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBootstrapExecutor() + */ + public void setConcurrentStartupForPhases(Map phasesWithTimeouts) { + this.concurrentStartupForPhases.putAll(phasesWithTimeouts); + } + + /** + * Switch to concurrent startup for a specific phase (group of {@link SmartLifecycle} + * beans with the same 'phase' value) with a corresponding timeout. + *

Note: By default, the startup for every phase will be sequential without + * a timeout. Calling this setter with a timeout for the given phase switches to a + * mode where the beans in this phase will be started concurrently, cancelling + * the startup if the corresponding timeout is not met for this phase. + *

For an actual concurrent startup, a bootstrap {@code Executor} needs to be + * set for the application context, typically through a "bootstrapExecutor" bean. + * @param phase the phase value (matching {@link SmartLifecycle#getPhase()}) + * @param timeout the corresponding timeout value (in milliseconds) + * @since 6.2.6 + * @see SmartLifecycle#getPhase() + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBootstrapExecutor() + */ + public void setConcurrentStartupForPhase(int phase, long timeout) { + this.concurrentStartupForPhases.put(phase, timeout); + } + /** * Specify the maximum time allotted for the shutdown of each given phase * (group of {@link SmartLifecycle} beans with the same 'phase' value). *

In case of no specific timeout configured, the default timeout per * shutdown phase will apply: 10000 milliseconds (10 seconds) as of 6.2. - * @param timeoutsForShutdownPhases a map of phase values (matching + * @param phasesWithTimeouts a map of phase values (matching * {@link SmartLifecycle#getPhase()}) and corresponding timeout values * (in milliseconds) * @since 6.2 * @see SmartLifecycle#getPhase() * @see #setTimeoutPerShutdownPhase */ - public void setTimeoutsForShutdownPhases(Map timeoutsForShutdownPhases) { - this.timeoutsForShutdownPhases.putAll(timeoutsForShutdownPhases); + public void setTimeoutsForShutdownPhases(Map phasesWithTimeouts) { + this.timeoutsForShutdownPhases.putAll(phasesWithTimeouts); } /** @@ -168,17 +224,15 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor this.timeoutPerShutdownPhase = timeoutPerShutdownPhase; } - private long determineTimeout(int phase) { - Long timeout = this.timeoutsForShutdownPhases.get(phase); - return (timeout != null ? timeout : this.timeoutPerShutdownPhase); - } - @Override public void setBeanFactory(BeanFactory beanFactory) { if (!(beanFactory instanceof ConfigurableListableBeanFactory clbf)) { throw new IllegalArgumentException( "DefaultLifecycleProcessor requires a ConfigurableListableBeanFactory: " + beanFactory); } + if (!this.concurrentStartupForPhases.isEmpty() && clbf.getBootstrapExecutor() == null) { + throw new IllegalStateException("'bootstrapExecutor' needs to be configured for concurrent startup"); + } this.beanFactory = clbf; } @@ -188,6 +242,21 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor return beanFactory; } + private Executor getBootstrapExecutor() { + Executor executor = getBeanFactory().getBootstrapExecutor(); + Assert.state(executor != null, "No 'bootstrapExecutor' available"); + return executor; + } + + private @Nullable Long determineConcurrentStartup(int phase) { + return this.concurrentStartupForPhases.get(phase); + } + + private long determineShutdownTimeout(int phase) { + Long timeout = this.timeoutsForShutdownPhases.get(phase); + return (timeout != null ? timeout : this.timeoutPerShutdownPhase); + } + // Lifecycle implementation @@ -282,9 +351,8 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor lifecycleBeans.forEach((beanName, bean) -> { if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) { int startupPhase = getPhase(bean); - phases.computeIfAbsent(startupPhase, - phase -> new LifecycleGroup(phase, determineTimeout(phase), lifecycleBeans, autoStartupOnly) - ).add(beanName, bean); + phases.computeIfAbsent(startupPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, autoStartupOnly)) + .add(beanName, bean); } }); @@ -305,30 +373,41 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor * @param lifecycleBeans a Map with bean name as key and Lifecycle instance as value * @param beanName the name of the bean to start */ - private void doStart(Map lifecycleBeans, String beanName, boolean autoStartupOnly) { + private void doStart(Map lifecycleBeans, String beanName, + boolean autoStartupOnly, @Nullable List> futures) { + Lifecycle bean = lifecycleBeans.remove(beanName); if (bean != null && bean != this) { String[] dependenciesForBean = getBeanFactory().getDependenciesForBean(beanName); for (String dependency : dependenciesForBean) { - doStart(lifecycleBeans, dependency, autoStartupOnly); + doStart(lifecycleBeans, dependency, autoStartupOnly, futures); } if (!bean.isRunning() && (!autoStartupOnly || toBeStarted(beanName, bean))) { - if (logger.isTraceEnabled()) { - logger.trace("Starting bean '" + beanName + "' of type [" + bean.getClass().getName() + "]"); + if (futures != null) { + futures.add(CompletableFuture.runAsync(() -> doStart(beanName, bean), getBootstrapExecutor())); } - try { - bean.start(); - } - catch (Throwable ex) { - throw new ApplicationContextException("Failed to start bean '" + beanName + "'", ex); - } - if (logger.isDebugEnabled()) { - logger.debug("Successfully started bean '" + beanName + "'"); + else { + doStart(beanName, bean); } } } } + private void doStart(String beanName, Lifecycle bean) { + if (logger.isTraceEnabled()) { + logger.trace("Starting bean '" + beanName + "' of type [" + bean.getClass().getName() + "]"); + } + try { + bean.start(); + } + catch (Throwable ex) { + throw new ApplicationContextException("Failed to start bean '" + beanName + "'", ex); + } + if (logger.isDebugEnabled()) { + logger.debug("Successfully started bean '" + beanName + "'"); + } + } + private boolean toBeStarted(String beanName, Lifecycle bean) { Set stoppedBeans = this.stoppedBeans; return (stoppedBeans != null ? stoppedBeans.contains(beanName) : @@ -341,9 +420,8 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor lifecycleBeans.forEach((beanName, bean) -> { int shutdownPhase = getPhase(bean); - phases.computeIfAbsent(shutdownPhase, - phase -> new LifecycleGroup(phase, determineTimeout(phase), lifecycleBeans, false) - ).add(beanName, bean); + phases.computeIfAbsent(shutdownPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, false)) + .add(beanName, bean); }); if (!phases.isEmpty()) { @@ -414,7 +492,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor } - // overridable hooks + // Overridable hooks /** * Retrieve all applicable Lifecycle beans: all singletons that have already been created, @@ -470,8 +548,6 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor private final int phase; - private final long timeout; - private final Map lifecycleBeans; private final boolean autoStartupOnly; @@ -480,11 +556,8 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor private int smartMemberCount; - public LifecycleGroup( - int phase, long timeout, Map lifecycleBeans, boolean autoStartupOnly) { - + public LifecycleGroup(int phase, Map lifecycleBeans, boolean autoStartupOnly) { this.phase = phase; - this.timeout = timeout; this.lifecycleBeans = lifecycleBeans; this.autoStartupOnly = autoStartupOnly; } @@ -503,8 +576,26 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor if (logger.isDebugEnabled()) { logger.debug("Starting beans in phase " + this.phase); } + Long concurrentStartup = determineConcurrentStartup(this.phase); + List> futures = (concurrentStartup != null ? new ArrayList<>() : null); for (LifecycleGroupMember member : this.members) { - doStart(this.lifecycleBeans, member.name, this.autoStartupOnly); + doStart(this.lifecycleBeans, member.name, this.autoStartupOnly, futures); + } + if (concurrentStartup != null && !CollectionUtils.isEmpty(futures)) { + try { + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .get(concurrentStartup, TimeUnit.MILLISECONDS); + } + catch (Exception ex) { + if (ex instanceof ExecutionException exEx) { + Throwable cause = exEx.getCause(); + if (cause instanceof ApplicationContextException acEx) { + throw acEx; + } + } + throw new ApplicationContextException("Failed to start beans in phase " + this.phase + + " within timeout of " + concurrentStartup + "ms", ex); + } } } @@ -528,11 +619,14 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor } } 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); + long shutdownTimeout = determineShutdownTimeout(this.phase); + if (!latch.await(shutdownTimeout, 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 " + shutdownTimeout + "ms: " + countDownBeanNames); + } } } catch (InterruptedException ex) { diff --git a/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java b/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java index 63951ce330..d8dbff7f32 100644 --- a/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java +++ b/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java @@ -523,8 +523,7 @@ final class PostProcessorRegistrationDelegate { try { typedStringValue.resolveTargetType(this.beanFactory.getBeanClassLoader()); } - catch (ClassNotFoundException ex) { - // ignore + catch (ClassNotFoundException ignored) { } } diff --git a/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java b/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java index 36ee67f3a9..18b2733b46 100644 --- a/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java +++ b/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.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. @@ -26,12 +26,12 @@ import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.PlaceholderConfigurerSupport; import org.springframework.context.EnvironmentAware; +import org.springframework.core.convert.ConversionService; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.ConfigurablePropertyResolver; import org.springframework.core.env.Environment; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertiesPropertySource; -import org.springframework.core.env.PropertyResolver; import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySources; import org.springframework.core.env.PropertySourcesPropertyResolver; @@ -50,7 +50,7 @@ import org.springframework.util.StringValueResolver; * XSD documentation for complete details. * *

Any local properties (for example, those added via {@link #setProperties}, {@link #setLocations} - * et al.) are added as a {@code PropertySource}. Search precedence of local properties is + * et al.) are added as a single {@link PropertySource}. Search precedence of local properties is * based on the value of the {@link #setLocalOverride localOverride} property, which is by * default {@code false} meaning that local properties are to be searched last, after all * environment property sources. @@ -99,8 +99,9 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS } /** - * {@code PropertySources} from the given {@link Environment} - * will be searched when replacing ${...} placeholders. + * {@inheritDoc} + *

{@code PropertySources} from the given {@link Environment} will be searched + * when replacing ${...} placeholders. * @see #setPropertySources * @see #postProcessBeanFactory */ @@ -130,27 +131,11 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS if (this.propertySources == null) { this.propertySources = new MutablePropertySources(); if (this.environment != null) { - PropertyResolver propertyResolver = this.environment; - // If the ignoreUnresolvablePlaceholders flag is set to true, we have to create a - // local PropertyResolver to enforce that setting, since the Environment is most - // likely not configured with ignoreUnresolvablePlaceholders set to true. - // See https://github.com/spring-projects/spring-framework/issues/27947 - if (this.ignoreUnresolvablePlaceholders && - (this.environment instanceof ConfigurableEnvironment configurableEnvironment)) { - PropertySourcesPropertyResolver resolver = - new PropertySourcesPropertyResolver(configurableEnvironment.getPropertySources()); - resolver.setIgnoreUnresolvableNestedPlaceholders(true); - propertyResolver = resolver; - } - PropertyResolver propertyResolverToUse = propertyResolver; - this.propertySources.addLast( - new PropertySource<>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) { - @Override - public @Nullable String getProperty(String key) { - return propertyResolverToUse.getProperty(key); - } - } - ); + PropertySource environmentPropertySource = + (this.environment instanceof ConfigurableEnvironment configurableEnvironment ? + new ConfigurableEnvironmentPropertySource(configurableEnvironment) : + new FallbackEnvironmentPropertySource(this.environment)); + this.propertySources.addLast(environmentPropertySource); } try { PropertySource localPropertySource = @@ -173,6 +158,7 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS /** * Create a {@link ConfigurablePropertyResolver} for the specified property sources. + *

The default implementation creates a {@link PropertySourcesPropertyResolver}. * @param propertySources the property sources to use * @since 6.0.12 */ @@ -185,7 +171,7 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS * placeholders with values from the given properties. */ protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, - final ConfigurablePropertyResolver propertyResolver) throws BeansException { + ConfigurablePropertyResolver propertyResolver) throws BeansException { propertyResolver.setPlaceholderPrefix(this.placeholderPrefix); propertyResolver.setPlaceholderSuffix(this.placeholderSuffix); @@ -213,7 +199,7 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS * {@link #processProperties(ConfigurableListableBeanFactory, ConfigurablePropertyResolver)} */ @Override - @Deprecated + @Deprecated(since = "3.1") protected void processProperties(ConfigurableListableBeanFactory beanFactory, Properties props) { throw new UnsupportedOperationException( "Call processProperties(ConfigurableListableBeanFactory, ConfigurablePropertyResolver) instead"); @@ -231,4 +217,91 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS return this.appliedPropertySources; } + + /** + * Custom {@link PropertySource} that delegates to the + * {@link ConfigurableEnvironment#getPropertySources() PropertySources} in a + * {@link ConfigurableEnvironment}. + * @since 6.2.7 + */ + private static class ConfigurableEnvironmentPropertySource extends PropertySource { + + ConfigurableEnvironmentPropertySource(ConfigurableEnvironment environment) { + super(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, environment); + } + + @Override + public boolean containsProperty(String name) { + for (PropertySource propertySource : super.source.getPropertySources()) { + if (propertySource.containsProperty(name)) { + return true; + } + } + return false; + } + + @Override + // Declare String as covariant return type, since a String is actually required. + public @Nullable String getProperty(String name) { + for (PropertySource propertySource : super.source.getPropertySources()) { + Object candidate = propertySource.getProperty(name); + if (candidate != null) { + return convertToString(candidate); + } + } + return null; + } + + /** + * Convert the supplied value to a {@link String} using the {@link ConversionService} + * from the {@link Environment}. + *

This is a modified version of + * {@link org.springframework.core.env.AbstractPropertyResolver#convertValueIfNecessary(Object, Class)}. + * @param value the value to convert + * @return the converted value, or the original value if no conversion is necessary + * @since 6.2.8 + */ + private @Nullable String convertToString(Object value) { + if (value instanceof String string) { + return string; + } + return super.source.getConversionService().convert(value, String.class); + } + + @Override + public String toString() { + return "ConfigurableEnvironmentPropertySource {propertySources=" + super.source.getPropertySources() + "}"; + } + } + + + /** + * Fallback {@link PropertySource} that delegates to a raw {@link Environment}. + *

Should never apply in a regular scenario, since the {@code Environment} + * in an {@code ApplicationContext} should always be a {@link ConfigurableEnvironment}. + * @since 6.2.7 + */ + private static class FallbackEnvironmentPropertySource extends PropertySource { + + FallbackEnvironmentPropertySource(Environment environment) { + super(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, environment); + } + + @Override + public boolean containsProperty(String name) { + return super.source.containsProperty(name); + } + + @Override + // Declare String as covariant return type, since a String is actually required. + public @Nullable String getProperty(String name) { + return super.source.getProperty(name); + } + + @Override + public String toString() { + return "FallbackEnvironmentPropertySource {environment=" + super.source + "}"; + } + } + } diff --git a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java index 97d48897ac..af524d54d6 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.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. @@ -926,8 +926,8 @@ public class MBeanExporter extends MBeanRegistrationSupport implements MBeanExpo */ private boolean isExcluded(String beanName) { return (this.excludedBeans.contains(beanName) || - (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX) && - this.excludedBeans.contains(beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length())))); + (!beanName.isEmpty() && (beanName.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR) && + this.excludedBeans.contains(beanName.substring(1)))); // length of '&' } /** diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameters.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameters.java index 0ccf57a820..89aa97786a 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameters.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameters.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. @@ -39,6 +39,6 @@ import java.lang.annotation.Target; @Documented public @interface ManagedOperationParameters { - ManagedOperationParameter[] value() default {}; + ManagedOperationParameter[] value(); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java index 38f5c2f410..cced791405 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.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. @@ -146,6 +146,12 @@ import org.springframework.core.Ordered; * compile-time weaving or load-time weaving applying the aspect to the affected classes. * There is no proxy involved in such a scenario; local calls will be intercepted as well. * + *

Note: {@code @EnableAsync} applies to its local application context only, + * allowing for selective activation at different levels. Please redeclare + * {@code @EnableAsync} in each individual context, for example, the common root web + * application context and any separate {@code DispatcherServlet} application contexts, + * if you need to apply its behavior at multiple levels. + * * @author Chris Beams * @author Juergen Hoeller * @author Stephane Nicoll diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupport.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupport.java index a99ecfedbf..874afca7d8 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupport.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupport.java @@ -204,9 +204,9 @@ abstract class ScheduledAnnotationReactiveSupport { final Supplier contextSupplier; SubscribingRunnable(Publisher publisher, boolean shouldBlock, - @Nullable String qualifier, List subscriptionTrackerRegistry, - String displayName, Supplier observationRegistrySupplier, - Supplier contextSupplier) { + @Nullable String qualifier, List subscriptionTrackerRegistry, + String displayName, Supplier observationRegistrySupplier, + Supplier contextSupplier) { this.publisher = publisher; this.shouldBlock = shouldBlock; @@ -234,7 +234,7 @@ abstract class ScheduledAnnotationReactiveSupport { latch.await(); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } } else { diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java index 322ced233a..a11ca7d04a 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.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. @@ -153,7 +153,7 @@ public class ConcurrentTaskExecutor implements AsyncTaskExecutor, SchedulingTask this.adaptedExecutor.execute(task); } - @Deprecated + @Deprecated(since = "5.3.16") @Override public void execute(Runnable task, long startTimeout) { this.adaptedExecutor.execute(task, startTimeout); diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java index b0d754cc67..729be479e0 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.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. @@ -299,10 +299,9 @@ public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport /** * Return the current setting for the remove-on-cancel mode. *

Requires an underlying {@link ScheduledThreadPoolExecutor}. - * @deprecated as of 5.3.9, in favor of direct - * {@link #getScheduledThreadPoolExecutor()} access + * @deprecated in favor of direct {@link #getScheduledThreadPoolExecutor()} access */ - @Deprecated + @Deprecated(since = "5.3.9") public boolean isRemoveOnCancelPolicy() { if (this.scheduledExecutor == null) { // Not initialized yet: return our setting for the time being. 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 6d3c9fbbe0..46ae985424 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.Collections; 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; @@ -536,14 +535,13 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { *

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 and 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. @@ -561,8 +559,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { 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; } @@ -1270,9 +1267,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { * 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 @@ -1288,8 +1285,13 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { 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/ValidationUtils.java b/spring-context/src/main/java/org/springframework/validation/ValidationUtils.java index 263558009c..7744ba60ab 100644 --- a/spring-context/src/main/java/org/springframework/validation/ValidationUtils.java +++ b/spring-context/src/main/java/org/springframework/validation/ValidationUtils.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. @@ -250,7 +250,7 @@ public abstract class ValidationUtils { Assert.notNull(errors, "Errors object must not be null"); Object value = errors.getFieldValue(field); - if (value == null ||!StringUtils.hasText(value.toString())) { + if (value == null || !StringUtils.hasText(value.toString())) { errors.rejectValue(field, errorCode, errorArgs, defaultMessage); } } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java index b07d8ef783..2c698a987c 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java @@ -266,8 +266,8 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter Method eclMethod = configuration.getClass().getMethod("externalClassLoader", ClassLoader.class); ReflectionUtils.invokeMethod(eclMethod, configuration, this.applicationContext.getClassLoader()); } - catch (NoSuchMethodException ex) { - // Ignore - no Hibernate Validator 5.2+ or similar provider + catch (NoSuchMethodException ignored) { + // no Hibernate Validator 5.2+ or similar provider } } @@ -417,8 +417,8 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter try { return super.unwrap(type); } - catch (ValidationException ex) { - // Ignore - we'll try ValidatorFactory unwrapping next + catch (ValidationException ignored) { + // we'll try ValidatorFactory unwrapping next } } if (this.validatorFactory != null) { diff --git a/spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml b/spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml deleted file mode 100644 index 3ea5d627e9..0000000000 --- a/spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - diff --git a/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java index b99c0c13cc..7e54c19129 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java @@ -233,8 +233,7 @@ abstract class AbstractAopProxyTests { try { p2.echo(new IOException()); } - catch (IOException ex) { - + catch (IOException ignored) { } assertThat(cta.getCalls()).isEqualTo(2); } diff --git a/spring-context/src/test/java/org/springframework/cache/annotation/ReactiveCachingTests.java b/spring-context/src/test/java/org/springframework/cache/annotation/ReactiveCachingTests.java index e065cde9d1..f8cc874eac 100644 --- a/spring-context/src/test/java/org/springframework/cache/annotation/ReactiveCachingTests.java +++ b/spring-context/src/test/java/org/springframework/cache/annotation/ReactiveCachingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,9 @@ package org.springframework.cache.annotation; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; @@ -40,7 +42,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for annotation-based caching methods that use reactive operators. @@ -58,8 +60,8 @@ class ReactiveCachingTests { LateCacheHitDeterminationWithValueWrapperConfig.class}) void cacheHitDetermination(Class configClass) { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class); + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + configClass, ReactiveCacheableService.class); ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); Object key = new Object(); @@ -119,68 +121,6 @@ class ReactiveCachingTests { ctx.close(); } - @Test - void cacheErrorHandlerWithLoggingCacheErrorHandler() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class, ErrorHandlerCachingConfiguration.class); - ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); - - Object key = new Object(); - Long r1 = service.cacheFuture(key).join(); - - assertThat(r1).isNotNull(); - assertThat(r1).as("cacheFuture").isEqualTo(0L); - - key = new Object(); - - r1 = service.cacheMono(key).block(); - - assertThat(r1).isNotNull(); - assertThat(r1).as("cacheMono").isEqualTo(1L); - - key = new Object(); - - r1 = service.cacheFlux(key).blockFirst(); - - assertThat(r1).isNotNull(); - assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L); - } - - @Test - void cacheErrorHandlerWithLoggingCacheErrorHandlerAndMethodError() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveFailureCacheableService.class, ErrorHandlerCachingConfiguration.class); - ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); - - Object key = new Object(); - StepVerifier.create(service.cacheMono(key)) - .expectErrorMessage("mono service error") - .verify(); - - key = new Object(); - StepVerifier.create(service.cacheFlux(key)) - .expectErrorMessage("flux service error") - .verify(); - } - - @Test - void cacheErrorHandlerWithSimpleCacheErrorHandler() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class); - ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); - - Throwable completableFuturThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join()); - assertThat(completableFuturThrowable).isInstanceOf(CompletionException.class) - .extracting(Throwable::getCause) - .isInstanceOf(UnsupportedOperationException.class); - - Throwable monoThrowable = catchThrowable(() -> service.cacheMono(new Object()).block()); - assertThat(monoThrowable).isInstanceOf(UnsupportedOperationException.class); - - Throwable fluxThrowable = catchThrowable(() -> service.cacheFlux(new Object()).blockFirst()); - assertThat(fluxThrowable).isInstanceOf(UnsupportedOperationException.class); - } - @ParameterizedTest @ValueSource(classes = {EarlyCacheHitDeterminationConfig.class, EarlyCacheHitDeterminationWithoutNullValuesConfig.class, @@ -188,8 +128,8 @@ class ReactiveCachingTests { LateCacheHitDeterminationWithValueWrapperConfig.class}) void fluxCacheDoesntDependOnFirstRequest(Class configClass) { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class); + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + configClass, ReactiveCacheableService.class); ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); Object key = new Object(); @@ -207,6 +147,115 @@ class ReactiveCachingTests { ctx.close(); } + @Test + void cacheErrorHandlerWithSimpleCacheErrorHandler() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + ExceptionCacheManager.class, ReactiveCacheableService.class); + ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); + + assertThatExceptionOfType(CompletionException.class) + .isThrownBy(() -> service.cacheFuture(new Object()).join()) + .withCauseInstanceOf(UnsupportedOperationException.class); + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> service.cacheMono(new Object()).block()); + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> service.cacheFlux(new Object()).blockFirst()); + } + + @Test + void cacheErrorHandlerWithSimpleCacheErrorHandlerAndSync() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + ExceptionCacheManager.class, ReactiveSyncCacheableService.class); + ReactiveSyncCacheableService service = ctx.getBean(ReactiveSyncCacheableService.class); + + assertThatExceptionOfType(CompletionException.class) + .isThrownBy(() -> service.cacheFuture(new Object()).join()) + .withCauseInstanceOf(UnsupportedOperationException.class); + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> service.cacheMono(new Object()).block()); + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> service.cacheFlux(new Object()).blockFirst()); + } + + @Test + void cacheErrorHandlerWithLoggingCacheErrorHandler() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + ExceptionCacheManager.class, ReactiveCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); + + Long r1 = service.cacheFuture(new Object()).join(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFuture").isEqualTo(0L); + + r1 = service.cacheMono(new Object()).block(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheMono").isEqualTo(1L); + + r1 = service.cacheFlux(new Object()).blockFirst(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L); + } + + @Test + void cacheErrorHandlerWithLoggingCacheErrorHandlerAndSync() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveSyncCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveSyncCacheableService service = ctx.getBean(ReactiveSyncCacheableService.class); + + Long r1 = service.cacheFuture(new Object()).join(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFuture").isEqualTo(0L); + + r1 = service.cacheMono(new Object()).block(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheMono").isEqualTo(1L); + + r1 = service.cacheFlux(new Object()).blockFirst(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L); + } + + @Test + void cacheErrorHandlerWithLoggingCacheErrorHandlerAndOperationException() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(EarlyCacheHitDeterminationConfig.class, ReactiveFailureCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveFailureCacheableService service = ctx.getBean(ReactiveFailureCacheableService.class); + + assertThatExceptionOfType(CompletionException.class).isThrownBy(() -> service.cacheFuture(new Object()).join()) + .withMessage(IllegalStateException.class.getName() + ": future service error"); + + StepVerifier.create(service.cacheMono(new Object())) + .expectErrorMessage("mono service error") + .verify(); + + StepVerifier.create(service.cacheFlux(new Object())) + .expectErrorMessage("flux service error") + .verify(); + } + + @Test + void cacheErrorHandlerWithLoggingCacheErrorHandlerAndOperationExceptionAndSync() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(EarlyCacheHitDeterminationConfig.class, ReactiveSyncFailureCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveSyncFailureCacheableService service = ctx.getBean(ReactiveSyncFailureCacheableService.class); + + assertThatExceptionOfType(CompletionException.class).isThrownBy(() -> service.cacheFuture(new Object()).join()) + .withMessage(IllegalStateException.class.getName() + ": future service error"); + + StepVerifier.create(service.cacheMono(new Object())) + .expectErrorMessage("mono service error") + .verify(); + + StepVerifier.create(service.cacheFlux(new Object())) + .expectErrorMessage("flux service error") + .verify(); + } + + @CacheConfig(cacheNames = "first") static class ReactiveCacheableService { @@ -232,16 +281,94 @@ class ReactiveCachingTests { } } + @CacheConfig(cacheNames = "first") - static class ReactiveFailureCacheableService extends ReactiveCacheableService { + static class ReactiveSyncCacheableService { + + private final AtomicLong counter = new AtomicLong(); + + @Cacheable(sync = true) + CompletableFuture cacheFuture(Object arg) { + return CompletableFuture.completedFuture(this.counter.getAndIncrement()); + } + + @Cacheable(sync = true) + Mono cacheMono(Object arg) { + return Mono.defer(() -> Mono.just(this.counter.getAndIncrement())); + } + + @Cacheable(sync = true) + Flux cacheFlux(Object arg) { + return Flux.defer(() -> Flux.just(this.counter.getAndIncrement(), 0L, -1L, -2L, -3L)); + } + } + + + @CacheConfig(cacheNames = "first") + static class ReactiveFailureCacheableService { + + private final AtomicBoolean cacheFutureInvoked = new AtomicBoolean(); + + private final AtomicBoolean cacheMonoInvoked = new AtomicBoolean(); + + private final AtomicBoolean cacheFluxInvoked = new AtomicBoolean(); + + @Cacheable + CompletableFuture cacheFuture(Object arg) { + if (!this.cacheFutureInvoked.compareAndSet(false, true)) { + return CompletableFuture.failedFuture(new IllegalStateException("future service invoked twice")); + } + return CompletableFuture.failedFuture(new IllegalStateException("future service error")); + } @Cacheable Mono cacheMono(Object arg) { + if (!this.cacheMonoInvoked.compareAndSet(false, true)) { + return Mono.error(new IllegalStateException("mono service invoked twice")); + } return Mono.error(new IllegalStateException("mono service error")); } @Cacheable Flux cacheFlux(Object arg) { + if (!this.cacheFluxInvoked.compareAndSet(false, true)) { + return Flux.error(new IllegalStateException("flux service invoked twice")); + } + return Flux.error(new IllegalStateException("flux service error")); + } + } + + + @CacheConfig(cacheNames = "first") + static class ReactiveSyncFailureCacheableService { + + private final AtomicBoolean cacheFutureInvoked = new AtomicBoolean(); + + private final AtomicBoolean cacheMonoInvoked = new AtomicBoolean(); + + private final AtomicBoolean cacheFluxInvoked = new AtomicBoolean(); + + @Cacheable(sync = true) + CompletableFuture cacheFuture(Object arg) { + if (!this.cacheFutureInvoked.compareAndSet(false, true)) { + return CompletableFuture.failedFuture(new IllegalStateException("future service invoked twice")); + } + return CompletableFuture.failedFuture(new IllegalStateException("future service error")); + } + + @Cacheable(sync = true) + Mono cacheMono(Object arg) { + if (!this.cacheMonoInvoked.compareAndSet(false, true)) { + return Mono.error(new IllegalStateException("mono service invoked twice")); + } + return Mono.error(new IllegalStateException("mono service error")); + } + + @Cacheable(sync = true) + Flux cacheFlux(Object arg) { + if (!this.cacheFluxInvoked.compareAndSet(false, true)) { + return Flux.error(new IllegalStateException("flux service invoked twice")); + } return Flux.error(new IllegalStateException("flux service error")); } } @@ -323,6 +450,7 @@ class ReactiveCachingTests { } } + @Configuration static class ErrorHandlerCachingConfiguration implements CachingConfigurer { @@ -333,6 +461,7 @@ class ReactiveCachingTests { } } + @Configuration(proxyBeanMethods = false) @EnableCaching static class ExceptionCacheManager { @@ -345,11 +474,12 @@ class ReactiveCachingTests { return new ConcurrentMapCache(name, isAllowNullValues()) { @Override public CompletableFuture retrieve(Object key) { - return CompletableFuture.supplyAsync(() -> { - throw new UnsupportedOperationException("Test exception on retrieve"); - }); + return CompletableFuture.failedFuture(new UnsupportedOperationException("Test exception on retrieve")); + } + @Override + public CompletableFuture retrieve(Object key, Supplier> valueLoader) { + return CompletableFuture.failedFuture(new UnsupportedOperationException("Test exception on retrieve")); } - @Override public void put(Object key, @Nullable Object value) { throw new UnsupportedOperationException("Test exception on put"); diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/SimpleKeyGeneratorTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/SimpleKeyGeneratorTests.java index 06ff77d58a..20f780fa3a 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/SimpleKeyGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/SimpleKeyGeneratorTests.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. @@ -130,7 +130,7 @@ class SimpleKeyGeneratorTests { private Object generateKey(Object[] arguments) { - Method method = ReflectionUtils.findMethod(this.getClass(), "generateKey", Object[].class); + Method method = ReflectionUtils.findMethod(getClass(), "generateKey", Object[].class); return this.generator.generate(this, method, arguments); } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java index 913cc863d0..ed5e45b85b 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java @@ -16,10 +16,17 @@ package org.springframework.context.annotation; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanCurrentlyInCreationException; +import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.UnsatisfiedDependencyException; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.testfixture.beans.TestBean; @@ -29,6 +36,7 @@ import org.springframework.core.testfixture.EnabledForTestGroups; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.context.annotation.Bean.Bootstrap.BACKGROUND; import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; @@ -39,7 +47,7 @@ import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; class BackgroundBootstrapTests { @Test - @Timeout(5) + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) void bootstrapWithUnmanagedThread() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadBeanConfig.class); @@ -49,7 +57,7 @@ class BackgroundBootstrapTests { } @Test - @Timeout(5) + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) void bootstrapWithUnmanagedThreads() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadsBeanConfig.class); @@ -61,9 +69,9 @@ class BackgroundBootstrapTests { } @Test - @Timeout(5) + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) - void bootstrapWithStrictLockingThread() { + void bootstrapWithStrictLockingFlag() { SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME); try { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(StrictLockingBeanConfig.class); @@ -76,17 +84,80 @@ class BackgroundBootstrapTests { } @Test - @Timeout(5) + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) - void bootstrapWithCircularReference() { - ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CircularReferenceBeanConfig.class); + void bootstrapWithStrictLockingInferred() throws InterruptedException { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(InferredLockingBeanConfig.class); + ExecutorService threadPool = Executors.newFixedThreadPool(2); + threadPool.submit(() -> ctx.refresh()); + Thread.sleep(500); + threadPool.submit(() -> ctx.getBean("testBean2")); + Thread.sleep(1000); + assertThat(ctx.getBean("testBean2", TestBean.class).getSpouse()).isSameAs(ctx.getBean("testBean1")); + ctx.close(); + } + + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithStrictLockingTurnedOff() throws InterruptedException { + SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, false); + try { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(InferredLockingBeanConfig.class); + ExecutorService threadPool = Executors.newFixedThreadPool(2); + threadPool.submit(() -> ctx.refresh()); + Thread.sleep(500); + threadPool.submit(() -> ctx.getBean("testBean2")); + Thread.sleep(1000); + assertThat(ctx.getBean("testBean2", TestBean.class).getSpouse()).isNull(); + ctx.close(); + } + finally { + SpringProperties.setProperty(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, null); + } + } + + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCircularReferenceAgainstMainThread() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CircularReferenceAgainstMainThreadBeanConfig.class); ctx.getBean("testBean1", TestBean.class); ctx.getBean("testBean2", TestBean.class); ctx.close(); } @Test - @Timeout(5) + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCircularReferenceWithBlockingMainThread() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> new AnnotationConfigApplicationContext(CircularReferenceWithBlockingMainThreadBeanConfig.class)) + .withRootCauseInstanceOf(BeanCurrentlyInCreationException.class); + } + + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCircularReferenceInSameThread() { + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> new AnnotationConfigApplicationContext(CircularReferenceInSameThreadBeanConfig.class)) + .withRootCauseInstanceOf(BeanCurrentlyInCreationException.class); + } + + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCircularReferenceInMultipleThreads() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> new AnnotationConfigApplicationContext(CircularReferenceInMultipleThreadsBeanConfig.class)) + .withRootCauseInstanceOf(BeanCurrentlyInCreationException.class); + } + + @Test + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) void bootstrapWithCustomExecutor() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CustomExecutorBeanConfig.class); @@ -97,6 +168,24 @@ class BackgroundBootstrapTests { ctx.close(); } + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCustomExecutorAndStrictLocking() { + SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME); + try { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CustomExecutorBeanConfig.class); + ctx.getBean("testBean1", TestBean.class); + ctx.getBean("testBean2", TestBean.class); + ctx.getBean("testBean3", TestBean.class); + ctx.getBean("testBean4", TestBean.class); + ctx.close(); + } + finally { + SpringProperties.setProperty(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, null); + } + } + @Configuration(proxyBeanMethods = false) static class UnmanagedThreadBeanConfig { @@ -108,7 +197,7 @@ class BackgroundBootstrapTests { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -119,7 +208,7 @@ class BackgroundBootstrapTests { Thread.sleep(2000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -139,7 +228,7 @@ class BackgroundBootstrapTests { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -155,14 +244,24 @@ class BackgroundBootstrapTests { } @Bean - public TestBean testBean4() { + public FactoryBean testBean4() { try { Thread.sleep(2000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } - return new TestBean(); + TestBean testBean = new TestBean(); + return new FactoryBean<>() { + @Override + public TestBean getObject() { + return testBean; + } + @Override + public Class getObjectType() { + return testBean.getClass(); + } + }; } } @@ -177,9 +276,9 @@ class BackgroundBootstrapTests { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } - return new TestBean(); + return new TestBean("testBean1"); } @Bean @@ -190,7 +289,28 @@ class BackgroundBootstrapTests { @Configuration(proxyBeanMethods = false) - static class CircularReferenceBeanConfig { + static class InferredLockingBeanConfig { + + @Bean + public TestBean testBean1() { + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean("testBean1"); + } + + @Bean + public TestBean testBean2(ConfigurableListableBeanFactory beanFactory) { + return new TestBean((TestBean) beanFactory.getSingleton("testBean1")); + } + } + + + @Configuration(proxyBeanMethods = false) + static class CircularReferenceAgainstMainThreadBeanConfig { @Bean public TestBean testBean1(ObjectProvider testBean2) { @@ -199,7 +319,7 @@ class BackgroundBootstrapTests { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -210,13 +330,128 @@ class BackgroundBootstrapTests { Thread.sleep(2000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } } + @Configuration(proxyBeanMethods = false) + static class CircularReferenceWithBlockingMainThreadBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean2) { + new Thread(testBean2::getObject).start(); + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(testBean2.getObject()); + } + + @Bean + public TestBean testBean2(ObjectProvider testBean1) { + try { + Thread.sleep(2000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(testBean1.getObject()); + } + } + + + @Configuration(proxyBeanMethods = false) + static class CircularReferenceInSameThreadBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean2) { + new Thread(testBean2::getObject).start(); + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(); + } + + @Bean + public TestBean testBean2(TestBean testBean3) { + try { + Thread.sleep(2000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(); + } + + @Bean + public TestBean testBean3(TestBean testBean2) { + return new TestBean(); + } + } + + + @Configuration(proxyBeanMethods = false) + static class CircularReferenceInMultipleThreadsBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean2, ObjectProvider testBean3, + ObjectProvider testBean4) { + + new Thread(testBean2::getObject).start(); + new Thread(testBean3::getObject).start(); + new Thread(testBean4::getObject).start(); + try { + Thread.sleep(3000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(); + } + + @Bean + public TestBean testBean2(ObjectProvider testBean3) { + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(testBean3.getObject()); + } + + @Bean + public TestBean testBean3(ObjectProvider testBean4) { + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(testBean4.getObject()); + } + + @Bean + public TestBean testBean4(ObjectProvider testBean2) { + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(testBean2.getObject()); + } + } + + @Configuration(proxyBeanMethods = false) static class CustomExecutorBeanConfig { @@ -231,13 +466,13 @@ class BackgroundBootstrapTests { @Bean(bootstrap = BACKGROUND) @DependsOn("testBean3") public TestBean testBean1(TestBean testBean3) throws InterruptedException { - Thread.sleep(3000); + Thread.sleep(6000); return new TestBean(); } @Bean(bootstrap = BACKGROUND) @Lazy public TestBean testBean2() throws InterruptedException { - Thread.sleep(3000); + Thread.sleep(6000); return new TestBean(); } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessorTests.java index 170d26dcf3..bfd003c533 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessorTests.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. @@ -227,7 +227,7 @@ class CommonAnnotationBeanPostProcessorTests { bf.registerResolvableDependency(BeanFactory.class, bf); bf.registerResolvableDependency(INestedTestBean.class, (ObjectFactory) NestedTestBean::new); - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "removal"}) org.springframework.beans.factory.config.PropertyPlaceholderConfigurer ppc = new org.springframework.beans.factory.config.PropertyPlaceholderConfigurer(); Properties props = new Properties(); props.setProperty("tb", "testBean4"); @@ -312,7 +312,7 @@ class CommonAnnotationBeanPostProcessorTests { bf.addBeanPostProcessor(bpp); bf.registerResolvableDependency(BeanFactory.class, bf); - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "removal"}) org.springframework.beans.factory.config.PropertyPlaceholderConfigurer ppc = new org.springframework.beans.factory.config.PropertyPlaceholderConfigurer(); Properties props = new Properties(); props.setProperty("tb", "testBean3"); @@ -363,7 +363,7 @@ class CommonAnnotationBeanPostProcessorTests { bf.addBeanPostProcessor(bpp); bf.registerResolvableDependency(BeanFactory.class, bf); - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "removal"}) org.springframework.beans.factory.config.PropertyPlaceholderConfigurer ppc = new org.springframework.beans.factory.config.PropertyPlaceholderConfigurer(); Properties props = new Properties(); props.setProperty("tb", "testBean3"); 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 5e60f31a6d..ff801346c7 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java @@ -76,7 +76,7 @@ class ConfigurationClassEnhancerTests { classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader); assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass); - assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); } @Test @@ -104,6 +104,31 @@ class ConfigurationClassEnhancerTests { 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(); @@ -111,7 +136,7 @@ class ConfigurationClassEnhancerTests { ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader()); Class enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass); - assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); classLoader = new OverridingClassLoader(getClass().getClassLoader()); enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); @@ -160,6 +185,19 @@ class ConfigurationClassEnhancerTests { } + @Configuration + public static class MyConfigWithNonPublicConstructor { + + MyConfigWithNonPublicConstructor() { + } + + @Bean + public String myBean() { + return "bean"; + } + } + + @Configuration public static class MyConfigWithNonPublicMethod { 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 122111aee9..2add6155bf 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java @@ -129,6 +129,22 @@ class ConfigurationClassPostProcessorTests { assertThat(beanFactory.getDependentBeans("config")).contains("bar"); } + @Test // gh-34663 + void enhancementIsPresentForAbstractConfigClassWithoutBeanMethods() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(AbstractConfigWithoutBeanMethods.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + RootBeanDefinition beanDefinition = (RootBeanDefinition) beanFactory.getBeanDefinition("config"); + assertThat(beanDefinition.hasBeanClass()).isTrue(); + assertThat(beanDefinition.getBeanClass().getName()).contains(ClassUtils.CGLIB_CLASS_SEPARATOR); + Foo foo = beanFactory.getBean("foo", Foo.class); + Bar bar = beanFactory.getBean("bar", Bar.class); + assertThat(bar.foo).isSameAs(foo); + assertThat(beanFactory.getDependentBeans("foo")).contains("bar"); + String[] dependentsOfSingletonBeanConfig = beanFactory.getDependentBeans(SingletonBeanConfig.class.getName()); + assertThat(dependentsOfSingletonBeanConfig).containsOnly("foo", "bar"); + } + @Test void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalse() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(NonEnhancedSingletonBeanConfig.class)); @@ -181,7 +197,7 @@ class ConfigurationClassPostProcessorTests { assertThat(bar.foo).isNotSameAs(foo); } - @Test + @Test // gh-34486 void enhancementIsNotPresentWithEmptyConfig() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(EmptyConfig.class)); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); @@ -1195,6 +1211,12 @@ class ConfigurationClassPostProcessorTests { } } + @Configuration + @Import(SingletonBeanConfig.class) + abstract static class AbstractConfigWithoutBeanMethods { + // This class intentionally does NOT declare @Bean methods. + } + @Configuration static final class EmptyConfig { } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java index 32d8c012e5..a552e26ede 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java @@ -267,7 +267,7 @@ class ConfigurationClassProcessingTests { void configurationWithPostProcessor() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ConfigWithPostProcessor.class); - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "removal"}) RootBeanDefinition placeholderConfigurer = new RootBeanDefinition( org.springframework.beans.factory.config.PropertyPlaceholderConfigurer.class); placeholderConfigurer.getPropertyValues().add("properties", "myProp=myValue"); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportResourceTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportResourceTests.java index 565b58e9d8..7b717f2c5a 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportResourceTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportResourceTests.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,8 +16,6 @@ package org.springframework.context.annotation.configuration; -import java.util.Collections; - import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.junit.jupiter.api.Test; @@ -25,18 +23,18 @@ import org.junit.jupiter.api.Test; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportResource; -import org.springframework.core.env.MapPropertySource; -import org.springframework.core.env.PropertySource; +import org.springframework.core.testfixture.env.MockPropertySource; import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link ImportResource} support. + * Integration tests for {@link ImportResource @ImportResource} support. * * @author Chris Beams * @author Juergen Hoeller @@ -45,81 +43,88 @@ import static org.assertj.core.api.Assertions.assertThat; class ImportResourceTests { @Test - void importXml() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlConfig.class); - assertThat(ctx.containsBean("javaDeclaredBean")).as("did not contain java-declared bean").isTrue(); - assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue(); - TestBean tb = ctx.getBean("javaDeclaredBean", TestBean.class); - assertThat(tb.getName()).isEqualTo("myName"); - ctx.close(); + void importResource() { + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlConfig.class)) { + assertThat(ctx.containsBean("javaDeclaredBean")).as("did not contain java-declared bean").isTrue(); + assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue(); + TestBean tb = ctx.getBean("javaDeclaredBean", TestBean.class); + assertThat(tb.getName()).isEqualTo("myName"); + } } @Test - void importXmlIsInheritedFromSuperclassDeclarations() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(FirstLevelSubConfig.class); - assertThat(ctx.containsBean("xmlDeclaredBean")).isTrue(); - ctx.close(); + void importResourceIsInheritedFromSuperclassDeclarations() { + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(FirstLevelSubConfig.class)) { + assertThat(ctx.containsBean("xmlDeclaredBean")).isTrue(); + } } @Test - void importXmlIsMergedFromSuperclassDeclarations() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SecondLevelSubConfig.class); - assertThat(ctx.containsBean("secondLevelXmlDeclaredBean")).as("failed to pick up second-level-declared XML bean").isTrue(); - assertThat(ctx.containsBean("xmlDeclaredBean")).as("failed to pick up parent-declared XML bean").isTrue(); - ctx.close(); + void importResourceIsMergedFromSuperclassDeclarations() { + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SecondLevelSubConfig.class)) { + assertThat(ctx.containsBean("secondLevelXmlDeclaredBean")).as("failed to pick up second-level-declared XML bean").isTrue(); + assertThat(ctx.containsBean("xmlDeclaredBean")).as("failed to pick up parent-declared XML bean").isTrue(); + } } @Test - void importXmlWithNamespaceConfig() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlWithAopNamespaceConfig.class); - Object bean = ctx.getBean("proxiedXmlBean"); - assertThat(AopUtils.isAopProxy(bean)).isTrue(); - ctx.close(); + void importResourceWithNamespaceConfig() { + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlWithAopNamespaceConfig.class)) { + Object bean = ctx.getBean("proxiedXmlBean"); + assertThat(AopUtils.isAopProxy(bean)).isTrue(); + } } @Test - void importXmlWithOtherConfigurationClass() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlWithConfigurationClass.class); - assertThat(ctx.containsBean("javaDeclaredBean")).as("did not contain java-declared bean").isTrue(); - assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue(); - TestBean tb = ctx.getBean("javaDeclaredBean", TestBean.class); - assertThat(tb.getName()).isEqualTo("myName"); - ctx.close(); + void importResourceWithOtherConfigurationClass() { + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlWithConfigurationClass.class)) { + assertThat(ctx.containsBean("javaDeclaredBean")).as("did not contain java-declared bean").isTrue(); + assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue(); + TestBean tb = ctx.getBean("javaDeclaredBean", TestBean.class); + assertThat(tb.getName()).isEqualTo("myName"); + } } @Test void importWithPlaceholder() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - PropertySource propertySource = new MapPropertySource("test", - Collections. singletonMap("test", "springframework")); - ctx.getEnvironment().getPropertySources().addFirst(propertySource); - ctx.register(ImportXmlConfig.class); - ctx.refresh(); - assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue(); - ctx.close(); + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) { + ctx.getEnvironment().getPropertySources().addFirst(new MockPropertySource("test").withProperty("test", "springframework")); + ctx.register(ImportXmlConfig.class); + ctx.refresh(); + assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue(); + } } @Test - void importXmlWithAutowiredConfig() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlAutowiredConfig.class); - String name = ctx.getBean("xmlBeanName", String.class); - assertThat(name).isEqualTo("xml.declared"); - ctx.close(); + void importResourceWithAutowiredConfig() { + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlAutowiredConfig.class)) { + String name = ctx.getBean("xmlBeanName", String.class); + assertThat(name).isEqualTo("xml.declared"); + } } @Test void importNonXmlResource() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportNonXmlResourceConfig.class); - assertThat(ctx.containsBean("propertiesDeclaredBean")).isTrue(); - ctx.close(); + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportNonXmlResourceConfig.class)) { + assertThat(ctx.containsBean("propertiesDeclaredBean")).isTrue(); + } + } + + @Test + void importResourceWithPrivateReader() { + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportWithPrivateReaderConfig.class)) { + assertThat(ctx.containsBean("propertiesDeclaredBean")).isTrue(); + } } @Configuration @ImportResource("classpath:org/springframework/context/annotation/configuration/ImportXmlConfig-context.xml") static class ImportXmlConfig { + @Value("${name}") private String name; + @Bean public TestBean javaDeclaredBean() { return new TestBean(this.name); } @@ -146,6 +151,7 @@ class ImportResourceTests { @Aspect static class AnAspect { + @Before("execution(* org.springframework.beans.testfixture.beans.TestBean.*(..))") public void advice() { } } @@ -158,18 +164,37 @@ class ImportResourceTests { @Configuration @ImportResource("classpath:org/springframework/context/annotation/configuration/ImportXmlConfig-context.xml") static class ImportXmlAutowiredConfig { - @Autowired TestBean xmlDeclaredBean; - @Bean public String xmlBeanName() { + @Autowired + TestBean xmlDeclaredBean; + + @Bean + public String xmlBeanName() { return xmlDeclaredBean.getName(); } } @SuppressWarnings("deprecation") @Configuration - @ImportResource(locations = "classpath:org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig-context.properties", + @ImportResource(locations = "org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig.properties", reader = org.springframework.beans.factory.support.PropertiesBeanDefinitionReader.class) static class ImportNonXmlResourceConfig { } + @SuppressWarnings("deprecation") + @Configuration + @ImportResource(locations = "org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig.properties", + reader = PrivatePropertiesBeanDefinitionReader.class) + static class ImportWithPrivateReaderConfig { + } + + @SuppressWarnings("deprecation") + private static class PrivatePropertiesBeanDefinitionReader + extends org.springframework.beans.factory.support.PropertiesBeanDefinitionReader { + + PrivatePropertiesBeanDefinitionReader(BeanDefinitionRegistry registry) { + super(registry); + } + } + } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java index 80172e5380..3dd4b3561b 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.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. @@ -34,37 +34,16 @@ import org.springframework.context.annotation.componentscan.ordered.SiblingImpor import static org.assertj.core.api.Assertions.assertThat; /** - * System tests for {@link Import} annotation support. + * Integration tests for {@link Import @Import} support. * * @author Chris Beams * @author Juergen Hoeller + * @author Daeho Kwon */ class ImportTests { - private DefaultListableBeanFactory processConfigurationClasses(Class... classes) { - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - beanFactory.setAllowBeanDefinitionOverriding(false); - for (Class clazz : classes) { - beanFactory.registerBeanDefinition(clazz.getSimpleName(), new RootBeanDefinition(clazz)); - } - ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); - pp.postProcessBeanFactory(beanFactory); - return beanFactory; - } - - private void assertBeanDefinitionCount(int expectedCount, Class... classes) { - DefaultListableBeanFactory beanFactory = processConfigurationClasses(classes); - assertThat(beanFactory.getBeanDefinitionCount()).isEqualTo(expectedCount); - beanFactory.preInstantiateSingletons(); - for (Class clazz : classes) { - beanFactory.getBean(clazz); - } - } - - // ------------------------------------------------------------------------ - @Test - void testProcessImportsWithAsm() { + void processImportsWithAsm() { int configClasses = 2; int beansInClasses = 2; DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); @@ -75,111 +54,56 @@ class ImportTests { } @Test - void testProcessImportsWithDoubleImports() { + void processImportsWithDoubleImports() { int configClasses = 3; int beansInClasses = 3; assertBeanDefinitionCount((configClasses + beansInClasses), ConfigurationWithImportAnnotation.class, OtherConfigurationWithImportAnnotation.class); } @Test - void testProcessImportsWithExplicitOverridingBefore() { + void processImportsWithExplicitOverridingBefore() { int configClasses = 2; int beansInClasses = 2; assertBeanDefinitionCount((configClasses + beansInClasses), OtherConfiguration.class, ConfigurationWithImportAnnotation.class); } @Test - void testProcessImportsWithExplicitOverridingAfter() { + void processImportsWithExplicitOverridingAfter() { int configClasses = 2; int beansInClasses = 2; assertBeanDefinitionCount((configClasses + beansInClasses), ConfigurationWithImportAnnotation.class, OtherConfiguration.class); } - @Configuration - @Import(OtherConfiguration.class) - static class ConfigurationWithImportAnnotation { - @Bean - ITestBean one() { - return new TestBean(); - } - } - - @Configuration - @Import(OtherConfiguration.class) - static class OtherConfigurationWithImportAnnotation { - @Bean - ITestBean two() { - return new TestBean(); - } - } - - @Configuration - static class OtherConfiguration { - @Bean - ITestBean three() { - return new TestBean(); - } - } - - // ------------------------------------------------------------------------ - @Test - void testImportAnnotationWithTwoLevelRecursion() { + void importAnnotationWithTwoLevelRecursion() { int configClasses = 2; int beansInClasses = 3; assertBeanDefinitionCount((configClasses + beansInClasses), AppConfig.class); } - @Configuration - @Import(DataSourceConfig.class) - static class AppConfig { - - @Bean - ITestBean transferService() { - return new TestBean(accountRepository()); - } - - @Bean - ITestBean accountRepository() { - return new TestBean(); - } - } - - @Configuration - static class DataSourceConfig { - @Bean - ITestBean dataSourceA() { - return new TestBean(); - } - } - - // ------------------------------------------------------------------------ - @Test - void testImportAnnotationWithThreeLevelRecursion() { + void importAnnotationWithThreeLevelRecursion() { int configClasses = 4; int beansInClasses = 5; assertBeanDefinitionCount(configClasses + beansInClasses, FirstLevel.class); } @Test - void testImportAnnotationWithThreeLevelRecursionAndDoubleImport() { + void importAnnotationWithThreeLevelRecursionAndDoubleImport() { int configClasses = 5; int beansInClasses = 5; assertBeanDefinitionCount(configClasses + beansInClasses, FirstLevel.class, FirstLevelPlus.class); } - // ------------------------------------------------------------------------ - @Test - void testImportAnnotationWithMultipleArguments() { + void importAnnotationWithMultipleArguments() { int configClasses = 3; int beansInClasses = 3; assertBeanDefinitionCount((configClasses + beansInClasses), WithMultipleArgumentsToImportAnnotation.class); } @Test - void testImportAnnotationWithMultipleArgumentsResultingInOverriddenBeanDefinition() { + void importAnnotationWithMultipleArgumentsResultingInOverriddenBeanDefinition() { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); beanFactory.setAllowBeanDefinitionOverriding(true); beanFactory.registerBeanDefinition("config", new RootBeanDefinition( @@ -190,159 +114,20 @@ class ImportTests { assertThat(beanFactory.getBean("foo", ITestBean.class).getName()).isEqualTo("foo2"); } - @Configuration - @Import({Foo1.class, Foo2.class}) - static class WithMultipleArgumentsThatWillCauseDuplication { - } - - @Configuration - static class Foo1 { - @Bean - ITestBean foo() { - return new TestBean("foo1"); - } - } - - @Configuration - static class Foo2 { - @Bean - ITestBean foo() { - return new TestBean("foo2"); - } - } - - // ------------------------------------------------------------------------ - @Test - void testImportAnnotationOnInnerClasses() { + void importAnnotationOnInnerClasses() { int configClasses = 2; int beansInClasses = 2; assertBeanDefinitionCount((configClasses + beansInClasses), OuterConfig.InnerConfig.class); } - @Configuration - static class OuterConfig { - @Bean - String whatev() { - return "whatev"; - } - - @Configuration - @Import(ExternalConfig.class) - static class InnerConfig { - @Bean - ITestBean innerBean() { - return new TestBean(); - } - } - } - - @Configuration - static class ExternalConfig { - @Bean - ITestBean extBean() { - return new TestBean(); - } - } - - // ------------------------------------------------------------------------ - - @Configuration - @Import(SecondLevel.class) - static class FirstLevel { - @Bean - TestBean m() { - return new TestBean(); - } - } - - @Configuration - @Import(ThirdLevel.class) - static class FirstLevelPlus { - } - - @Configuration - @Import({ThirdLevel.class, InitBean.class}) - static class SecondLevel { - @Bean - TestBean n() { - return new TestBean(); - } - } - - @Configuration - @DependsOn("org.springframework.context.annotation.configuration.ImportTests$InitBean") - static class ThirdLevel { - ThirdLevel() { - assertThat(InitBean.initialized).isTrue(); - } - - @Bean - ITestBean thirdLevelA() { - return new TestBean(); - } - - @Bean - ITestBean thirdLevelB() { - return new TestBean(); - } - - @Bean - ITestBean thirdLevelC() { - return new TestBean(); - } - } - - static class InitBean { - public static boolean initialized = false; - - InitBean() { - initialized = true; - } - } - - @Configuration - @Import({LeftConfig.class, RightConfig.class}) - static class WithMultipleArgumentsToImportAnnotation { - @Bean - TestBean m() { - return new TestBean(); - } - } - - @Configuration - static class LeftConfig { - @Bean - ITestBean left() { - return new TestBean(); - } - } - - @Configuration - static class RightConfig { - @Bean - ITestBean right() { - return new TestBean(); - } - } - - // ------------------------------------------------------------------------ - @Test - void testImportNonConfigurationAnnotationClass() { + void importNonConfigurationAnnotationClass() { int configClasses = 2; int beansInClasses = 0; assertBeanDefinitionCount((configClasses + beansInClasses), ConfigAnnotated.class); } - @Configuration - @Import(NonConfigAnnotated.class) - static class ConfigAnnotated { } - - static class NonConfigAnnotated { } - - // ------------------------------------------------------------------------ - /** * Test that values supplied to @Configuration(value="...") are propagated as the * bean name for the configuration class even in the case of inclusion via @Import @@ -350,25 +135,14 @@ class ImportTests { */ @Test void reproSpr9023() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.register(B.class); - ctx.refresh(); + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(B.class); assertThat(ctx.getBeanNamesForType(B.class)[0]).isEqualTo("config-b"); assertThat(ctx.getBeanNamesForType(A.class)[0]).isEqualTo("config-a"); ctx.close(); } - @Configuration("config-a") - static class A { } - - @Configuration("config-b") - @Import(A.class) - static class B { } - - // ------------------------------------------------------------------------ - @Test - void testProcessImports() { + void processImports() { int configClasses = 2; int beansInClasses = 2; assertBeanDefinitionCount((configClasses + beansInClasses), ConfigurationWithImportAnnotation.class); @@ -391,4 +165,283 @@ class ImportTests { assertThat(ctx.getBeansOfType(SiblingImportingConfigB.class)).hasSize(1); } + @Test // gh-34820 + void importAnnotationOnImplementedInterfaceIsRespected() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(InterfaceBasedConfig.class); + + assertThat(context.getBean(ImportedConfig.class)).isNotNull(); + assertThat(context.getBean(ImportedBean.class)).hasFieldOrPropertyWithValue("name", "imported"); + + context.close(); + } + + @Test // gh-34820 + void localImportShouldOverrideInterfaceImport() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(OverridingConfig.class); + + assertThat(context.getBean(ImportedConfig.class)).isNotNull(); + assertThat(context.getBean(OverridingImportedConfig.class)).isNotNull(); + assertThat(context.getBean(ImportedBean.class)).hasFieldOrPropertyWithValue("name", "from class"); + + context.close(); + } + + + private static DefaultListableBeanFactory processConfigurationClasses(Class... classes) { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.setAllowBeanDefinitionOverriding(false); + for (Class clazz : classes) { + beanFactory.registerBeanDefinition(clazz.getSimpleName(), new RootBeanDefinition(clazz)); + } + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + return beanFactory; + } + + private static void assertBeanDefinitionCount(int expectedCount, Class... classes) { + DefaultListableBeanFactory beanFactory = processConfigurationClasses(classes); + assertThat(beanFactory.getBeanDefinitionCount()).isEqualTo(expectedCount); + beanFactory.preInstantiateSingletons(); + for (Class clazz : classes) { + beanFactory.getBean(clazz); + } + } + + + @Configuration + @Import(OtherConfiguration.class) + static class ConfigurationWithImportAnnotation { + + @Bean + ITestBean one() { + return new TestBean(); + } + } + + @Configuration + @Import(OtherConfiguration.class) + static class OtherConfigurationWithImportAnnotation { + + @Bean + ITestBean two() { + return new TestBean(); + } + } + + @Configuration + static class OtherConfiguration { + + @Bean + ITestBean three() { + return new TestBean(); + } + } + + @Configuration + @Import(DataSourceConfig.class) + static class AppConfig { + + @Bean + ITestBean transferService() { + return new TestBean(accountRepository()); + } + + @Bean + ITestBean accountRepository() { + return new TestBean(); + } + } + + @Configuration + static class DataSourceConfig { + + @Bean + ITestBean dataSourceA() { + return new TestBean(); + } + } + + @Configuration + @Import({Foo1.class, Foo2.class}) + static class WithMultipleArgumentsThatWillCauseDuplication { + } + + @Configuration + static class Foo1 { + + @Bean + ITestBean foo() { + return new TestBean("foo1"); + } + } + + @Configuration + static class Foo2 { + + @Bean + ITestBean foo() { + return new TestBean("foo2"); + } + } + + @Configuration + static class OuterConfig { + + @Bean + String whatev() { + return "whatev"; + } + + @Configuration + @Import(ExternalConfig.class) + static class InnerConfig { + @Bean + ITestBean innerBean() { + return new TestBean(); + } + } + } + + @Configuration + static class ExternalConfig { + + @Bean + ITestBean extBean() { + return new TestBean(); + } + } + + @Configuration + @Import(SecondLevel.class) + static class FirstLevel { + + @Bean + TestBean m() { + return new TestBean(); + } + } + + @Configuration + @Import(ThirdLevel.class) + static class FirstLevelPlus { + } + + @Configuration + @Import({ThirdLevel.class, InitBean.class}) + static class SecondLevel { + + @Bean + TestBean n() { + return new TestBean(); + } + } + + @Configuration + @DependsOn("org.springframework.context.annotation.configuration.ImportTests$InitBean") + static class ThirdLevel { + + ThirdLevel() { + assertThat(InitBean.initialized).isTrue(); + } + + @Bean + ITestBean thirdLevelA() { + return new TestBean(); + } + + @Bean + ITestBean thirdLevelB() { + return new TestBean(); + } + + @Bean + ITestBean thirdLevelC() { + return new TestBean(); + } + } + + static class InitBean { + + static boolean initialized = false; + + InitBean() { + initialized = true; + } + } + + @Configuration + @Import({LeftConfig.class, RightConfig.class}) + static class WithMultipleArgumentsToImportAnnotation { + + @Bean + TestBean m() { + return new TestBean(); + } + } + + @Configuration + static class LeftConfig { + + @Bean + ITestBean left() { + return new TestBean(); + } + } + + @Configuration + static class RightConfig { + + @Bean + ITestBean right() { + return new TestBean(); + } + } + + @Configuration + @Import(NonConfigAnnotated.class) + static class ConfigAnnotated { } + + static class NonConfigAnnotated { } + + @Configuration("config-a") + static class A { } + + @Configuration("config-b") + @Import(A.class) + static class B { } + + record ImportedBean(String name) { + } + + @Configuration + static class ImportedConfig { + + @Bean + ImportedBean importedBean() { + return new ImportedBean("imported"); + } + } + + @Configuration + static class OverridingImportedConfig { + + @Bean + ImportedBean importedBean() { + return new ImportedBean("from class"); + } + } + + @Import(ImportedConfig.class) + interface ConfigImportMarker { + } + + @Configuration + static class InterfaceBasedConfig implements ConfigImportMarker { + } + + @Configuration + @Import(OverridingImportedConfig.class) + static class OverridingConfig implements ConfigImportMarker { + } + } 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 f9d0574d55..bc36d8fd46 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 @@ import org.springframework.context.support.GenericApplicationContext; * @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/aot/ContextAotProcessorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ContextAotProcessorTests.java index 28901e94d5..aee1446448 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/ContextAotProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ContextAotProcessorTests.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. @@ -45,8 +45,9 @@ class ContextAotProcessorTests { void processGeneratesAssets(@TempDir Path directory) { GenericApplicationContext context = new AnnotationConfigApplicationContext(); context.registerBean(SampleApplication.class); - ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory); + DemoContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory); ClassName className = processor.process(); + assertThat(processor.context.isClosed()).isTrue(); assertThat(className).isEqualTo(ClassName.get(SampleApplication.class.getPackageName(), "ContextAotProcessorTests_SampleApplication__ApplicationContextInitializer")); assertThat(directory).satisfies(hasGeneratedAssetsForSampleApplication()); @@ -61,9 +62,10 @@ class ContextAotProcessorTests { Path existingSourceOutput = createExisting(sourceOutput); Path existingResourceOutput = createExisting(resourceOutput); Path existingClassOutput = createExisting(classOutput); - ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, + DemoContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, sourceOutput, resourceOutput, classOutput); processor.process(); + assertThat(processor.context.isClosed()).isTrue(); assertThat(existingSourceOutput).doesNotExist(); assertThat(existingResourceOutput).doesNotExist(); assertThat(existingClassOutput).doesNotExist(); @@ -73,13 +75,14 @@ class ContextAotProcessorTests { void processWithEmptyNativeImageArgumentsDoesNotCreateNativeImageProperties(@TempDir Path directory) { GenericApplicationContext context = new AnnotationConfigApplicationContext(); context.registerBean(SampleApplication.class); - ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory) { + DemoContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory) { @Override protected List getDefaultNativeImageArguments(String application) { return Collections.emptyList(); } }; processor.process(); + assertThat(processor.context.isClosed()).isTrue(); assertThat(directory.resolve("resource/META-INF/native-image/com.example/example/native-image.properties")) .doesNotExist(); context.close(); @@ -118,6 +121,8 @@ class ContextAotProcessorTests { private static class DemoContextAotProcessor extends ContextAotProcessor { + AnnotationConfigApplicationContext context; + DemoContextAotProcessor(Class application, Path rootPath) { this(application, rootPath.resolve("source"), rootPath.resolve("resource"), rootPath.resolve("class")); } @@ -141,11 +146,12 @@ class ContextAotProcessorTests { protected GenericApplicationContext prepareApplicationContext(Class application) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(application); + this.context = context; return context; } - } + @Configuration(proxyBeanMethods = false) static class SampleApplication { @@ -153,7 +159,6 @@ class ContextAotProcessorTests { public String testBean() { return "Hello"; } - } } diff --git a/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java b/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java index 4e18f567b6..347aa9171d 100644 --- a/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java +++ b/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.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. @@ -68,7 +68,7 @@ import static org.springframework.context.expression.StandardBeanExpressionResol class ApplicationContextExpressionTests { @Test - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "removal"}) void genericApplicationContext() throws Exception { GenericApplicationContext ac = new GenericApplicationContext(); AnnotationConfigUtils.registerAnnotationConfigProcessors(ac); diff --git a/spring-context/src/test/java/org/springframework/context/support/BeanFactoryPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/support/BeanFactoryPostProcessorTests.java index 7c153e3fd4..b1cb1b8aa3 100644 --- a/spring-context/src/test/java/org/springframework/context/support/BeanFactoryPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/BeanFactoryPostProcessorTests.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. @@ -76,7 +76,7 @@ class BeanFactoryPostProcessorTests { } @Test - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "removal"}) void multipleDefinedBeanFactoryPostProcessors() { StaticApplicationContext ac = new StaticApplicationContext(); ac.registerSingleton("tb1", TestBean.class); 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 da666fea6e..1a657a7f7f 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. @@ -16,6 +16,7 @@ package org.springframework.context.support; +import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; @@ -30,6 +31,7 @@ import org.springframework.context.Lifecycle; import org.springframework.context.LifecycleProcessor; import org.springframework.context.SmartLifecycle; import org.springframework.core.testfixture.EnabledForTestGroups; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -54,10 +56,11 @@ class DefaultLifecycleProcessorTests { @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 +73,12 @@ class DefaultLifecycleProcessorTests { @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 +118,13 @@ class DefaultLifecycleProcessorTests { @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 +135,12 @@ class DefaultLifecycleProcessorTests { @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 +154,16 @@ class DefaultLifecycleProcessorTests { @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(); @@ -169,20 +176,42 @@ class DefaultLifecycleProcessorTests { context.close(); } + @Test + void singleSmartLifecycleAutoStartupWithBootstrapExecutor() { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinition beanDefinition = new RootBeanDefinition(DefaultLifecycleProcessor.class); + beanDefinition.getPropertyValues().addPropertyValue("concurrentStartupForPhases", Map.of(1, 1000)); + context.registerBeanDefinition(StaticApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME, beanDefinition); + context.registerSingleton(StaticApplicationContext.BOOTSTRAP_EXECUTOR_BEAN_NAME, ThreadPoolTaskExecutor.class); + + CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); + TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); + bean.setAutoStartup(true); + context.getBeanFactory().registerSingleton("bean", bean); + assertThat(bean.isRunning()).isFalse(); + context.refresh(); + assertThat(bean.isRunning()).isTrue(); + context.stop(); + assertThat(bean.isRunning()).isFalse(); + assertThat(startedBeans).hasSize(1); + context.close(); + } + @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 +231,17 @@ class DefaultLifecycleProcessorTests { @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 +263,17 @@ class DefaultLifecycleProcessorTests { @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 +301,17 @@ class DefaultLifecycleProcessorTests { @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 +351,7 @@ class DefaultLifecycleProcessorTests { @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 +360,6 @@ class DefaultLifecycleProcessorTests { 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 +367,7 @@ class DefaultLifecycleProcessorTests { 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 +378,12 @@ class DefaultLifecycleProcessorTests { @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 +393,11 @@ class DefaultLifecycleProcessorTests { @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 +410,7 @@ class DefaultLifecycleProcessorTests { @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 +419,6 @@ class DefaultLifecycleProcessorTests { 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 +426,7 @@ class DefaultLifecycleProcessorTests { 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 +454,18 @@ class DefaultLifecycleProcessorTests { @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 +483,7 @@ class DefaultLifecycleProcessorTests { @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 +491,6 @@ class DefaultLifecycleProcessorTests { 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 +498,7 @@ class DefaultLifecycleProcessorTests { 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 +524,18 @@ class DefaultLifecycleProcessorTests { @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 +553,7 @@ class DefaultLifecycleProcessorTests { @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 +561,6 @@ class DefaultLifecycleProcessorTests { 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 +568,7 @@ class DefaultLifecycleProcessorTests { 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 +591,16 @@ class DefaultLifecycleProcessorTests { @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 +613,20 @@ class DefaultLifecycleProcessorTests { @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 +653,7 @@ class DefaultLifecycleProcessorTests { }; } + private static class TestLifecycleBean implements Lifecycle { private final CopyOnWriteArrayList startedBeans; diff --git a/spring-context/src/test/java/org/springframework/context/support/PropertyResourceConfigurerIntegrationTests.java b/spring-context/src/test/java/org/springframework/context/support/PropertyResourceConfigurerIntegrationTests.java index 5df9f1c089..4efbe86f28 100644 --- a/spring-context/src/test/java/org/springframework/context/support/PropertyResourceConfigurerIntegrationTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/PropertyResourceConfigurerIntegrationTests.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 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; * @author Sam Brannen * @see org.springframework.beans.factory.config.PropertyResourceConfigurerTests */ -@SuppressWarnings("deprecation") +@SuppressWarnings({"deprecation", "removal"}) class PropertyResourceConfigurerIntegrationTests { @Test 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 c8bb5d5ef7..1da2ffabab 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. @@ -16,20 +16,36 @@ package org.springframework.context.support; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.Properties; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.core.SpringProperties; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.env.AbstractPropertyResolver; +import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.core.env.StandardEnvironment; @@ -38,12 +54,15 @@ import org.springframework.core.io.Resource; import org.springframework.core.testfixture.env.MockPropertySource; import org.springframework.mock.env.MockEnvironment; import org.springframework.util.PlaceholderResolutionException; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition; +import static org.springframework.core.env.AbstractPropertyResolver.DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME; /** * Tests for {@link PropertySourcesPlaceholderConfigurer}. @@ -73,6 +92,75 @@ class PropertySourcesPlaceholderConfigurerTests { assertThat(ppc.getAppliedPropertySources()).isNotNull(); } + /** + * Ensure that a {@link Converter} registered in the {@link ConversionService} + * used by the {@code Environment} is applied during placeholder resolution + * against a {@link PropertySource} registered in the {@code Environment}. + */ + @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)"); + } + + /** + * Ensure that a {@link PropertySource} added to the {@code Environment} after context + * refresh (i.e., after {@link PropertySourcesPlaceholderConfigurer#postProcessBeanFactory()} + * has been invoked) can still contribute properties in late-binding scenarios. + */ + @Test // gh-34861 + void replacementFromEnvironmentPropertiesWithLateBinding() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + MutablePropertySources propertySources = context.getEnvironment().getPropertySources(); + propertySources.addFirst(new MockPropertySource("early properties").withProperty("foo", "bar")); + + context.register(PropertySourcesPlaceholderConfigurer.class); + context.register(PrototypeBean.class); + context.refresh(); + + // Verify that placeholder resolution works for early binding. + PrototypeBean prototypeBean = context.getBean(PrototypeBean.class); + assertThat(prototypeBean.getName()).isEqualTo("bar"); + assertThat(prototypeBean.isJedi()).isFalse(); + + // Add new PropertySource after context refresh. + propertySources.addFirst(new MockPropertySource("late properties").withProperty("jedi", "true")); + + // Verify that placeholder resolution works for late binding: isJedi() switches to true. + prototypeBean = context.getBean(PrototypeBean.class); + assertThat(prototypeBean.getName()).isEqualTo("bar"); + assertThat(prototypeBean.isJedi()).isTrue(); + + // Add yet another PropertySource after context refresh. + propertySources.addFirst(new MockPropertySource("even later properties").withProperty("foo", "enigma")); + + // Verify that placeholder resolution works for even later binding: getName() switches to enigma. + prototypeBean = context.getBean(PrototypeBean.class); + assertThat(prototypeBean.getName()).isEqualTo("enigma"); + assertThat(prototypeBean.isJedi()).isTrue(); + } + @Test void localPropertiesViaResource() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); @@ -82,20 +170,35 @@ class PropertySourcesPlaceholderConfigurerTests { .getBeanDefinition()); PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); - Resource resource = new ClassPathResource("PropertySourcesPlaceholderConfigurerTests.properties", this.getClass()); + Resource resource = new ClassPathResource("PropertySourcesPlaceholderConfigurerTests.properties", getClass()); ppc.setLocation(resource); ppc.postProcessBeanFactory(bf); assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("foo"); } - @Test - void localPropertiesOverrideFalse() { - localPropertiesOverride(false); - } + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void localPropertiesOverride(boolean override) { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${foo}") + .getBeanDefinition()); - @Test - void localPropertiesOverrideTrue() { - localPropertiesOverride(true); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + + ppc.setLocalOverride(override); + ppc.setProperties(new Properties() {{ + setProperty("foo", "local"); + }}); + ppc.setEnvironment(new MockEnvironment().withProperty("foo", "enclosing")); + ppc.postProcessBeanFactory(bf); + if (override) { + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("local"); + } + else { + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("enclosing"); + } } @Test @@ -281,28 +384,58 @@ class PropertySourcesPlaceholderConfigurerTests { assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("bar"); } - @SuppressWarnings("serial") - private void localPropertiesOverride(boolean override) { + @Test // gh-34861 + void withEnumerableAndNonEnumerablePropertySourcesInTheEnvironmentAndLocalProperties() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerBeanDefinition("testBean", genericBeanDefinition(TestBean.class) - .addPropertyValue("name", "${foo}") + .addPropertyValue("name", "${foo:bogus}") + .addPropertyValue("jedi", "${local:false}") .getBeanDefinition()); - PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + // 1) MockPropertySource is an EnumerablePropertySource. + MockPropertySource mockPropertySource = new MockPropertySource("mockPropertySource") + .withProperty("foo", "${bar}"); - ppc.setLocalOverride(override); + // 2) PropertySource is not an EnumerablePropertySource. + PropertySource rawPropertySource = new PropertySource<>("rawPropertySource", new Object()) { + @Override + public Object getProperty(String key) { + return ("bar".equals(key) ? "quux" : null); + } + }; + + MockEnvironment env = new MockEnvironment(); + env.getPropertySources().addFirst(mockPropertySource); + env.getPropertySources().addLast(rawPropertySource); + + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + // 3) Local properties are stored in a PropertiesPropertySource which is an EnumerablePropertySource. ppc.setProperties(new Properties() {{ - setProperty("foo", "local"); + setProperty("local", "true"); }}); - ppc.setEnvironment(new MockEnvironment().withProperty("foo", "enclosing")); ppc.postProcessBeanFactory(bf); - if (override) { - assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("local"); - } - else { - assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("enclosing"); + + // Verify all properties can be resolved via the Environment. + assertThat(env.getProperty("foo")).isEqualTo("quux"); + assertThat(env.getProperty("bar")).isEqualTo("quux"); + + // Verify that placeholder resolution works. + TestBean testBean = bf.getBean(TestBean.class); + assertThat(testBean.getName()).isEqualTo("quux"); + assertThat(testBean.isJedi()).isTrue(); + + // Verify that the presence of a non-EnumerablePropertySource does not prevent + // accessing EnumerablePropertySources via getAppliedPropertySources(). + List propertyNames = new ArrayList<>(); + for (PropertySource propertySource : ppc.getAppliedPropertySources()) { + if (propertySource instanceof EnumerablePropertySource enumerablePropertySource) { + Collections.addAll(propertyNames, enumerablePropertySource.getPropertyNames()); + } } + // Should not contain "foo" or "bar" from the Environment. + assertThat(propertyNames).containsOnly("local"); } @Test @@ -432,6 +565,252 @@ class PropertySourcesPlaceholderConfigurerTests { } + /** + * Tests that use the escape character (or disable it) with nested placeholder + * resolution. + */ + @Nested + class EscapedNestedPlaceholdersTests { + + @Test // gh-34861 + void singleEscapeWithDefaultEscapeCharacter() { + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "admin") + .withProperty("my.property", "\\DOMAIN\\${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.postProcessBeanFactory(bf); + + // \DOMAIN\${user.home} resolves to \DOMAIN${user.home} instead of \DOMAIN\admin + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("\\DOMAIN${user.home}"); + } + + @Test // gh-34861 + void singleEscapeWithCustomEscapeCharacter() { + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "admin\\~${nested}") + .withProperty("my.property", "DOMAIN\\${user.home}\\~${enigma}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + // Set custom escape character. + ppc.setEscapeCharacter('~'); + ppc.postProcessBeanFactory(bf); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("DOMAIN\\admin\\${nested}\\${enigma}"); + } + + @Test // gh-34861 + void singleEscapeWithEscapeCharacterDisabled() { + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "admin\\") + .withProperty("my.property", "\\DOMAIN\\${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + // Disable escape character. + ppc.setEscapeCharacter(null); + ppc.postProcessBeanFactory(bf); + + // \DOMAIN\${user.home} resolves to \DOMAIN\admin + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("\\DOMAIN\\admin\\"); + } + + @Test // gh-34861 + void tripleEscapeWithDefaultEscapeCharacter() { + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "admin\\\\\\") + .withProperty("my.property", "DOMAIN\\\\\\${user.home}#${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.postProcessBeanFactory(bf); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("DOMAIN\\\\${user.home}#admin\\\\\\"); + } + + @Test // gh-34861 + void tripleEscapeWithCustomEscapeCharacter() { + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "admin\\~${enigma}") + .withProperty("my.property", "DOMAIN~~~${user.home}#${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + // Set custom escape character. + ppc.setEscapeCharacter('~'); + ppc.postProcessBeanFactory(bf); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("DOMAIN~~${user.home}#admin\\${enigma}"); + } + + @Test // gh-34861 + void singleEscapeWithDefaultEscapeCharacterAndIgnoreUnresolvablePlaceholders() { + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "${enigma}") + .withProperty("my.property", "\\${DOMAIN}${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.setIgnoreUnresolvablePlaceholders(true); + ppc.postProcessBeanFactory(bf); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("${DOMAIN}${enigma}"); + } + + @Test // gh-34861 + void singleEscapeWithCustomEscapeCharacterAndIgnoreUnresolvablePlaceholders() { + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "${enigma}") + .withProperty("my.property", "~${DOMAIN}\\${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + // Set custom escape character. + ppc.setEscapeCharacter('~'); + ppc.setIgnoreUnresolvablePlaceholders(true); + ppc.postProcessBeanFactory(bf); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("${DOMAIN}\\${enigma}"); + } + + @Test // gh-34861 + void tripleEscapeWithDefaultEscapeCharacterAndIgnoreUnresolvablePlaceholders() { + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "${enigma}") + .withProperty("my.property", "X:\\\\\\${DOMAIN}${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.setIgnoreUnresolvablePlaceholders(true); + ppc.postProcessBeanFactory(bf); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("X:\\\\${DOMAIN}${enigma}"); + } + + private static DefaultListableBeanFactory createBeanFactory() { + BeanDefinition beanDefinition = genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.property}") + .getBeanDefinition(); + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean",beanDefinition); + return bf; + } + + } + + + /** + * Tests that globally set the default escape character (or disable it) and + * rely on nested placeholder resolution. + */ + @Nested + class GlobalDefaultEscapeCharacterTests { + + private static final Field defaultEscapeCharacterField = + ReflectionUtils.findField(AbstractPropertyResolver.class, "defaultEscapeCharacter"); + + static { + ReflectionUtils.makeAccessible(defaultEscapeCharacterField); + } + + + @BeforeEach + void resetStateBeforeEachTest() { + resetState(); + } + + @AfterAll + static void resetState() { + ReflectionUtils.setField(defaultEscapeCharacterField, null, Character.MIN_VALUE); + setSpringProperty(null); + } + + + @Test // gh-34865 + void defaultEscapeCharacterSetToXyz() { + setSpringProperty("XYZ"); + + assertThatIllegalArgumentException() + .isThrownBy(PropertySourcesPlaceholderConfigurer::new) + .withMessage("Value [XYZ] for property [%s] must be a single character or an empty string", + DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME); + } + + @Test // gh-34865 + void defaultEscapeCharacterDisabled() { + setSpringProperty(""); + + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "admin") + .withProperty("my.property", "\\DOMAIN\\${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.postProcessBeanFactory(bf); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("\\DOMAIN\\admin"); + } + + @Test // gh-34865 + void defaultEscapeCharacterSetToBackslash() { + setSpringProperty("\\"); + + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "admin") + .withProperty("my.property", "\\DOMAIN\\${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.postProcessBeanFactory(bf); + + // \DOMAIN\${user.home} resolves to \DOMAIN${user.home} instead of \DOMAIN\admin + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("\\DOMAIN${user.home}"); + } + + @Test // gh-34865 + void defaultEscapeCharacterSetToTilde() { + setSpringProperty("~"); + + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "admin\\~${nested}") + .withProperty("my.property", "DOMAIN\\${user.home}\\~${enigma}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.postProcessBeanFactory(bf); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("DOMAIN\\admin\\${nested}\\${enigma}"); + } + + private static void setSpringProperty(String value) { + SpringProperties.setProperty(DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME, value); + } + + private static DefaultListableBeanFactory createBeanFactory() { + BeanDefinition beanDefinition = genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.property}") + .getBeanDefinition(); + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean",beanDefinition); + return bf; + } + + } + + private static class OptionalTestBean { private Optional name; @@ -472,4 +851,23 @@ class PropertySourcesPlaceholderConfigurerTests { } } + @Scope(BeanDefinition.SCOPE_PROTOTYPE) + static class PrototypeBean { + + @Value("${foo:bogus}") + private String name; + + @Value("${jedi:false}") + private boolean jedi; + + + public String getName() { + return this.name; + } + + public boolean isJedi() { + return this.jedi; + } + } + } diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java index c57bc66bac..bb6d42b487 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.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. @@ -32,6 +32,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.ParameterDeclarations; import static java.time.Instant.MAX; import static java.time.Instant.MIN; @@ -91,7 +92,7 @@ class InstantFormatterTests { private static final Random random = new Random(); @Override - public final Stream provideArguments(ExtensionContext context) { + public final Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) { return provideArguments().map(Arguments::of).limit(DATA_SET_SIZE); } @@ -137,7 +138,7 @@ class InstantFormatterTests { private static final Random random = new Random(); @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) { return random.longs(DATA_SET_SIZE, Long.MIN_VALUE, Long.MAX_VALUE) .mapToObj(Instant::ofEpochMilli) .map(instant -> instant.truncatedTo(ChronoUnit.MILLIS)) 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 9a533f5635..34cde9f634 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 @@ import org.springframework.core.testfixture.env.MockPropertySource; * @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,23 @@ public class MockEnvironment extends AbstractEnvironment { /** * Set a property on the underlying {@link MockPropertySource} for this environment. + * @since 6.2.8 + * @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 + * @since 6.2.8 + * @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/kotlin/org/springframework/context/annotation/BeanRegistrarDslConfigurationTests.kt b/spring-context/src/test/kotlin/org/springframework/context/annotation/BeanRegistrarDslConfigurationTests.kt index a3558bca34..41adc23a5d 100644 --- a/spring-context/src/test/kotlin/org/springframework/context/annotation/BeanRegistrarDslConfigurationTests.kt +++ b/spring-context/src/test/kotlin/org/springframework/context/annotation/BeanRegistrarDslConfigurationTests.kt @@ -18,7 +18,6 @@ package org.springframework.context.annotation import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy -import org.assertj.core.api.ThrowableAssert import org.junit.jupiter.api.Test import org.springframework.beans.factory.BeanRegistrarDsl import org.springframework.beans.factory.InitializingBean @@ -26,6 +25,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException import org.springframework.beans.factory.config.BeanDefinition import org.springframework.beans.factory.getBean import org.springframework.beans.factory.support.RootBeanDefinition +import org.springframework.mock.env.MockEnvironment import java.util.function.Supplier /** @@ -37,22 +37,27 @@ class BeanRegistrarDslConfigurationTests { @Test fun beanRegistrar() { - val context = AnnotationConfigApplicationContext(BeanRegistrarKotlinConfiguration::class.java) + val context = AnnotationConfigApplicationContext() + context.register(BeanRegistrarKotlinConfiguration::class.java) + context.environment = MockEnvironment().withProperty("hello.world", "Hello World!") + context.refresh() assertThat(context.getBean().foo).isEqualTo(context.getBean()) assertThat(context.getBean("foo")).isEqualTo(context.getBean("fooAlias")) - assertThatThrownBy(ThrowableAssert.ThrowingCallable { context.getBean() }).isInstanceOf(NoSuchBeanDefinitionException::class.java) + assertThatThrownBy { context.getBean() }.isInstanceOf(NoSuchBeanDefinitionException::class.java) assertThat(context.getBean().initialized).isTrue() val beanDefinition = context.getBeanDefinition("bar") assertThat(beanDefinition.scope).isEqualTo(BeanDefinition.SCOPE_PROTOTYPE) assertThat(beanDefinition.isLazyInit).isTrue() assertThat(beanDefinition.description).isEqualTo("Custom description") + assertThat(context.getBean()).isEqualTo(Boo("booFactory")) } @Test fun beanRegistrarWithProfile() { val context = AnnotationConfigApplicationContext() context.register(BeanRegistrarKotlinConfiguration::class.java) - context.getEnvironment().addActiveProfile("baz") + context.environment = MockEnvironment().withProperty("hello.world", "Hello World!") + context.environment.addActiveProfile("baz") context.refresh() assertThat(context.getBean().foo).isEqualTo(context.getBean()) assertThat(context.getBean().message).isEqualTo("Hello World!") @@ -75,6 +80,7 @@ class BeanRegistrarDslConfigurationTests { class Foo data class Bar(val foo: Foo) data class Baz(val message: String = "") + data class Boo(val message: String = "") class Init : InitializingBean { var initialized: Boolean = false @@ -99,9 +105,10 @@ class BeanRegistrarDslConfigurationTests { Bar(bean()) } profile("baz") { - registerBean { Baz("Hello World!") } + registerBean { Baz(env.getRequiredProperty("hello.world")) } } registerBean() + registerBean(::booFactory, "fooFactory") }) @Configuration @@ -110,11 +117,7 @@ class BeanRegistrarDslConfigurationTests { private class GenericBeanRegistrar : BeanRegistrarDsl({ registerBean>(name = "fooSupplier") { - object: Supplier { - override fun get(): Foo { - return Foo() - } - } + Supplier { Foo() } } }) @@ -126,3 +129,5 @@ class BeanRegistrarDslConfigurationTests { register(SampleBeanRegistrar()) }) } + +fun booFactory() = BeanRegistrarDslConfigurationTests.Boo("booFactory") diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig-context.properties b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig.properties similarity index 100% rename from spring-context/src/test/resources/org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig-context.properties rename to spring-context/src/test/resources/org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig.properties diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedBridgeMethods.java b/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedBridgeMethods.java index ac2b890ad9..d734e817e7 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedBridgeMethods.java +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedBridgeMethods.java @@ -44,7 +44,7 @@ import org.jspecify.annotations.Nullable; * @deprecated This class should only be used by the runtime-hints agent when instrumenting bytecode * and is not considered public API. */ -@Deprecated +@Deprecated(since = "6.0") public abstract class InstrumentedBridgeMethods { private InstrumentedBridgeMethods() { diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/RuntimeHintsAgent.java b/spring-core-test/src/main/java/org/springframework/aot/agent/RuntimeHintsAgent.java index 1d1cf826a1..3ea8f86a24 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/agent/RuntimeHintsAgent.java +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/RuntimeHintsAgent.java @@ -43,7 +43,7 @@ import org.springframework.util.StringUtils; * @deprecated as of 7.0 in favor of the {@code -XX:MissingRegistrationReportingMode=Warn} and * {@code -XX:MissingRegistrationReportingMode=Exit} JVM flags with GraalVM. */ -@Deprecated(forRemoval = true) +@Deprecated(since = "7.0", forRemoval = true) public final class RuntimeHintsAgent { private static boolean loaded = false; diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsRecorder.java b/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsRecorder.java index b6471174ff..7184c9f908 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsRecorder.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsRecorder.java @@ -34,7 +34,7 @@ import org.springframework.util.Assert; * @deprecated as of 7.0 in favor of the {@code -XX:MissingRegistrationReportingMode=Warn} and * {@code -XX:MissingRegistrationReportingMode=Exit} JVM flags with GraalVM. */ -@Deprecated(forRemoval = true) +@Deprecated(since = "7.0", forRemoval = true) @SuppressWarnings("removal") public final class RuntimeHintsRecorder { diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/CompileWithForkedClassLoaderClassLoader.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/CompileWithForkedClassLoaderClassLoader.java index 36bf851210..e13b16ee6c 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/CompileWithForkedClassLoaderClassLoader.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/CompileWithForkedClassLoaderClassLoader.java @@ -84,8 +84,7 @@ final class CompileWithForkedClassLoaderClassLoader extends ClassLoader { try (stream) { return stream.readAllBytes(); } - catch (IOException ex) { - // ignore + catch (IOException ignored) { } } return null; diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/ResourceFiles.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/ResourceFiles.java index 870713cf5b..8608e37a24 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/ResourceFiles.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/ResourceFiles.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,11 +81,11 @@ public final class ResourceFiles implements Iterable { /** * Return a new {@link ResourceFiles} instance that merges files from * another {@link ResourceFiles} instance. - * @param ResourceFiles the instance to merge + * @param resourceFiles the instance to merge * @return a new {@link ResourceFiles} instance containing merged content */ - public ResourceFiles and(ResourceFiles ResourceFiles) { - return new ResourceFiles(this.files.and(ResourceFiles.files)); + public ResourceFiles and(ResourceFiles resourceFiles) { + return new ResourceFiles(this.files.and(resourceFiles.files)); } @Override diff --git a/spring-core/src/main/java/org/springframework/aot/hint/AbstractTypeReference.java b/spring-core/src/main/java/org/springframework/aot/hint/AbstractTypeReference.java index a70ecfcd6c..666b943223 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/AbstractTypeReference.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/AbstractTypeReference.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. @@ -79,7 +79,7 @@ public abstract class AbstractTypeReference implements TypeReference { @Override public int compareTo(TypeReference other) { - return this.getCanonicalName().compareToIgnoreCase(other.getCanonicalName()); + return getCanonicalName().compareToIgnoreCase(other.getCanonicalName()); } @Override diff --git a/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java b/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java index f5620a6832..173e75f45e 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.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. @@ -148,7 +148,8 @@ public class BindingReflectionHintsRegistrar { String companionClassName = clazz.getCanonicalName() + KOTLIN_COMPANION_SUFFIX; if (ClassUtils.isPresent(companionClassName, null)) { Class companionClass = ClassUtils.resolveClassName(companionClassName, null); - Method serializerMethod = ClassUtils.getMethodIfAvailable(companionClass, "serializer"); + Method serializerMethod = ClassUtils.getMethodIfAvailable(companionClass, "serializer", + (Class[]) null); if (serializerMethod != null) { hints.registerMethod(serializerMethod, ExecutableMode.INVOKE); } diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/substitution/Target_Introspector.java b/spring-core/src/main/java/org/springframework/aot/nativex/substitution/Target_Introspector.java index c88d6a9367..af68391c08 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/substitution/Target_Introspector.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/substitution/Target_Introspector.java @@ -48,7 +48,7 @@ final class Target_Introspector { } while (!c.getName().equals("java.lang.Object")); } } - catch (Exception exception) { + catch (Exception ignored) { } return null; } 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 102f333c07..bbbdcbaeee 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 class ReflectUtils { 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 class ReflectUtils { 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/cglib/proxy/MethodProxy.java b/spring-core/src/main/java/org/springframework/cglib/proxy/MethodProxy.java index de3e7de1b0..4c6050c8f9 100644 --- a/spring-core/src/main/java/org/springframework/cglib/proxy/MethodProxy.java +++ b/spring-core/src/main/java/org/springframework/cglib/proxy/MethodProxy.java @@ -62,8 +62,8 @@ public class MethodProxy { try { proxy.init(); } - catch (CodeGenerationException ex) { - // Ignore - to be retried when actually needed later on (possibly not at all) + catch (CodeGenerationException ignored) { + // to be retried when actually needed later on (possibly not at all) } } // SPRING PATCH END diff --git a/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java b/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java index c336fe30ed..f53847044d 100644 --- a/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java +++ b/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java @@ -19,6 +19,7 @@ package org.springframework.core; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Map; +import java.util.Objects; import kotlin.Unit; import kotlin.coroutines.CoroutineContext; @@ -131,11 +132,7 @@ public abstract class CoroutinesUtils { if (!(type.isMarkedNullable() && arg == null) && type.getClassifier() instanceof KClass kClass && KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) { - KFunction constructor = KClasses.getPrimaryConstructor(kClass); - if (!KCallablesJvm.isAccessible(constructor)) { - KCallablesJvm.setAccessible(constructor, true); - } - arg = constructor.call(arg); + arg = box(kClass, arg); } argMap.put(parameter, arg); } @@ -161,6 +158,20 @@ public abstract class CoroutinesUtils { return mono; } + private static Object box(KClass kClass, @Nullable Object arg) { + KFunction constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass)); + KType type = constructor.getParameters().get(0).getType(); + if (!(type.isMarkedNullable() && arg == null) && + type.getClassifier() instanceof KClass parameterClass && + KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) { + arg = box(parameterClass, arg); + } + if (!KCallablesJvm.isAccessible(constructor)) { + KCallablesJvm.setAccessible(constructor, true); + } + return constructor.call(arg); + } + private static Flux asFlux(Object flow) { return ReactorFlowKt.asFlux(((Flow) flow)); } diff --git a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java index 8213565599..3b77fcd1e6 100644 --- a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java +++ b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java @@ -59,9 +59,9 @@ public final class GenericTypeResolver { * @param methodParameter the method parameter specification * @param implementationClass the class to resolve type variables against * @return the corresponding generic parameter or return type - * @deprecated since 5.2 in favor of {@code methodParameter.withContainingClass(implementationClass).getParameterType()} + * @deprecated in favor of {@code methodParameter.withContainingClass(implementationClass).getParameterType()} */ - @Deprecated + @Deprecated(since = "5.2") public static Class resolveParameterType(MethodParameter methodParameter, Class implementationClass) { Assert.notNull(methodParameter, "MethodParameter must not be null"); Assert.notNull(implementationClass, "Class must not be null"); diff --git a/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java index fe7faca797..0e8493d2f0 100644 --- a/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java +++ b/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.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. @@ -41,36 +41,34 @@ public class KotlinReflectionParameterNameDiscoverer implements ParameterNameDis @Override public @Nullable String @Nullable [] getParameterNames(Method method) { - if (!KotlinDetector.isKotlinType(method.getDeclaringClass())) { - return null; - } - - try { - KFunction function = ReflectJvmMapping.getKotlinFunction(method); - return (function != null ? getParameterNames(function.getParameters()) : null); - } - catch (UnsupportedOperationException ex) { - return null; + if (KotlinDetector.isKotlinType(method.getDeclaringClass())) { + try { + KFunction function = ReflectJvmMapping.getKotlinFunction(method); + return (function != null ? getParameterNames(function.getParameters()) : null); + } + catch (UnsupportedOperationException ignored) { + } } + return null; } @Override public @Nullable String @Nullable [] getParameterNames(Constructor ctor) { - if (ctor.getDeclaringClass().isEnum() || !KotlinDetector.isKotlinType(ctor.getDeclaringClass())) { - return null; - } - - try { - KFunction function = ReflectJvmMapping.getKotlinFunction(ctor); - return (function != null ? getParameterNames(function.getParameters()) : null); - } - catch (UnsupportedOperationException ex) { - return null; + if (!ctor.getDeclaringClass().isEnum() && KotlinDetector.isKotlinType(ctor.getDeclaringClass())) { + try { + KFunction function = ReflectJvmMapping.getKotlinFunction(ctor); + if (function != null) { + return getParameterNames(function.getParameters()); + } + } + catch (UnsupportedOperationException ignored) { + } } + return null; } private @Nullable String @Nullable [] getParameterNames(List parameters) { - String[] parameterNames = parameters.stream() + @Nullable String[] parameterNames = parameters.stream() // Extension receivers of extension methods must be included as they appear as normal method parameters in Java .filter(p -> KParameter.Kind.VALUE.equals(p.getKind()) || KParameter.Kind.EXTENSION_RECEIVER.equals(p.getKind())) // extension receivers are not explicitly named, but require a name for Java interoperability diff --git a/spring-core/src/main/java/org/springframework/core/MethodParameter.java b/spring-core/src/main/java/org/springframework/core/MethodParameter.java index 8070c7b216..aa5acd48a4 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodParameter.java +++ b/spring-core/src/main/java/org/springframework/core/MethodParameter.java @@ -264,9 +264,9 @@ public class MethodParameter { /** * Increase this parameter's nesting level. * @see #getNestingLevel() - * @deprecated since 5.2 in favor of {@link #nested(Integer)} + * @deprecated in favor of {@link #nested(Integer)} */ - @Deprecated + @Deprecated(since = "5.2") public void increaseNestingLevel() { this.nestingLevel++; } @@ -274,10 +274,10 @@ public class MethodParameter { /** * Decrease this parameter's nesting level. * @see #getNestingLevel() - * @deprecated since 5.2 in favor of retaining the original MethodParameter and + * @deprecated in favor of retaining the original MethodParameter and * using {@link #nested(Integer)} if nesting is required */ - @Deprecated + @Deprecated(since = "5.2") public void decreaseNestingLevel() { getTypeIndexesPerLevel().remove(this.nestingLevel); this.nestingLevel--; @@ -307,9 +307,9 @@ public class MethodParameter { * @param typeIndex the corresponding type index * (or {@code null} for the default type index) * @see #getNestingLevel() - * @deprecated since 5.2 in favor of {@link #withTypeIndex} + * @deprecated in favor of {@link #withTypeIndex} */ - @Deprecated + @Deprecated(since = "5.2") public void setTypeIndexForCurrentLevel(int typeIndex) { getTypeIndexesPerLevel().put(this.nestingLevel, typeIndex); } @@ -429,7 +429,7 @@ public class MethodParameter { /** * Set a containing class to resolve the parameter type against. */ - @Deprecated + @Deprecated(since = "5.2") void setContainingClass(Class containingClass) { this.containingClass = containingClass; this.parameterType = null; @@ -449,7 +449,7 @@ public class MethodParameter { /** * Set a resolved (generic) parameter type. */ - @Deprecated + @Deprecated(since = "5.2") void setParameterType(@Nullable Class parameterType) { this.parameterType = parameterType; } @@ -758,9 +758,9 @@ public class MethodParameter { * @param methodOrConstructor the Method or Constructor to specify a parameter for * @param parameterIndex the index of the parameter * @return the corresponding MethodParameter instance - * @deprecated as of 5.0, in favor of {@link #forExecutable} + * @deprecated in favor of {@link #forExecutable} */ - @Deprecated + @Deprecated(since = "5.0") public static MethodParameter forMethodOrConstructor(Object methodOrConstructor, int parameterIndex) { if (!(methodOrConstructor instanceof Executable executable)) { throw new IllegalArgumentException( 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 75991b0dbf..b38f6b9e66 100644 --- a/spring-core/src/main/java/org/springframework/core/SpringProperties.java +++ b/spring-core/src/main/java/org/springframework/core/SpringProperties.java @@ -26,18 +26,20 @@ import org.jspecify.annotations.Nullable; /** * Static holder for local Spring properties, i.e. defined at the Spring library level. * - *

Reads a {@code spring.properties} file from the root of the Spring library classpath, - * and also allows for programmatically setting properties through {@link #setProperty}. - * When checking a property, local entries are being checked first, then falling back - * to JVM-level system properties through a {@link System#getProperty} check. + *

Reads a {@code spring.properties} file from the root of the classpath and + * also allows for programmatically setting properties via {@link #setProperty}. + * When retrieving properties, local entries are checked first, with JVM-level + * system properties checked next as a fallback via {@link System#getProperty}. * *

This is an alternative way to set Spring-related system properties such as - * "spring.getenv.ignore" and "spring.beaninfo.ignore", in particular for scenarios - * where JVM system properties are locked on the target platform (for example, WebSphere). - * See {@link #setFlag} for a convenient way to locally set such flags to "true". + * {@code spring.getenv.ignore} and {@code spring.beaninfo.ignore}, in particular + * for scenarios where JVM system properties are locked on the target platform + * (for example, WebSphere). See {@link #setFlag} for a convenient way to locally + * set such flags to {@code "true"}. * * @author Juergen Hoeller * @since 3.2.7 + * @see org.springframework.aot.AotDetector#AOT_ENABLED * @see org.springframework.beans.StandardBeanInfoFactory#IGNORE_BEANINFO_PROPERTY_NAME * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#STRICT_LOCKING_PROPERTY_NAME * @see org.springframework.core.env.AbstractEnvironment#IGNORE_GETENV_PROPERTY_NAME @@ -117,7 +119,18 @@ public final class SpringProperties { * @param key the property key */ public static void setFlag(String key) { - localProperties.put(key, Boolean.TRUE.toString()); + localProperties.setProperty(key, Boolean.TRUE.toString()); + } + + /** + * Programmatically set a local flag to the given value, overriding + * an entry in the {@code spring.properties} file (if any). + * @param key the property key + * @param value the associated boolean value + * @since 6.2.6 + */ + public static void setFlag(String key, boolean value) { + localProperties.setProperty(key, Boolean.toString(value)); } /** @@ -130,4 +143,18 @@ public final class SpringProperties { return Boolean.parseBoolean(getProperty(key)); } + /** + * Retrieve the flag for the given property key, returning {@code null} + * instead of {@code false} in case of no actual flag set. + * @param key the property key + * @return {@code true} if the property is set to the string "true" + * (ignoring case), {@code} false if it is set to any other value, + * {@code null} if it is not set at all + * @since 6.2.6 + */ + public static @Nullable Boolean checkFlag(String key) { + String flag = getProperty(key); + return (flag != null ? Boolean.valueOf(flag) : null); + } + } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationFilter.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationFilter.java index 9b5e4ecd2f..b54b9351eb 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationFilter.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationFilter.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. @@ -75,11 +75,10 @@ public interface AnnotationFilter { * {@link AnnotationFilter} that never matches and can be used when no * filtering is needed (allowing for any annotation types to be present). * @see #PLAIN - * @deprecated as of 5.2.6 since the {@link MergedAnnotations} model - * always ignores lang annotations according to the {@link #PLAIN} filter - * (for efficiency reasons) + * @deprecated since the {@link MergedAnnotations} model always ignores lang + * annotations according to the {@link #PLAIN} filter, for efficiency reasons */ - @Deprecated + @Deprecated(since = "5.2.6") AnnotationFilter NONE = new AnnotationFilter() { @Override public boolean matches(Annotation annotation) { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index 862cadbf0e..e17b8e12f3 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -263,9 +263,9 @@ public abstract class AnnotationUtils { * failed to resolve at runtime) * @since 4.0.8 * @see AnnotatedElement#getAnnotations() - * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + * @deprecated since it is superseded by the {@link MergedAnnotations} API */ - @Deprecated + @Deprecated(since = "5.2") public static Annotation @Nullable [] getAnnotations(AnnotatedElement annotatedElement) { try { return synthesizeAnnotationArray(annotatedElement.getAnnotations(), annotatedElement); @@ -287,9 +287,9 @@ public abstract class AnnotationUtils { * failed to resolve at runtime) * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod(Method) * @see AnnotatedElement#getAnnotations() - * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + * @deprecated since it is superseded by the {@link MergedAnnotations} API */ - @Deprecated + @Deprecated(since = "5.2") public static Annotation @Nullable [] getAnnotations(Method method) { try { return synthesizeAnnotationArray(BridgeMethodResolver.findBridgedMethod(method).getAnnotations(), method); @@ -326,9 +326,9 @@ public abstract class AnnotationUtils { * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod * @see java.lang.annotation.Repeatable * @see java.lang.reflect.AnnotatedElement#getAnnotationsByType - * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + * @deprecated since it is superseded by the {@link MergedAnnotations} API */ - @Deprecated + @Deprecated(since = "5.2") public static Set getRepeatableAnnotations(AnnotatedElement annotatedElement, Class annotationType) { @@ -363,9 +363,9 @@ public abstract class AnnotationUtils { * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod * @see java.lang.annotation.Repeatable * @see java.lang.reflect.AnnotatedElement#getAnnotationsByType - * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + * @deprecated since it is superseded by the {@link MergedAnnotations} API */ - @Deprecated + @Deprecated(since = "5.2") public static Set getRepeatableAnnotations(AnnotatedElement annotatedElement, Class annotationType, @Nullable Class containerAnnotationType) { @@ -407,9 +407,9 @@ public abstract class AnnotationUtils { * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod * @see java.lang.annotation.Repeatable * @see java.lang.reflect.AnnotatedElement#getDeclaredAnnotationsByType - * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + * @deprecated since it is superseded by the {@link MergedAnnotations} API */ - @Deprecated + @Deprecated(since = "5.2") public static Set getDeclaredRepeatableAnnotations(AnnotatedElement annotatedElement, Class annotationType) { @@ -444,9 +444,9 @@ public abstract class AnnotationUtils { * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod * @see java.lang.annotation.Repeatable * @see java.lang.reflect.AnnotatedElement#getDeclaredAnnotationsByType - * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + * @deprecated since it is superseded by the {@link MergedAnnotations} API */ - @Deprecated + @Deprecated(since = "5.2") public static Set getDeclaredRepeatableAnnotations(AnnotatedElement annotatedElement, Class annotationType, @Nullable Class containerAnnotationType) { @@ -595,9 +595,9 @@ public abstract class AnnotationUtils { * or {@code null} if not found * @see Class#isAnnotationPresent(Class) * @see Class#getDeclaredAnnotations() - * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + * @deprecated since it is superseded by the {@link MergedAnnotations} API */ - @Deprecated + @Deprecated(since = "5.2") public static @Nullable Class findAnnotationDeclaringClass( Class annotationType, @Nullable Class clazz) { @@ -631,9 +631,9 @@ public abstract class AnnotationUtils { * @since 3.2.2 * @see Class#isAnnotationPresent(Class) * @see Class#getDeclaredAnnotations() - * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + * @deprecated since it is superseded by the {@link MergedAnnotations} API */ - @Deprecated + @Deprecated(since = "5.2") public static @Nullable Class findAnnotationDeclaringClassForTypes( List> annotationTypes, @Nullable Class clazz) { @@ -684,9 +684,9 @@ public abstract class AnnotationUtils { * is present and inherited * @see Class#isAnnotationPresent(Class) * @see #isAnnotationDeclaredLocally(Class, Class) - * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + * @deprecated since it is superseded by the {@link MergedAnnotations} API */ - @Deprecated + @Deprecated(since = "5.2") public static boolean isAnnotationInherited(Class annotationType, Class clazz) { return MergedAnnotations.from(clazz, SearchStrategy.INHERITED_ANNOTATIONS) .stream(annotationType) @@ -702,9 +702,9 @@ public abstract class AnnotationUtils { * @param metaAnnotationType the type of meta-annotation to search for * @return {@code true} if such an annotation is meta-present * @since 4.2.1 - * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + * @deprecated since it is superseded by the {@link MergedAnnotations} API */ - @Deprecated + @Deprecated(since = "5.2") public static boolean isAnnotationMetaPresent(Class annotationType, @Nullable Class metaAnnotationType) { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java index 5274214e5b..a0f9a146a6 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java @@ -350,21 +350,18 @@ abstract class AnnotationsScanner { private static boolean isOverride(Method rootMethod, Method candidateMethod) { return (!Modifier.isPrivate(candidateMethod.getModifiers()) && + candidateMethod.getParameterCount() == rootMethod.getParameterCount() && candidateMethod.getName().equals(rootMethod.getName()) && hasSameParameterTypes(rootMethod, candidateMethod)); } private static boolean hasSameParameterTypes(Method rootMethod, Method candidateMethod) { - if (candidateMethod.getParameterCount() != rootMethod.getParameterCount()) { - return false; - } Class[] rootParameterTypes = rootMethod.getParameterTypes(); Class[] candidateParameterTypes = candidateMethod.getParameterTypes(); if (Arrays.equals(candidateParameterTypes, rootParameterTypes)) { return true; } - return hasSameGenericTypeParameters(rootMethod, candidateMethod, - rootParameterTypes); + return hasSameGenericTypeParameters(rootMethod, candidateMethod, rootParameterTypes); } private static boolean hasSameGenericTypeParameters( diff --git a/spring-core/src/main/java/org/springframework/core/codec/AbstractDataBufferDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/AbstractDataBufferDecoder.java index 7db7d427eb..a26d106ff2 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/AbstractDataBufferDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/AbstractDataBufferDecoder.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. @@ -97,10 +97,10 @@ public abstract class AbstractDataBufferDecoder extends AbstractDecoder { /** * How to decode a {@code DataBuffer} to the target element type. - * @deprecated as of 5.2, please implement + * @deprecated in favor of implementing * {@link #decode(DataBuffer, ResolvableType, MimeType, Map)} instead */ - @Deprecated + @Deprecated(since = "5.2") protected @Nullable T decodeDataBuffer(DataBuffer buffer, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java index 9f48679653..d7881accdc 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.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. @@ -99,6 +99,7 @@ public class DefaultConversionService extends GenericConversionService { converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry)); converterRegistry.addConverter(new FallbackObjectToStringConverter()); converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry)); + converterRegistry.addConverter(new OptionalToObjectConverter((ConversionService) converterRegistry)); } /** diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java index 49b3df34c0..3cd9adf877 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.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,13 +29,14 @@ import org.springframework.core.convert.converter.ConditionalGenericConverter; import org.springframework.util.CollectionUtils; /** - * Convert an Object to {@code java.util.Optional} if necessary using the + * Convert an Object to a {@code java.util.Optional}, if necessary using the * {@code ConversionService} to convert the source Object to the generic type * of Optional when known. * * @author Rossen Stoyanchev * @author Juergen Hoeller * @since 4.1 + * @see OptionalToObjectConverter */ final class ObjectToOptionalConverter implements ConditionalGenericConverter { diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/OptionalToObjectConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/OptionalToObjectConverter.java new file mode 100644 index 0000000000..7393473d81 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/OptionalToObjectConverter.java @@ -0,0 +1,68 @@ +/* + * 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.core.convert.support; + +import java.util.Optional; +import java.util.Set; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * Convert an {@link Optional} to an {@link Object} by unwrapping the {@code Optional}, + * using the {@link ConversionService} to convert the object contained in the + * {@code Optional} (potentially {@code null}) to the target type. + * + * @author Sam Brannen + * @since 7.0 + * @see ObjectToOptionalConverter + */ +final class OptionalToObjectConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + OptionalToObjectConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return Set.of(new ConvertiblePair(Optional.class, Object.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType.getElementTypeDescriptor(), targetType, this.conversionService); + } + + @Override + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Optional optional = (Optional) source; + Object unwrappedSource = optional.orElse(null); + TypeDescriptor unwrappedSourceType = TypeDescriptor.forObject(unwrappedSource); + return this.conversionService.convert(unwrappedSource, unwrappedSourceType, targetType); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java index fa3fa1907b..21a2d528cf 100644 --- a/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java +++ b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.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. @@ -42,7 +42,7 @@ import org.springframework.util.StringUtils; * add by default. {@code AbstractEnvironment} adds none. Subclasses should contribute * property sources through the protected {@link #customizePropertySources(MutablePropertySources)} * hook, while clients should customize using {@link ConfigurableEnvironment#getPropertySources()} - * and working against the {@link MutablePropertySources} API. + * and work against the {@link MutablePropertySources} API. * See {@link ConfigurableEnvironment} javadoc for usage examples. * * @author Chris Beams @@ -66,7 +66,7 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment { public static final String IGNORE_GETENV_PROPERTY_NAME = "spring.getenv.ignore"; /** - * Name of the property to set to specify active profiles: {@value}. + * Name of the property to specify active profiles: {@value}. *

The value may be comma delimited. *

Note that certain shell environments such as Bash disallow the use of the period * character in variable names. Assuming that Spring's {@link SystemEnvironmentPropertySource} @@ -77,7 +77,7 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment { public static final String ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active"; /** - * Name of the property to set to specify profiles that are active by default: {@value}. + * Name of the property to specify profiles that are active by default: {@value}. *

The value may be comma delimited. *

Note that certain shell environments such as Bash disallow the use of the period * character in variable names. Assuming that Spring's {@link SystemEnvironmentPropertySource} @@ -141,7 +141,7 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment { /** * Factory method used to create the {@link ConfigurablePropertyResolver} - * instance used by the Environment. + * used by this {@code Environment}. * @since 5.3.4 * @see #getPropertyResolver() */ @@ -150,8 +150,7 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment { } /** - * Return the {@link ConfigurablePropertyResolver} being used by the - * {@link Environment}. + * Return the {@link ConfigurablePropertyResolver} used by the {@code Environment}. * @since 5.3.4 * @see #createPropertyResolver(MutablePropertySources) */ @@ -319,7 +318,6 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment { } } - @Override public String[] getDefaultProfiles() { return StringUtils.toStringArray(doGetDefaultProfiles()); @@ -327,7 +325,7 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment { /** * Return the set of default profiles explicitly set via - * {@link #setDefaultProfiles(String...)} or if the current set of default profiles + * {@link #setDefaultProfiles(String...)}, or if the current set of default profiles * consists only of {@linkplain #getReservedDefaultProfiles() reserved default * profiles}, then check for the presence of {@link #doGetActiveProfilesProperty()} * and assign its value (if any) to the set of default profiles. @@ -379,7 +377,7 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment { } @Override - @Deprecated + @Deprecated(since = "5.1") public boolean acceptsProfiles(String... profiles) { Assert.notEmpty(profiles, "Must specify at least one profile"); for (String profile : profiles) { @@ -418,7 +416,7 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment { * active or default profiles. *

Subclasses may override to impose further restrictions on profile syntax. * @throws IllegalArgumentException if the profile is null, empty, whitespace-only or - * begins with the profile NOT operator (!). + * begins with the profile NOT operator (!) * @see #acceptsProfiles * @see #addActiveProfile * @see #setDefaultProfiles diff --git a/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java index fdea0e0ce2..d8da275e2f 100644 --- a/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java +++ b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.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. @@ -24,6 +24,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; +import org.springframework.core.SpringProperties; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.DefaultConversionService; @@ -37,10 +38,51 @@ import org.springframework.util.SystemPropertyUtils; * * @author Chris Beams * @author Juergen Hoeller + * @author Sam Brannen * @since 3.1 */ public abstract class AbstractPropertyResolver implements ConfigurablePropertyResolver { + /** + * JVM system property used to change the default escape character + * for property placeholder support: {@value}. + *

To configure a custom escape character, supply a string containing a + * single character (other than {@link Character#MIN_VALUE}). For example, + * supplying the following JVM system property via the command line sets the + * default escape character to {@code '@'}. + *

-Dspring.placeholder.escapeCharacter.default=@
+ *

To disable escape character support, set the value to an empty string + * — for example, by supplying the following JVM system property via + * the command line. + *

-Dspring.placeholder.escapeCharacter.default=
+ *

If the property is not set, {@code '\'} will be used as the default + * escape character. + *

May alternatively be configured via a + * {@link org.springframework.core.SpringProperties spring.properties} file + * in the root of the classpath. + * @since 6.2.7 + * @see #getDefaultEscapeCharacter() + */ + public static final String DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME = + "spring.placeholder.escapeCharacter.default"; + + /** + * Since {@code null} is a valid value for {@link #defaultEscapeCharacter}, + * this constant provides a way to represent an undefined (or not yet set) + * value. Consequently, {@link #getDefaultEscapeCharacter()} prevents the use + * of {@link Character#MIN_VALUE} as the actual escape character. + * @since 6.2.7 + */ + static final Character UNDEFINED_ESCAPE_CHARACTER = Character.MIN_VALUE; + + + /** + * Cached value for the default escape character. + * @since 6.2.7 + */ + static volatile @Nullable Character defaultEscapeCharacter = UNDEFINED_ESCAPE_CHARACTER; + + protected final Log logger = LogFactory.getLog(getClass()); private volatile @Nullable ConfigurableConversionService conversionService; @@ -57,7 +99,7 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe private @Nullable String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR; - private @Nullable Character escapeCharacter = SystemPropertyUtils.ESCAPE_CHARACTER; + private @Nullable Character escapeCharacter = getDefaultEscapeCharacter(); private final Set requiredProperties = new LinkedHashSet<>(); @@ -86,9 +128,9 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe } /** - * Set the prefix that placeholders replaced by this resolver must begin with. - *

The default is "${". - * @see org.springframework.util.SystemPropertyUtils#PLACEHOLDER_PREFIX + * {@inheritDoc} + *

The default is "${". + * @see SystemPropertyUtils#PLACEHOLDER_PREFIX */ @Override public void setPlaceholderPrefix(String placeholderPrefix) { @@ -97,9 +139,9 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe } /** - * Set the suffix that placeholders replaced by this resolver must end with. - *

The default is "}". - * @see org.springframework.util.SystemPropertyUtils#PLACEHOLDER_SUFFIX + * {@inheritDoc} + *

The default is "}". + * @see SystemPropertyUtils#PLACEHOLDER_SUFFIX */ @Override public void setPlaceholderSuffix(String placeholderSuffix) { @@ -108,11 +150,9 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe } /** - * Specify the separating character between the placeholders replaced by this - * resolver and their associated default value, or {@code null} if no such - * special character should be processed as a value separator. - *

The default is ":". - * @see org.springframework.util.SystemPropertyUtils#VALUE_SEPARATOR + * {@inheritDoc} + *

The default is {@code ":"}. + * @see SystemPropertyUtils#VALUE_SEPARATOR */ @Override public void setValueSeparator(@Nullable String valueSeparator) { @@ -120,12 +160,9 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe } /** - * Specify the escape character to use to ignore placeholder prefix - * or value separator, or {@code null} if no escaping should take - * place. - *

The default is "\". + * {@inheritDoc} + *

The default is determined by {@link #getDefaultEscapeCharacter()}. * @since 6.2 - * @see org.springframework.util.SystemPropertyUtils#ESCAPE_CHARACTER */ @Override public void setEscapeCharacter(@Nullable Character escapeCharacter) { @@ -155,7 +192,7 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe public void validateRequiredProperties() { MissingRequiredPropertiesException ex = new MissingRequiredPropertiesException(); for (String key : this.requiredProperties) { - if (this.getProperty(key) == null) { + if (getProperty(key) == null) { ex.addMissingRequiredProperty(key); } } @@ -283,4 +320,59 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe */ protected abstract @Nullable String getPropertyAsRawString(String key); + + /** + * Get the default {@linkplain #setEscapeCharacter(Character) escape character} + * to use when parsing strings for property placeholder resolution. + *

This method attempts to retrieve the default escape character configured + * via the {@value #DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME} JVM system + * property or Spring property. + *

Falls back to {@code '\'} if the property has not been set. + * @return the configured default escape character, {@code null} if escape character + * support has been disabled, or {@code '\'} if the property has not been set + * @throws IllegalArgumentException if the property is configured with an + * invalid value, such as {@link Character#MIN_VALUE} or a string containing + * more than one character + * @since 6.2.7 + * @see #DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME + * @see SystemPropertyUtils#ESCAPE_CHARACTER + * @see SpringProperties + */ + public static @Nullable Character getDefaultEscapeCharacter() throws IllegalArgumentException { + Character escapeCharacter = defaultEscapeCharacter; + if (UNDEFINED_ESCAPE_CHARACTER.equals(escapeCharacter)) { + String value = SpringProperties.getProperty(DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME); + if (value != null) { + if (value.isEmpty()) { + // Disable escape character support by default. + escapeCharacter = null; + } + else if (value.length() == 1) { + try { + // Use custom default escape character. + escapeCharacter = value.charAt(0); + } + catch (Exception ex) { + throw new IllegalArgumentException("Failed to process value [%s] for property [%s]: %s" + .formatted(value, DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME, ex.getMessage()), ex); + } + Assert.isTrue(!escapeCharacter.equals(Character.MIN_VALUE), + () -> "Value for property [%s] must not be Character.MIN_VALUE" + .formatted(DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME)); + } + else { + throw new IllegalArgumentException( + "Value [%s] for property [%s] must be a single character or an empty string" + .formatted(value, DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME)); + } + } + else { + // Use standard default value for the escape character. + escapeCharacter = SystemPropertyUtils.ESCAPE_CHARACTER; + } + defaultEscapeCharacter = escapeCharacter; + } + return escapeCharacter; + } + } 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 c3c0ba4aae..646fa30256 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-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,7 +35,10 @@ import org.springframework.util.StringUtils; * *

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/env/ConfigurableEnvironment.java b/spring-core/src/main/java/org/springframework/core/env/ConfigurableEnvironment.java index 9de866854f..34ecf66234 100644 --- a/spring-core/src/main/java/org/springframework/core/env/ConfigurableEnvironment.java +++ b/spring-core/src/main/java/org/springframework/core/env/ConfigurableEnvironment.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. @@ -137,13 +137,13 @@ public interface ConfigurableEnvironment extends Environment, ConfigurableProper Map getSystemEnvironment(); /** - * Append the given parent environment's active profiles, default profiles and + * Append the given parent environment's active profiles, default profiles, and * property sources to this (child) environment's respective collections of each. *

For any identically-named {@code PropertySource} instance existing in both * parent and child, the child instance is to be preserved and the parent instance * discarded. This has the effect of allowing overriding of property sources by the - * child as well as avoiding redundant searches through common property source types, - * for example, system environment and system properties. + * child as well as avoiding redundant searches through common property source types + * — for example, system environment and system properties. *

Active and default profile names are also filtered for duplicates, to avoid * confusion and redundant storage. *

The parent environment remains unmodified in any case. Note that any changes to diff --git a/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java index f4113d144c..851f408fb9 100644 --- a/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java +++ b/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.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. @@ -70,21 +70,23 @@ public interface ConfigurablePropertyResolver extends PropertyResolver { void setPlaceholderSuffix(String placeholderSuffix); /** - * Specify the separating character between the placeholders replaced by this - * resolver and their associated default value, or {@code null} if no such + * Set the separating character to be honored between placeholders replaced by + * this resolver and their associated default values, or {@code null} if no such * special character should be processed as a value separator. */ void setValueSeparator(@Nullable String valueSeparator); /** - * Specify the escape character to use to ignore placeholder prefix or - * value separator, or {@code null} if no escaping should take place. + * Set the escape character to use to ignore the + * {@linkplain #setPlaceholderPrefix(String) placeholder prefix} and the + * {@linkplain #setValueSeparator(String) value separator}, or {@code null} + * if no escaping should take place. * @since 6.2 */ void setEscapeCharacter(@Nullable Character escapeCharacter); /** - * Set whether to throw an exception when encountering an unresolvable placeholder + * Specify whether to throw an exception when encountering an unresolvable placeholder * nested within the value of a given property. A {@code false} value indicates strict * resolution, i.e. that an exception will be thrown. A {@code true} value indicates * that unresolvable nested placeholders should be passed through in their unresolved @@ -107,7 +109,7 @@ public interface ConfigurablePropertyResolver extends PropertyResolver { * {@link #setRequiredProperties} is present and resolves to a * non-{@code null} value. * @throws MissingRequiredPropertiesException if any of the required - * properties are not resolvable. + * properties are not resolvable */ void validateRequiredProperties() throws MissingRequiredPropertiesException; diff --git a/spring-core/src/main/java/org/springframework/core/env/Environment.java b/spring-core/src/main/java/org/springframework/core/env/Environment.java index bf83d4e7f9..15bac65698 100644 --- a/spring-core/src/main/java/org/springframework/core/env/Environment.java +++ b/spring-core/src/main/java/org/springframework/core/env/Environment.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. @@ -130,10 +130,9 @@ public interface Environment extends PropertyResolver { * @see #getDefaultProfiles * @see #matchesProfiles(String...) * @see #acceptsProfiles(Profiles) - * @deprecated as of 5.1 in favor of {@link #acceptsProfiles(Profiles)} or - * {@link #matchesProfiles(String...)} + * @deprecated in favor of {@link #acceptsProfiles(Profiles)} or {@link #matchesProfiles(String...)} */ - @Deprecated + @Deprecated(since = "5.1") boolean acceptsProfiles(String... profiles); /** diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java index e346e53041..dcc034f752 100644 --- a/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java +++ b/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -30,13 +30,13 @@ import org.jspecify.annotations.Nullable; public interface PropertyResolver { /** - * Return whether the given property key is available for resolution, - * i.e. if the value for the given key is not {@code null}. + * Determine whether the given property key is available for resolution + * — for example, if the value for the given key is not {@code null}. */ boolean containsProperty(String key); /** - * Return the property value associated with the given key, + * Resolve the property value associated with the given key, * or {@code null} if the key cannot be resolved. * @param key the property name to resolve * @see #getProperty(String, String) @@ -46,7 +46,7 @@ public interface PropertyResolver { @Nullable String getProperty(String key); /** - * Return the property value associated with the given key, or + * Resolve the property value associated with the given key, or * {@code defaultValue} if the key cannot be resolved. * @param key the property name to resolve * @param defaultValue the default value to return if no value is found @@ -56,7 +56,7 @@ public interface PropertyResolver { String getProperty(String key, String defaultValue); /** - * Return the property value associated with the given key, + * Resolve the property value associated with the given key, * or {@code null} if the key cannot be resolved. * @param key the property name to resolve * @param targetType the expected type of the property value @@ -65,7 +65,7 @@ public interface PropertyResolver { @Nullable T getProperty(String key, Class targetType); /** - * Return the property value associated with the given key, + * Resolve the property value associated with the given key, * or {@code defaultValue} if the key cannot be resolved. * @param key the property name to resolve * @param targetType the expected type of the property value @@ -75,14 +75,14 @@ public interface PropertyResolver { T getProperty(String key, Class targetType, T defaultValue); /** - * Return the property value associated with the given key (never {@code null}). + * Resolve the property value associated with the given key (never {@code null}). * @throws IllegalStateException if the key cannot be resolved * @see #getRequiredProperty(String, Class) */ String getRequiredProperty(String key) throws IllegalStateException; /** - * Return the property value associated with the given key, converted to the given + * Resolve the property value associated with the given key, converted to the given * targetType (never {@code null}). * @throws IllegalStateException if the given key cannot be resolved */ diff --git a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java index 2dcfb4f322..b65ff3faac 100644 --- a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java @@ -29,6 +29,7 @@ import java.nio.channels.ReadableByteChannel; import java.nio.file.NoSuchFileException; import java.nio.file.StandardOpenOption; import java.util.jar.JarEntry; +import java.util.jar.JarFile; import org.springframework.util.ResourceUtils; @@ -86,7 +87,17 @@ public abstract class AbstractFileResolvingResource extends AbstractResource { if (con instanceof JarURLConnection jarCon) { // For JarURLConnection, do not check content-length but rather the // existence of the entry (or the jar root in case of no entryName). - return (jarCon.getEntryName() == null || jarCon.getJarEntry() != null); + // getJarFile() called for enforced presence check of the jar file, + // throwing a NoSuchFileException otherwise (turned to false below). + JarFile jarFile = jarCon.getJarFile(); + try { + return (jarCon.getEntryName() == null || jarCon.getJarEntry() != null); + } + finally { + if (!jarCon.getUseCaches()) { + jarFile.close(); + } + } } else if (con.getContentLengthLong() > 0) { return true; diff --git a/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java b/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java index eac1fd4646..bc2594123d 100644 --- a/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java +++ b/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.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. @@ -59,7 +59,7 @@ public class DefaultResourceLoader implements ResourceLoader { /** * Create a new DefaultResourceLoader. *

ClassLoader access will happen using the thread context class loader - * at the time of actual resource access (since 5.3). For more control, pass + * at the time of actual resource access. For more control, pass * a specific ClassLoader to {@link #DefaultResourceLoader(ClassLoader)}. * @see java.lang.Thread#getContextClassLoader() */ @@ -80,7 +80,7 @@ public class DefaultResourceLoader implements ResourceLoader { * Specify the ClassLoader to load class path resources with, or {@code null} * for using the thread context class loader at the time of actual resource access. *

The default is that ClassLoader access will happen using the thread context - * class loader at the time of actual resource access (since 5.3). + * class loader at the time of actual resource access. */ public void setClassLoader(@Nullable ClassLoader classLoader) { this.classLoader = classLoader; diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferInputStream.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferInputStream.java index 33135285ae..d8bbed67e9 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferInputStream.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferInputStream.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. @@ -18,6 +18,8 @@ package org.springframework.core.io.buffer; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.util.Objects; import org.springframework.util.Assert; @@ -103,10 +105,44 @@ final class DataBufferInputStream extends InputStream { this.closed = true; } + @Override + public byte[] readNBytes(int len) throws IOException { + if (len < 0) { + throw new IllegalArgumentException("len < 0"); + } + checkClosed(); + int size = Math.min(available(), len); + byte[] out = new byte[size]; + this.dataBuffer.read(out); + return out; + } + + @Override + public long skip(long n) throws IOException { + checkClosed(); + if (n <= 0) { + return 0L; + } + int skipped = Math.min(available(), n > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) n); + this.dataBuffer.readPosition(this.dataBuffer.readPosition() + skipped); + return skipped; + } + + @Override + public long transferTo(OutputStream out) throws IOException { + Objects.requireNonNull(out, "out"); + checkClosed(); + if (available() == 0) { + return 0L; + } + byte[] buf = readAllBytes(); + out.write(buf); + return buf.length; + } + private void checkClosed() throws IOException { if (this.closed) { throw new IOException("DataBufferInputStream is closed"); } } - } diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferWrapper.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferWrapper.java index 1de857f629..cfba592732 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferWrapper.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferWrapper.java @@ -85,13 +85,13 @@ public class DataBufferWrapper implements DataBuffer { } @Override - @Deprecated + @Deprecated(since = "6.0") public DataBuffer capacity(int capacity) { return this.delegate.capacity(capacity); } @Override - @Deprecated + @Deprecated(since = "6.0") public DataBuffer ensureCapacity(int capacity) { return this.delegate.ensureCapacity(capacity); } @@ -173,13 +173,13 @@ public class DataBufferWrapper implements DataBuffer { } @Override - @Deprecated + @Deprecated(since = "6.0") public DataBuffer slice(int index, int length) { return this.delegate.slice(index, length); } @Override - @Deprecated + @Deprecated(since = "6.0") public DataBuffer retainedSlice(int index, int length) { return this.delegate.retainedSlice(index, length); } @@ -190,25 +190,25 @@ public class DataBufferWrapper implements DataBuffer { } @Override - @Deprecated + @Deprecated(since = "6.0") public ByteBuffer asByteBuffer() { return this.delegate.asByteBuffer(); } @Override - @Deprecated + @Deprecated(since = "6.0") public ByteBuffer asByteBuffer(int index, int length) { return this.delegate.asByteBuffer(index, length); } @Override - @Deprecated + @Deprecated(since = "6.0.5") public ByteBuffer toByteBuffer() { return this.delegate.toByteBuffer(); } @Override - @Deprecated + @Deprecated(since = "6.0.5") public ByteBuffer toByteBuffer(int index, int length) { return this.delegate.toByteBuffer(index, length); } diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java index fa05dd6ae1..ece053d4b7 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -180,7 +180,7 @@ public class DefaultDataBuffer implements DataBuffer { } @Override - @Deprecated + @Deprecated(since = "6.0") public DataBuffer capacity(int capacity) { setCapacity(capacity); return this; @@ -340,7 +340,7 @@ public class DefaultDataBuffer implements DataBuffer { } @Override - @Deprecated + @Deprecated(since = "6.0") public DefaultDataBuffer slice(int index, int length) { checkIndex(index, length); int oldPosition = this.byteBuffer.position(); @@ -380,13 +380,13 @@ public class DefaultDataBuffer implements DataBuffer { } @Override - @Deprecated + @Deprecated(since = "6.0") public ByteBuffer asByteBuffer() { return asByteBuffer(this.readPosition, readableByteCount()); } @Override - @Deprecated + @Deprecated(since = "6.0") public ByteBuffer asByteBuffer(int index, int length) { checkIndex(index, length); @@ -397,7 +397,7 @@ public class DefaultDataBuffer implements DataBuffer { } @Override - @Deprecated + @Deprecated(since = "6.0.5") public ByteBuffer toByteBuffer(int index, int length) { checkIndex(index, length); diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferFactory.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferFactory.java index 81ed6242bd..5572f29c07 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferFactory.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferFactory.java @@ -85,7 +85,7 @@ public class DefaultDataBufferFactory implements DataBufferFactory { @Override - @Deprecated + @Deprecated(since = "6.0") public DefaultDataBuffer allocateBuffer() { return allocateBuffer(this.defaultInitialCapacity); } diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBuffer.java index 3ce726369b..5d1c73aa95 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBuffer.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBuffer.java @@ -138,7 +138,7 @@ public final class JettyDataBuffer implements PooledDataBuffer { } @Override - @Deprecated + @Deprecated(since = "6.0") public DataBuffer capacity(int capacity) { this.delegate.capacity(capacity); return this; @@ -225,7 +225,7 @@ public final class JettyDataBuffer implements PooledDataBuffer { } @Override - @Deprecated + @Deprecated(since = "6.0") public DataBuffer slice(int index, int length) { DefaultDataBuffer delegateSlice = this.delegate.slice(index, length); if (this.chunk != null) { @@ -250,19 +250,19 @@ public final class JettyDataBuffer implements PooledDataBuffer { } @Override - @Deprecated + @Deprecated(since = "6.0") public ByteBuffer asByteBuffer() { return this.delegate.asByteBuffer(); } @Override - @Deprecated + @Deprecated(since = "6.0") public ByteBuffer asByteBuffer(int index, int length) { return this.delegate.asByteBuffer(index, length); } @Override - @Deprecated + @Deprecated(since = "6.0.5") public ByteBuffer toByteBuffer(int index, int length) { return this.delegate.toByteBuffer(index, length); } diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBufferFactory.java b/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBufferFactory.java index 02a78c0274..c22fed6a1b 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBufferFactory.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBufferFactory.java @@ -65,7 +65,7 @@ public class JettyDataBufferFactory implements DataBufferFactory { @Override - @Deprecated + @Deprecated(since = "6.0") public JettyDataBuffer allocateBuffer() { DefaultDataBuffer delegate = this.delegate.allocateBuffer(); return new JettyDataBuffer(this, delegate); diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java index 9c7d22e15b..ec695cbb79 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java @@ -135,7 +135,7 @@ public class NettyDataBuffer implements PooledDataBuffer { } @Override - @Deprecated + @Deprecated(since = "6.0") public NettyDataBuffer capacity(int capacity) { this.byteBuf.capacity(capacity); return this; @@ -255,14 +255,14 @@ public class NettyDataBuffer implements PooledDataBuffer { } @Override - @Deprecated + @Deprecated(since = "6.0") public NettyDataBuffer slice(int index, int length) { ByteBuf slice = this.byteBuf.slice(index, length); return new NettyDataBuffer(slice, this.dataBufferFactory); } @Override - @Deprecated + @Deprecated(since = "6.0") public NettyDataBuffer retainedSlice(int index, int length) { ByteBuf slice = this.byteBuf.retainedSlice(index, length); return new NettyDataBuffer(slice, this.dataBufferFactory); @@ -285,19 +285,19 @@ public class NettyDataBuffer implements PooledDataBuffer { } @Override - @Deprecated + @Deprecated(since = "6.0") public ByteBuffer asByteBuffer() { return this.byteBuf.nioBuffer(); } @Override - @Deprecated + @Deprecated(since = "6.0") public ByteBuffer asByteBuffer(int index, int length) { return this.byteBuf.nioBuffer(index, length); } @Override - @Deprecated + @Deprecated(since = "6.0.5") public ByteBuffer toByteBuffer(int index, int length) { ByteBuffer result = this.byteBuf.isDirect() ? ByteBuffer.allocateDirect(length) : diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java b/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java index 40082b4371..2f1ff84ec0 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java @@ -61,7 +61,7 @@ public class NettyDataBufferFactory implements DataBufferFactory { } @Override - @Deprecated + @Deprecated(since = "6.0") public NettyDataBuffer allocateBuffer() { ByteBuf byteBuf = this.byteBufAllocator.buffer(); return new NettyDataBuffer(byteBuf, this); 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 e2fe140ad0..8d20885cb2 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 @@ -36,6 +36,7 @@ import java.nio.file.FileSystemNotFoundException; import java.nio.file.FileSystems; import java.nio.file.FileVisitOption; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.Collections; import java.util.Enumeration; @@ -215,6 +216,8 @@ import org.springframework.util.StringUtils; */ public class PathMatchingResourcePatternResolver implements ResourcePatternResolver { + private static final Resource[] EMPTY_RESOURCE_ARRAY = {}; + private static final Log logger = LogFactory.getLog(PathMatchingResourcePatternResolver.class); /** @@ -256,6 +259,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol private PathMatcher pathMatcher = new AntPathMatcher(); + private boolean useCaches = true; + private final Map rootDirCache = new ConcurrentHashMap<>(); private final Map> jarEntriesCache = new ConcurrentHashMap<>(); @@ -328,6 +333,22 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol 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 any jar caching, at + * the {@link JarURLConnection} level as well as within this resolver instance. + *

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 + * @see #clearCache() + */ + public void setUseCaches(boolean useCaches) { + this.useCaches = useCaches; + } + @Override public Resource getResource(String location) { @@ -351,7 +372,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol // 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, @@ -395,7 +416,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol 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); } /** @@ -532,7 +553,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol Set entries = this.manifestEntriesCache; if (entries == null) { entries = getClassPathManifestEntries(); - this.manifestEntriesCache = entries; + if (this.useCaches) { + this.manifestEntriesCache = entries; + } } for (ClassPathManifestEntry entry : entries) { if (!result.contains(entry.resource()) && @@ -684,7 +707,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol if (rootDirResources == null) { // Lookup for specific directory, creating a cache entry for it. rootDirResources = getResources(rootDirPath); - this.rootDirCache.put(rootDirPath, rootDirResources); + if (this.useCaches) { + this.rootDirCache.put(rootDirPath, rootDirResources); + } } } @@ -716,7 +741,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol if (logger.isTraceEnabled()) { logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result); } - return result.toArray(new Resource[0]); + return result.toArray(EMPTY_RESOURCE_ARRAY); } /** @@ -837,6 +862,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol if (con instanceof JarURLConnection jarCon) { // Should usually be the case for traditional JAR files. + if (!this.useCaches) { + jarCon.setUseCaches(false); + } try { jarFile = jarCon.getJarFile(); jarFileUrl = jarCon.getJarFileURL().toExternalForm(); @@ -844,9 +872,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol rootEntryPath = (jarEntry != null ? jarEntry.getName() : ""); closeJarFile = !jarCon.getUseCaches(); } - catch (ZipException | FileNotFoundException ex) { + catch (ZipException | FileNotFoundException | NoSuchFileException ex) { // Happens in case of a non-jar file or in case of a cached root directory - // without specific subdirectory present, respectively. + // without the specific subdirectory present, respectively. return Collections.emptySet(); } } @@ -900,8 +928,10 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol } } } - // Cache jar entries in TreeSet for efficient searching on re-encounter. - this.jarEntriesCache.put(jarFileUrl, entriesCache); + if (this.useCaches) { + // Cache jar entries in TreeSet for efficient searching on re-encounter. + this.jarEntriesCache.put(jarFileUrl, entriesCache); + } return result; } finally { @@ -1240,7 +1270,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol } /** - * Return a alternative form of the resource, i.e. with or without a leading slash. + * Return an alternative form of the resource, i.e. with or without a leading slash. * @param path the file path (with or without a leading slash) * @return the alternative form or {@code null} */ diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java index 3a6955bd03..9c04d198b3 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.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. @@ -138,8 +138,8 @@ public abstract class PropertiesLoaderSupport { /** - * Return a merged Properties instance containing both the - * loaded properties and properties set on this FactoryBean. + * Return a merged {@link Properties} instance containing both the + * loaded properties and properties set on this component. */ protected Properties mergeProperties() throws IOException { Properties result = new Properties(); diff --git a/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java b/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java index ec69cc588e..27725dda91 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java @@ -125,13 +125,12 @@ public class SpringFactoriesLoader { * Load and instantiate the factory implementations of the given type from * {@value #FACTORIES_RESOURCE_LOCATION}, using the configured class loader * and a default argument resolver that expects a no-arg constructor. - *

The returned factories are sorted through {@link AnnotationAwareOrderComparator}. + *

The returned factories are sorted using {@link AnnotationAwareOrderComparator}. *

If a custom instantiation strategy is required, use {@code load(...)} * with a custom {@link ArgumentResolver ArgumentResolver} and/or * {@link FailureHandler FailureHandler}. - *

As of Spring Framework 5.3, if duplicate implementation class names are - * discovered for a given factory type, only one instance of the duplicated - * implementation type will be instantiated. + *

If duplicate implementation class names are discovered for a given factory + * type, only one instance of the duplicated implementation type will be instantiated. * @param factoryType the interface or abstract class representing the factory * @throws IllegalArgumentException if any factory implementation class cannot * be loaded or if an error occurs while instantiating any factory @@ -145,10 +144,9 @@ public class SpringFactoriesLoader { * Load and instantiate the factory implementations of the given type from * {@value #FACTORIES_RESOURCE_LOCATION}, using the configured class loader * and the given argument resolver. - *

The returned factories are sorted through {@link AnnotationAwareOrderComparator}. - *

As of Spring Framework 5.3, if duplicate implementation class names are - * discovered for a given factory type, only one instance of the duplicated - * implementation type will be instantiated. + *

The returned factories are sorted using {@link AnnotationAwareOrderComparator}. + *

If duplicate implementation class names are discovered for a given factory + * type, only one instance of the duplicated implementation type will be instantiated. * @param factoryType the interface or abstract class representing the factory * @param argumentResolver strategy used to resolve constructor arguments by their type * @throws IllegalArgumentException if any factory implementation class cannot @@ -163,10 +161,9 @@ public class SpringFactoriesLoader { * Load and instantiate the factory implementations of the given type from * {@value #FACTORIES_RESOURCE_LOCATION}, using the configured class loader * with custom failure handling provided by the given failure handler. - *

The returned factories are sorted through {@link AnnotationAwareOrderComparator}. - *

As of Spring Framework 5.3, if duplicate implementation class names are - * discovered for a given factory type, only one instance of the duplicated - * implementation type will be instantiated. + *

The returned factories are sorted using {@link AnnotationAwareOrderComparator}. + *

If duplicate implementation class names are discovered for a given factory + * type, only one instance of the duplicated implementation type will be instantiated. *

For any factory implementation class that cannot be loaded or error that * occurs while instantiating it, the given failure handler is called. * @param factoryType the interface or abstract class representing the factory @@ -182,10 +179,9 @@ public class SpringFactoriesLoader { * {@value #FACTORIES_RESOURCE_LOCATION}, using the configured class loader, * the given argument resolver, and custom failure handling provided by the given * failure handler. - *

The returned factories are sorted through {@link AnnotationAwareOrderComparator}. - *

As of Spring Framework 5.3, if duplicate implementation class names are - * discovered for a given factory type, only one instance of the duplicated - * implementation type will be instantiated. + *

The returned factories are sorted using {@link AnnotationAwareOrderComparator}. + *

If duplicate implementation class names are discovered for a given factory + * type, only one instance of the duplicated implementation type will be instantiated. *

For any factory implementation class that cannot be loaded or error that * occurs while instantiating it, the given failure handler is called. * @param factoryType the interface or abstract class representing the factory @@ -235,12 +231,11 @@ public class SpringFactoriesLoader { /** * Load and instantiate the factory implementations of the given type from * {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader. - *

The returned factories are sorted through {@link AnnotationAwareOrderComparator}. - *

As of Spring Framework 5.3, if duplicate implementation class names are - * discovered for a given factory type, only one instance of the duplicated - * implementation type will be instantiated. + *

The returned factories are sorted using {@link AnnotationAwareOrderComparator}. + *

If duplicate implementation class names are discovered for a given factory + * type, only one instance of the duplicated implementation type will be instantiated. *

For more advanced factory loading with {@link ArgumentResolver} or - * {@link FailureHandler} support use {@link #forDefaultResourceLocation(ClassLoader)} + * {@link FailureHandler} support, use {@link #forDefaultResourceLocation(ClassLoader)} * to obtain a {@link SpringFactoriesLoader} instance. * @param factoryType the interface or abstract class representing the factory * @param classLoader the ClassLoader to use for loading (can be {@code null} @@ -256,9 +251,8 @@ public class SpringFactoriesLoader { * Load the fully qualified class names of factory implementations of the * given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given * class loader. - *

As of Spring Framework 5.3, if a particular implementation class name - * is discovered more than once for the given factory type, duplicates will - * be ignored. + *

If a particular implementation class name is discovered more than once + * for the given factory type, duplicates will be ignored. * @param factoryType the interface or abstract class representing the factory * @param classLoader the ClassLoader to use for loading resources; can be * {@code null} to use the default @@ -444,8 +438,7 @@ public class SpringFactoriesLoader { return constructor; } } - catch (UnsupportedOperationException ex) { - // ignore + catch (UnsupportedOperationException ignored) { } return null; } diff --git a/spring-core/src/main/java/org/springframework/core/retry/DefaultRetryPolicy.java b/spring-core/src/main/java/org/springframework/core/retry/DefaultRetryPolicy.java new file mode 100644 index 0000000000..e37c5ddad5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/DefaultRetryPolicy.java @@ -0,0 +1,167 @@ +/* + * 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.core.retry; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Set; +import java.util.StringJoiner; +import java.util.function.Predicate; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; + +/** + * Default {@link RetryPolicy} created by {@link RetryPolicy.Builder}. + * + * @author Sam Brannen + * @author Mahmoud Ben Hassine + * @since 7.0 + */ +class DefaultRetryPolicy implements RetryPolicy { + + private final int maxAttempts; + + private final @Nullable Duration maxDuration; + + private final Set> includes; + + private final Set> excludes; + + private final @Nullable Predicate predicate; + + + DefaultRetryPolicy(int maxAttempts, @Nullable Duration maxDuration, Set> includes, + Set> excludes, @Nullable Predicate predicate) { + + Assert.isTrue((maxAttempts > 0 || maxDuration != null), "Max attempts or max duration must be specified"); + + this.maxAttempts = maxAttempts; + this.maxDuration = maxDuration; + this.includes = includes; + this.excludes = excludes; + this.predicate = predicate; + } + + + @Override + public RetryExecution start() { + return new DefaultRetryPolicyExecution(); + } + + @Override + public String toString() { + StringJoiner result = new StringJoiner(", ", "DefaultRetryPolicy[", "]"); + if (this.maxAttempts > 0) { + result.add("maxAttempts=" + this.maxAttempts); + } + if (this.maxDuration != null) { + result.add("maxDuration=" + this.maxDuration.toMillis() + "ms"); + } + if (!this.includes.isEmpty()) { + result.add("includes=" + names(this.includes)); + } + if (!this.excludes.isEmpty()) { + result.add("excludes=" + names(this.excludes)); + } + if (this.predicate != null) { + result.add("predicate=" + this.predicate.getClass().getSimpleName()); + } + return result.toString(); + } + + + private static String names(Set> types) { + StringJoiner result = new StringJoiner(", ", "[", "]"); + for (Class type : types) { + String name = type.getCanonicalName(); + result.add(name != null? name : type.getName()); + } + return result.toString(); + } + + + /** + * {@link RetryExecution} for {@link DefaultRetryPolicy}. + */ + private class DefaultRetryPolicyExecution implements RetryExecution { + + private final LocalDateTime retryStartTime = LocalDateTime.now(); + + private int retryCount; + + + @Override + public boolean shouldRetry(Throwable throwable) { + if (DefaultRetryPolicy.this.maxAttempts > 0 && + this.retryCount++ >= DefaultRetryPolicy.this.maxAttempts) { + return false; + } + if (DefaultRetryPolicy.this.maxDuration != null) { + Duration retryDuration = Duration.between(this.retryStartTime, LocalDateTime.now()); + if (retryDuration.compareTo(DefaultRetryPolicy.this.maxDuration) > 0) { + return false; + } + } + if (!DefaultRetryPolicy.this.excludes.isEmpty()) { + for (Class excludedType : DefaultRetryPolicy.this.excludes) { + if (excludedType.isInstance(throwable)) { + return false; + } + } + } + if (!DefaultRetryPolicy.this.includes.isEmpty()) { + boolean included = false; + for (Class includedType : DefaultRetryPolicy.this.includes) { + if (includedType.isInstance(throwable)) { + included = true; + break; + } + } + if (!included) { + return false; + } + } + return DefaultRetryPolicy.this.predicate == null || DefaultRetryPolicy.this.predicate.test(throwable); + } + + @Override + public String toString() { + StringJoiner result = new StringJoiner(", ", "DefaultRetryPolicyExecution[", "]"); + if (DefaultRetryPolicy.this.maxAttempts > 0) { + result.add("maxAttempts=" + DefaultRetryPolicy.this.maxAttempts); + result.add("retryCount=" + this.retryCount); + } + if (DefaultRetryPolicy.this.maxDuration != null) { + result.add("maxDuration=" + DefaultRetryPolicy.this.maxDuration.toMillis() + "ms"); + result.add("retryStartTime=" + this.retryStartTime); + } + if (!DefaultRetryPolicy.this.includes.isEmpty()) { + result.add("includes=" + names(DefaultRetryPolicy.this.includes)); + } + if (!DefaultRetryPolicy.this.excludes.isEmpty()) { + result.add("excludes=" + names(DefaultRetryPolicy.this.excludes)); + } + if (DefaultRetryPolicy.this.predicate != null) { + result.add("predicate=" + DefaultRetryPolicy.this.predicate.getClass().getSimpleName()); + } + return result.toString(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryException.java b/spring-core/src/main/java/org/springframework/core/retry/RetryException.java new file mode 100644 index 0000000000..93c46c19f3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryException.java @@ -0,0 +1,51 @@ +/* + * 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.core.retry; + +import java.io.Serial; + +/** + * Exception thrown when a {@link RetryPolicy} has been exhausted. + * + * @author Mahmoud Ben Hassine + * @since 7.0 + * @see RetryOperations + */ +public class RetryException extends Exception { + + @Serial + private static final long serialVersionUID = 5439915454935047936L; + + + /** + * Create a new {@code RetryException} for the supplied message. + * @param message the detail message + */ + public RetryException(String message) { + super(message); + } + + /** + * Create a new {@code RetryException} for the supplied message and cause. + * @param message the detail message + * @param cause the root cause + */ + public RetryException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryExecution.java b/spring-core/src/main/java/org/springframework/core/retry/RetryExecution.java new file mode 100644 index 0000000000..e0d7e84b90 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryExecution.java @@ -0,0 +1,40 @@ +/* + * 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.core.retry; + +/** + * Strategy interface to define a retry execution created for a given + * {@link RetryPolicy}. + * + *

A {@code RetryExecution} is effectively an executable instance of a given + * {@code RetryPolicy}. + * + *

Implementations may be stateful but do not need to be thread-safe. + * + * @author Mahmoud Ben Hassine + * @since 7.0 + */ +public interface RetryExecution { + + /** + * Specify if the operation should be retried based on the given throwable. + * @param throwable the exception that caused the operation to fail + * @return {@code true} if the operation should be retried, {@code false} otherwise + */ + boolean shouldRetry(Throwable throwable); + +} diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryListener.java b/spring-core/src/main/java/org/springframework/core/retry/RetryListener.java new file mode 100644 index 0000000000..c575746b02 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryListener.java @@ -0,0 +1,66 @@ +/* + * 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.core.retry; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.retry.support.CompositeRetryListener; + +/** + * An extension point that allows to inject code during key retry phases. + * + *

Typically registered in a {@link RetryTemplate}, and can be composed using + * a {@link CompositeRetryListener}. + * + * @author Mahmoud Ben Hassine + * @since 7.0 + * @see CompositeRetryListener + */ +public interface RetryListener { + + /** + * Called before every retry attempt. + * @param retryExecution the retry execution + */ + default void beforeRetry(RetryExecution retryExecution) { + } + + /** + * Called after the first successful retry attempt. + * @param retryExecution the retry execution + * @param result the result of the {@link Retryable} + */ + default void onRetrySuccess(RetryExecution retryExecution, @Nullable Object result) { + } + + /** + * Called every time a retry attempt fails. + * @param retryExecution the retry execution + * @param throwable the exception thrown by the {@link Retryable} + */ + default void onRetryFailure(RetryExecution retryExecution, Throwable throwable) { + } + + /** + * Called if the {@link RetryPolicy} is exhausted. + * @param retryExecution the retry execution + * @param throwable the last exception thrown by the {@link Retryable} + */ + default void onRetryPolicyExhaustion(RetryExecution retryExecution, Throwable throwable) { + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryOperations.java b/spring-core/src/main/java/org/springframework/core/retry/RetryOperations.java new file mode 100644 index 0000000000..45d99c7e5c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryOperations.java @@ -0,0 +1,46 @@ +/* + * 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.core.retry; + +import org.jspecify.annotations.Nullable; + +/** + * Interface specifying basic retry operations. + * + *

Implemented by {@link RetryTemplate}. Not often used directly, but a useful + * option to enhance testability, as it can easily be mocked or stubbed. + * + * @author Mahmoud Ben Hassine + * @since 7.0 + * @see RetryTemplate + */ +public interface RetryOperations { + + /** + * Execute the given {@link Retryable} (according to the {@link RetryPolicy} + * configured at the implementation level) until it succeeds, or eventually + * throw an exception if the {@code RetryPolicy} is exhausted. + * @param retryable the {@code Retryable} to execute and retry if needed + * @param the type of the result + * @return the result of the {@code Retryable}, if any + * @throws RetryException if the {@code RetryPolicy} is exhausted; exceptions + * encountered during retry attempts should be made available as suppressed + * exceptions + */ + @Nullable R execute(Retryable retryable) throws RetryException; + +} diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java b/spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java new file mode 100644 index 0000000000..cf314f1e41 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java @@ -0,0 +1,188 @@ +/* + * 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.core.retry; + +import java.time.Duration; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.function.Predicate; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; + +/** + * Strategy interface to define a retry policy. + * + *

Also provides factory methods and a fluent builder API for creating retry + * policies with common configurations. See {@link #withMaxAttempts(int)}, + * {@link #withMaxDuration(Duration)}, {@link #builder()}, and the configuration + * options in {@link Builder} for details. + * + * @author Sam Brannen + * @author Mahmoud Ben Hassine + * @since 7.0 + * @see RetryExecution + */ +public interface RetryPolicy { + + /** + * Start a new execution for this retry policy. + * @return a new {@link RetryExecution} + */ + RetryExecution start(); + + + /** + * Create a {@link RetryPolicy} configured with a maximum number of retry attempts. + * @param maxAttempts the maximum number of retry attempts; must be greater than zero + * @see Builder#maxAttempts(int) + */ + static RetryPolicy withMaxAttempts(int maxAttempts) { + return builder().maxAttempts(maxAttempts).build(); + } + + /** + * Create a {@link RetryPolicy} configured with a maximum retry {@link Duration}. + * @param maxDuration the maximum retry duration; must be positive + * @see Builder#maxDuration(Duration) + */ + static RetryPolicy withMaxDuration(Duration maxDuration) { + return builder().maxDuration(maxDuration).build(); + } + + /** + * Create a {@link Builder} to configure a {@link RetryPolicy} with common + * configuration options. + */ + static Builder builder() { + return new Builder(); + } + + + /** + * Fluent API for configuring a {@link RetryPolicy} with common configuration + * options. + */ + final class Builder { + + private int maxAttempts; + + private @Nullable Duration maxDuration; + + private final Set> includes = new LinkedHashSet<>(); + + private final Set> excludes = new LinkedHashSet<>(); + + private @Nullable Predicate predicate; + + + private Builder() { + // internal constructor + } + + + /** + * Specify the maximum number of retry attempts. + *

If a {@code maxAttempts} value has already been configured, the + * supplied value will override the existing value. + * @param maxAttempts the maximum number of retry attempts; must be + * greater than zero + * @return this {@code Builder} instance for chained method invocations + */ + public Builder maxAttempts(int maxAttempts) { + Assert.isTrue(maxAttempts > 0, "Max attempts must be greater than zero"); + this.maxAttempts = maxAttempts; + return this; + } + + /** + * Specify the maximum retry {@link Duration}. + *

If a {@code maxDuration} value has already been configured, the + * supplied value will override the existing value. + * @param maxDuration the maximum retry duration; must be positive + * @return this {@code Builder} instance for chained method invocations + */ + public Builder maxDuration(Duration maxDuration) { + Assert.isTrue(!maxDuration.isNegative() && !maxDuration.isZero(), "Max duration must be positive"); + this.maxDuration = maxDuration; + return this; + } + + /** + * Specify the types of exceptions for which the {@link RetryPolicy} + * should retry a failed operation. + *

Defaults to all exception types. + *

If included exception types have already been configured, the supplied + * types will be added to the existing list of included types. + *

This can be combined with {@link #excludes(Class...)} and + * {@link #predicate(Predicate)}. + * @param types the types of exceptions to include in the policy + * @return this {@code Builder} instance for chained method invocations + */ + @SafeVarargs // Making the method final allows us to use @SafeVarargs. + @SuppressWarnings("varargs") + public final Builder includes(Class... types) { + Collections.addAll(this.includes, types); + return this; + } + + /** + * Specify the types of exceptions for which the {@link RetryPolicy} + * should not retry a failed operation. + *

If excluded exception types have already been configured, the supplied + * types will be added to the existing list of excluded types. + *

This can be combined with {@link #includes(Class...)} and + * {@link #predicate(Predicate)}. + * @param types the types of exceptions to exclude from the policy + * @return this {@code Builder} instance for chained method invocations + */ + @SafeVarargs // Making the method final allows us to use @SafeVarargs. + @SuppressWarnings("varargs") + public final Builder excludes(Class... types) { + Collections.addAll(this.excludes, types); + return this; + } + + /** + * Specify a custom {@link Predicate} that the {@link RetryPolicy} will + * use to determine whether to retry a failed operation based on a given + * {@link Throwable}. + *

If a predicate has already been configured, the supplied predicate + * will be {@linkplain Predicate#and(Predicate) combined} with the + * existing predicate. + *

This can be combined with {@link #includes(Class...)} and + * {@link #excludes(Class...)}. + * @param predicate a custom predicate + * @return this {@code Builder} instance for chained method invocations + */ + public Builder predicate(Predicate predicate) { + this.predicate = (this.predicate != null ? this.predicate.and(predicate) : predicate); + return this; + } + + /** + * Build the {@link RetryPolicy} configured via this {@code Builder}. + */ + public RetryPolicy build() { + return new DefaultRetryPolicy(this.maxAttempts, this.maxDuration, + this.includes, this.excludes, this.predicate); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java b/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java new file mode 100644 index 0000000000..5c0871901a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java @@ -0,0 +1,214 @@ +/* + * 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.core.retry; + +import java.time.Duration; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.log.LogAccessor; +import org.springframework.util.Assert; +import org.springframework.util.backoff.BackOff; +import org.springframework.util.backoff.BackOffExecution; +import org.springframework.util.backoff.FixedBackOff; + +/** + * A basic implementation of {@link RetryOperations} that executes and potentially + * retries a {@link Retryable} operation based on a configured {@link RetryPolicy} + * and {@link BackOff} policy. + * + *

By default, a retryable operation will be retried at most 3 times with a + * fixed backoff of 1 second. + * + *

A {@link RetryListener} can be {@linkplain #setRetryListener(RetryListener) + * registered} to intercept and inject behavior during key retry phases (before a + * retry attempt, after a retry attempt, etc.). + * + *

All retry actions performed by this template are logged at debug level, using + * {@code "org.springframework.core.retry.RetryTemplate"} as the log category. + * + * @author Mahmoud Ben Hassine + * @author Sam Brannen + * @author Juergen Hoeller + * @since 7.0 + * @see RetryOperations + * @see RetryPolicy + * @see BackOff + * @see RetryListener + * @see Retryable + */ +public class RetryTemplate implements RetryOperations { + + private static final LogAccessor logger = new LogAccessor(RetryTemplate.class); + + + private RetryPolicy retryPolicy = RetryPolicy.withMaxAttempts(3); + + private BackOff backOffPolicy = new FixedBackOff(Duration.ofSeconds(1)); + + private RetryListener retryListener = new RetryListener() {}; + + + /** + * Create a new {@code RetryTemplate} with maximum 3 retry attempts and a + * fixed backoff of 1 second. + */ + public RetryTemplate() { + } + + /** + * Create a new {@code RetryTemplate} with a custom {@link RetryPolicy} and a + * fixed backoff of 1 second. + * @param retryPolicy the retry policy to use + */ + public RetryTemplate(RetryPolicy retryPolicy) { + Assert.notNull(retryPolicy, "RetryPolicy must not be null"); + this.retryPolicy = retryPolicy; + } + + /** + * Create a new {@code RetryTemplate} with a custom {@link RetryPolicy} and + * {@link BackOff} policy. + * @param retryPolicy the retry policy to use + * @param backOffPolicy the backoff policy to use + */ + public RetryTemplate(RetryPolicy retryPolicy, BackOff backOffPolicy) { + this(retryPolicy); + Assert.notNull(backOffPolicy, "BackOff policy must not be null"); + this.backOffPolicy = backOffPolicy; + } + + + /** + * Set the {@link RetryPolicy} to use. + *

Defaults to {@code RetryPolicy.withMaxAttempts(3)}. + * @param retryPolicy the retry policy to use + * @see RetryPolicy#withMaxAttempts(int) + * @see RetryPolicy#withMaxDuration(Duration) + * @see RetryPolicy#builder() + */ + public void setRetryPolicy(RetryPolicy retryPolicy) { + Assert.notNull(retryPolicy, "Retry policy must not be null"); + this.retryPolicy = retryPolicy; + } + + /** + * Set the {@link BackOff} policy to use. + *

Defaults to {@code new FixedBackOff(Duration.ofSeconds(1))}. + * @param backOffPolicy the backoff policy to use + * @see FixedBackOff + */ + public void setBackOffPolicy(BackOff backOffPolicy) { + Assert.notNull(backOffPolicy, "BackOff policy must not be null"); + this.backOffPolicy = backOffPolicy; + } + + /** + * Set the {@link RetryListener} to use. + *

If multiple listeners are needed, use a + * {@link org.springframework.core.retry.support.CompositeRetryListener}. + *

Defaults to a no-op implementation. + * @param retryListener the retry listener to use + */ + public void setRetryListener(RetryListener retryListener) { + Assert.notNull(retryListener, "Retry listener must not be null"); + this.retryListener = retryListener; + } + + /** + * Execute the supplied {@link Retryable} according to the configured retry + * and backoff policies. + *

If the {@code Retryable} succeeds, its result will be returned. Otherwise, + * a {@link RetryException} will be thrown to the caller. + * @param retryable the {@code Retryable} to execute and retry if needed + * @param the type of the result + * @return the result of the {@code Retryable}, if any + * @throws RetryException if the {@code RetryPolicy} is exhausted; exceptions + * encountered during retry attempts are available as suppressed exceptions + */ + @Override + public @Nullable R execute(Retryable retryable) throws RetryException { + String retryableName = retryable.getName(); + // Initial attempt + try { + logger.debug(() -> "Preparing to execute retryable operation '%s'".formatted(retryableName)); + R result = retryable.execute(); + logger.debug(() -> "Retryable operation '%s' completed successfully".formatted(retryableName)); + return result; + } + catch (Throwable initialException) { + logger.debug(initialException, + () -> "Execution of retryable operation '%s' failed; initiating the retry process" + .formatted(retryableName)); + // Retry process starts here + RetryExecution retryExecution = this.retryPolicy.start(); + BackOffExecution backOffExecution = this.backOffPolicy.start(); + Deque exceptions = new ArrayDeque<>(); + exceptions.add(initialException); + + Throwable retryException = initialException; + while (retryExecution.shouldRetry(retryException)) { + try { + long duration = backOffExecution.nextBackOff(); + if (duration == BackOffExecution.STOP) { + break; + } + logger.debug(() -> "Backing off for %dms after retryable operation '%s'" + .formatted(duration, retryableName)); + Thread.sleep(duration); + } + catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + throw new RetryException( + "Unable to back off for retryable operation '%s'".formatted(retryableName), + interruptedException); + } + logger.debug(() -> "Preparing to retry operation '%s'".formatted(retryableName)); + try { + this.retryListener.beforeRetry(retryExecution); + R result = retryable.execute(); + this.retryListener.onRetrySuccess(retryExecution, result); + logger.debug(() -> "Retryable operation '%s' completed successfully after retry" + .formatted(retryableName)); + return result; + } + catch (Throwable currentAttemptException) { + logger.debug(() -> "Retry attempt for operation '%s' failed due to '%s'" + .formatted(retryableName, currentAttemptException)); + this.retryListener.onRetryFailure(retryExecution, currentAttemptException); + exceptions.add(currentAttemptException); + retryException = currentAttemptException; + } + } + + // The RetryPolicy has exhausted at this point, so we throw a RetryException with the + // initial exception as the cause and remaining exceptions as suppressed exceptions. + RetryException finalException = new RetryException( + "Retry policy for operation '%s' exhausted; aborting execution".formatted(retryableName), + exceptions.removeLast()); + for (Iterator it = exceptions.descendingIterator(); it.hasNext();) { + finalException.addSuppressed(it.next()); + } + this.retryListener.onRetryPolicyExhaustion(retryExecution, finalException); + throw finalException; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/retry/Retryable.java b/spring-core/src/main/java/org/springframework/core/retry/Retryable.java new file mode 100644 index 0000000000..21460b4537 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/Retryable.java @@ -0,0 +1,53 @@ +/* + * 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.core.retry; + +import org.jspecify.annotations.Nullable; + +/** + * {@code Retryable} is a functional interface that can be used to implement any + * generic block of code that can potentially be retried. + * + *

Used in conjunction with {@link RetryOperations}. + * + * @author Mahmoud Ben Hassine + * @author Sam Brannen + * @since 7.0 + * @param the type of the result + * @see RetryOperations + */ +@FunctionalInterface +public interface Retryable { + + /** + * Method to execute and retry if needed. + * @return the result of the operation + * @throws Throwable if an error occurs during the execution of the operation + */ + @Nullable R execute() throws Throwable; + + /** + * A unique, logical name for this retryable operation, used to distinguish + * between retries for different business operations. + *

Defaults to the fully-qualified class name of the implementation class. + * @return the name of this retryable operation + */ + default String getName() { + return getClass().getName(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/retry/package-info.java b/spring-core/src/main/java/org/springframework/core/retry/package-info.java new file mode 100644 index 0000000000..9c7f8598c8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/package-info.java @@ -0,0 +1,7 @@ +/** + * Main package for the core retry functionality. + */ +@NullMarked +package org.springframework.core.retry; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java b/spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java new file mode 100644 index 0000000000..8f7d99fa1b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java @@ -0,0 +1,88 @@ +/* + * 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.core.retry.support; + +import java.util.LinkedList; +import java.util.List; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.retry.RetryExecution; +import org.springframework.core.retry.RetryListener; +import org.springframework.core.retry.RetryTemplate; +import org.springframework.util.Assert; + +/** + * A composite implementation of the {@link RetryListener} interface. + * Delegate listeners will be called in their registration order. + * + *

This class is used to compose multiple listeners within a {@link RetryTemplate}. + * + * @author Mahmoud Ben Hassine + * @since 7.0 + */ +public class CompositeRetryListener implements RetryListener { + + private final List listeners = new LinkedList<>(); + + + /** + * Create a new {@code CompositeRetryListener}. + */ + public CompositeRetryListener() { + } + + /** + * Create a new {@code CompositeRetryListener} with the supplied list of + * delegates. + * @param listeners the list of delegate listeners to register; must not be empty + */ + public CompositeRetryListener(List listeners) { + Assert.notEmpty(listeners, "RetryListener List must not be empty"); + this.listeners.addAll(listeners); + } + + /** + * Add a new listener to the list of delegates. + * @param listener the listener to add + */ + public void addListener(RetryListener listener) { + this.listeners.add(listener); + } + + + @Override + public void beforeRetry(RetryExecution retryExecution) { + this.listeners.forEach(retryListener -> retryListener.beforeRetry(retryExecution)); + } + + @Override + public void onRetrySuccess(RetryExecution retryExecution, @Nullable Object result) { + this.listeners.forEach(listener -> listener.onRetrySuccess(retryExecution, result)); + } + + @Override + public void onRetryFailure(RetryExecution retryExecution, Throwable throwable) { + this.listeners.forEach(listener -> listener.onRetryFailure(retryExecution, throwable)); + } + + @Override + public void onRetryPolicyExhaustion(RetryExecution retryExecution, Throwable throwable) { + this.listeners.forEach(listener -> listener.onRetryPolicyExhaustion(retryExecution, throwable)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/retry/support/package-info.java b/spring-core/src/main/java/org/springframework/core/retry/support/package-info.java new file mode 100644 index 0000000000..9a42d8400c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/support/package-info.java @@ -0,0 +1,7 @@ +/** + * Support package for the core retry functionality. + */ +@NullMarked +package org.springframework.core.retry.support; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java index 39cc0b5cb6..d4e7268f3e 100644 --- a/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java +++ b/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.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. @@ -46,16 +46,16 @@ public interface AsyncTaskExecutor extends TaskExecutor { /** * Constant that indicates immediate execution. - * @deprecated as of 5.3.16 along with {@link #execute(Runnable, long)} + * @deprecated along with {@link #execute(Runnable, long)} */ - @Deprecated + @Deprecated(since = "5.3.16") long TIMEOUT_IMMEDIATE = 0; /** * Constant that indicates no time limit. - * @deprecated as of 5.3.16 along with {@link #execute(Runnable, long)} + * @deprecated along with {@link #execute(Runnable, long)} */ - @Deprecated + @Deprecated(since = "5.3.16") long TIMEOUT_INDEFINITE = Long.MAX_VALUE; @@ -72,9 +72,9 @@ public interface AsyncTaskExecutor extends TaskExecutor { * of the timeout (i.e. it cannot be started in time) * @throws TaskRejectedException if the given task was not accepted * @see #execute(Runnable) - * @deprecated as of 5.3.16 since the common executors do not support start timeouts + * @deprecated since the common executors do not support start timeouts */ - @Deprecated + @Deprecated(since = "5.3.16") default void execute(Runnable task, long startTimeout) { execute(task); } diff --git a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java index 1d7e821a70..d34a6bf107 100644 --- a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java +++ b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java @@ -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,8 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator private @Nullable Set activeThreads; + private boolean rejectTasksWhenLimitReached = false; + private volatile boolean active = true; @@ -184,6 +186,17 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator this.activeThreads = (timeout > 0 ? ConcurrentHashMap.newKeySet() : null); } + /** + * Specify whether to reject tasks when the concurrency limit has been reached, + * throwing {@link TaskRejectedException} on any further submission attempts. + *

The default is {@code false}, blocking the caller until the submission can + * be accepted. Switch this to {@code true} for immediate rejection instead. + * @since 6.2.6 + */ + public void setRejectTasksWhenLimitReached(boolean rejectTasksWhenLimitReached) { + this.rejectTasksWhenLimitReached = rejectTasksWhenLimitReached; + } + /** * Set the maximum number of parallel task executions allowed. * The default of -1 indicates no concurrency limit at all. @@ -251,7 +264,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator * @see #TIMEOUT_IMMEDIATE * @see #doExecute(Runnable) */ - @Deprecated + @Deprecated(since = "5.3.16") @Override public void execute(Runnable task, long startTimeout) { Assert.notNull(task, "Runnable must not be null"); @@ -350,13 +363,21 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator * making {@code beforeAccess()} and {@code afterAccess()} * visible to the surrounding class. */ - private static class ConcurrencyThrottleAdapter extends ConcurrencyThrottleSupport { + private class ConcurrencyThrottleAdapter extends ConcurrencyThrottleSupport { @Override protected void beforeAccess() { super.beforeAccess(); } + @Override + protected void onLimitReached() { + if (rejectTasksWhenLimitReached) { + throw new TaskRejectedException("Concurrency limit reached: " + getConcurrencyLimit()); + } + super.onLimitReached(); + } + @Override protected void afterAccess() { super.afterAccess(); diff --git a/spring-core/src/main/java/org/springframework/core/task/TaskTimeoutException.java b/spring-core/src/main/java/org/springframework/core/task/TaskTimeoutException.java index ddec24acb6..fc03e1d92d 100644 --- a/spring-core/src/main/java/org/springframework/core/task/TaskTimeoutException.java +++ b/spring-core/src/main/java/org/springframework/core/task/TaskTimeoutException.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. @@ -23,9 +23,9 @@ package org.springframework.core.task; * @author Juergen Hoeller * @since 2.0.3 * @see AsyncTaskExecutor#execute(Runnable, long) - * @deprecated as of 5.3.16 since the common executors do not support start timeouts + * @deprecated since the common executors do not support start timeouts */ -@Deprecated +@Deprecated(since = "5.3.16") @SuppressWarnings("serial") public class TaskTimeoutException extends TaskRejectedException { diff --git a/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java b/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java index ec6064992c..9bf5b5eac9 100644 --- a/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java @@ -58,9 +58,9 @@ public class StandardAnnotationMetadata extends StandardClassMetadata implements * Create a new {@code StandardAnnotationMetadata} wrapper for the given Class. * @param introspectedClass the Class to introspect * @see #StandardAnnotationMetadata(Class, boolean) - * @deprecated since 5.2 in favor of the factory method {@link AnnotationMetadata#introspect(Class)} + * @deprecated in favor of the factory method {@link AnnotationMetadata#introspect(Class)} */ - @Deprecated + @Deprecated(since = "5.2") public StandardAnnotationMetadata(Class introspectedClass) { this(introspectedClass, false); } @@ -75,12 +75,12 @@ public class StandardAnnotationMetadata extends StandardClassMetadata implements * {@link org.springframework.core.annotation.AnnotationAttributes} for compatibility * with ASM-based {@link AnnotationMetadata} implementations * @since 3.1.1 - * @deprecated since 5.2 in favor of the factory method {@link AnnotationMetadata#introspect(Class)}. + * @deprecated in favor of the factory method {@link AnnotationMetadata#introspect(Class)}. * Use {@link MergedAnnotation#asMap(org.springframework.core.annotation.MergedAnnotation.Adapt...) MergedAnnotation.asMap} * from {@link #getAnnotations()} rather than {@link #getAnnotationAttributes(String)} * if {@code nestedAnnotationsAsMap} is {@code false} */ - @Deprecated + @Deprecated(since = "5.2") public StandardAnnotationMetadata(Class introspectedClass, boolean nestedAnnotationsAsMap) { super(introspectedClass); this.mergedAnnotations = MergedAnnotations.from(introspectedClass, diff --git a/spring-core/src/main/java/org/springframework/core/type/StandardClassMetadata.java b/spring-core/src/main/java/org/springframework/core/type/StandardClassMetadata.java index 6e57d49c45..86c47696d4 100644 --- a/spring-core/src/main/java/org/springframework/core/type/StandardClassMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/StandardClassMetadata.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. @@ -40,9 +40,9 @@ public class StandardClassMetadata implements ClassMetadata { /** * Create a new StandardClassMetadata wrapper for the given Class. * @param introspectedClass the Class to introspect - * @deprecated since 5.2 in favor of {@link StandardAnnotationMetadata} + * @deprecated in favor of {@link StandardAnnotationMetadata} */ - @Deprecated + @Deprecated(since = "5.2") public StandardClassMetadata(Class introspectedClass) { Assert.notNull(introspectedClass, "Class must not be null"); this.introspectedClass = introspectedClass; diff --git a/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java b/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java index 25ae1e1976..4dedaac96e 100644 --- a/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java @@ -52,9 +52,9 @@ public class StandardMethodMetadata implements MethodMetadata { /** * Create a new StandardMethodMetadata wrapper for the given Method. * @param introspectedMethod the Method to introspect - * @deprecated since 5.2 in favor of obtaining instances via {@link AnnotationMetadata} + * @deprecated in favor of obtaining instances via {@link AnnotationMetadata} */ - @Deprecated + @Deprecated(since = "5.2") public StandardMethodMetadata(Method introspectedMethod) { this(introspectedMethod, false); } diff --git a/spring-core/src/main/java/org/springframework/lang/Contract.java b/spring-core/src/main/java/org/springframework/lang/Contract.java index 143e3b4c4c..eb6321ad85 100644 --- a/spring-core/src/main/java/org/springframework/lang/Contract.java +++ b/spring-core/src/main/java/org/springframework/lang/Contract.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,8 +18,6 @@ package org.springframework.lang; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** @@ -77,7 +75,6 @@ import java.lang.annotation.Target; * NullAway custom contract annotations */ @Documented -@Retention(RetentionPolicy.CLASS) @Target(ElementType.METHOD) public @interface Contract { diff --git a/spring-core/src/main/java/org/springframework/lang/NonNull.java b/spring-core/src/main/java/org/springframework/lang/NonNull.java index 393737c93d..8bad805feb 100644 --- a/spring-core/src/main/java/org/springframework/lang/NonNull.java +++ b/spring-core/src/main/java/org/springframework/lang/NonNull.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. @@ -42,7 +42,7 @@ import javax.annotation.meta.TypeQualifierNickname; * @author Sebastien Deleuze * @author Juergen Hoeller * @since 5.0 - * @deprecated since 7.0; use {@link org.jspecify.annotations.NonNull} instead + * @deprecated use {@link org.jspecify.annotations.NonNull} instead * @see NonNullApi * @see NonNullFields * @see Nullable @@ -52,6 +52,6 @@ import javax.annotation.meta.TypeQualifierNickname; @Documented @Nonnull @TypeQualifierNickname -@Deprecated +@Deprecated(since = "7.0") public @interface NonNull { } diff --git a/spring-core/src/main/java/org/springframework/lang/NonNullApi.java b/spring-core/src/main/java/org/springframework/lang/NonNullApi.java index 720dc4704a..54cb21ddf1 100644 --- a/spring-core/src/main/java/org/springframework/lang/NonNullApi.java +++ b/spring-core/src/main/java/org/springframework/lang/NonNullApi.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,7 @@ import javax.annotation.meta.TypeQualifierDefault; * @author Sebastien Deleuze * @author Juergen Hoeller * @since 5.0 - * @deprecated since 7.0; use {@link org.jspecify.annotations.NullMarked} instead + * @deprecated use {@link org.jspecify.annotations.NullMarked} instead * @see NonNullFields * @see Nullable * @see NonNull @@ -48,6 +48,6 @@ import javax.annotation.meta.TypeQualifierDefault; @Documented @Nonnull @TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER}) -@Deprecated +@Deprecated(since = "7.0") public @interface NonNullApi { } diff --git a/spring-core/src/main/java/org/springframework/lang/NonNullFields.java b/spring-core/src/main/java/org/springframework/lang/NonNullFields.java index 2cb03e38b0..21e3a57dfb 100644 --- a/spring-core/src/main/java/org/springframework/lang/NonNullFields.java +++ b/spring-core/src/main/java/org/springframework/lang/NonNullFields.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,7 +37,7 @@ import javax.annotation.meta.TypeQualifierDefault; * * @author Sebastien Deleuze * @since 5.0 - * @deprecated since 7.0; use {@link org.jspecify.annotations.NullMarked} instead + * @deprecated use {@link org.jspecify.annotations.NullMarked} instead * @see NonNullApi * @see Nullable * @see NonNull @@ -47,6 +47,6 @@ import javax.annotation.meta.TypeQualifierDefault; @Documented @Nonnull @TypeQualifierDefault(ElementType.FIELD) -@Deprecated +@Deprecated(since = "7.0") public @interface NonNullFields { } diff --git a/spring-core/src/main/java/org/springframework/lang/Nullable.java b/spring-core/src/main/java/org/springframework/lang/Nullable.java index 4b1b401cd7..95b8713c0d 100644 --- a/spring-core/src/main/java/org/springframework/lang/Nullable.java +++ b/spring-core/src/main/java/org/springframework/lang/Nullable.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. @@ -42,7 +42,7 @@ import javax.annotation.meta.TypeQualifierNickname; * @author Sebastien Deleuze * @author Juergen Hoeller * @since 5.0 - * @deprecated since 7.0; use {@link org.jspecify.annotations.Nullable} instead + * @deprecated use {@link org.jspecify.annotations.Nullable} instead * @see NonNullApi * @see NonNullFields * @see NonNull @@ -52,6 +52,6 @@ import javax.annotation.meta.TypeQualifierNickname; @Documented @CheckForNull @TypeQualifierNickname -@Deprecated +@Deprecated(since = "7.0") public @interface Nullable { } diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index 1e15e291c8..b5a933c1d5 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -54,6 +54,8 @@ import java.util.regex.Pattern; import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; + /** * Miscellaneous {@code java.lang.Class} utility methods. * @@ -246,6 +248,7 @@ public abstract class ClassUtils { * @param classLoaderToUse the actual ClassLoader to use for the thread context * @return the original thread context ClassLoader, or {@code null} if not overridden */ + @Contract("null -> null") public static @Nullable ClassLoader overrideThreadContextClassLoader(@Nullable ClassLoader classLoaderToUse) { Thread currentThread = Thread.currentThread(); ClassLoader threadContextClassLoader = currentThread.getContextClassLoader(); @@ -386,6 +389,7 @@ public abstract class ClassUtils { * @param classLoader the ClassLoader to check against * (can be {@code null} in which case this method will always return {@code true}) */ + @Contract("_, null -> true") public static boolean isVisible(Class clazz, @Nullable ClassLoader classLoader) { if (classLoader == null) { return true; @@ -473,6 +477,7 @@ public abstract class ClassUtils { * @return the primitive class, or {@code null} if the name does not denote * a primitive class or primitive array class */ + @Contract("null -> null") public static @Nullable Class resolvePrimitiveClassName(@Nullable String name) { Class result = null; // Most class names will be quite long, considering that they @@ -552,6 +557,7 @@ public abstract class ClassUtils { * @see Void * @see Void#TYPE */ + @Contract("null -> false") public static boolean isVoidType(@Nullable Class type) { return (type == void.class || type == Void.class); } @@ -614,10 +620,11 @@ public abstract class ClassUtils { Class resolvedPrimitive = primitiveWrapperTypeMap.get(rhsType); return (lhsType == resolvedPrimitive); } - else { + else if (rhsType.isPrimitive()) { Class resolvedWrapper = primitiveTypeToWrapperMap.get(rhsType); return (resolvedWrapper != null && lhsType.isAssignableFrom(resolvedWrapper)); } + return false; } /** @@ -860,6 +867,7 @@ public abstract class ClassUtils { * given classes is {@code null}, the other class will be returned. * @since 3.2.6 */ + @Contract("null, _ -> param2; _, null -> param1") public static @Nullable Class determineCommonAncestor(@Nullable Class clazz1, @Nullable Class clazz2) { if (clazz1 == null) { return clazz2; @@ -938,10 +946,10 @@ public abstract class ClassUtils { * Check whether the given object is a CGLIB proxy. * @param object the object to check * @see org.springframework.aop.support.AopUtils#isCglibProxy(Object) - * @deprecated as of 5.2, in favor of custom (possibly narrower) checks + * @deprecated in favor of custom (possibly narrower) checks * such as for a Spring AOP proxy */ - @Deprecated + @Deprecated(since = "5.2") public static boolean isCglibProxy(Object object) { return isCglibProxyClass(object.getClass()); } @@ -950,10 +958,11 @@ public abstract class ClassUtils { * Check whether the specified class is a CGLIB-generated class. * @param clazz the class to check * @see #getUserClass(Class) - * @deprecated as of 5.2, in favor of custom (possibly narrower) checks + * @deprecated in favor of custom (possibly narrower) checks * or simply a check for containing {@link #CGLIB_CLASS_SEPARATOR} */ - @Deprecated + @Deprecated(since = "5.2") + @Contract("null -> false") public static boolean isCglibProxyClass(@Nullable Class clazz) { return (clazz != null && isCglibProxyClassName(clazz.getName())); } @@ -962,10 +971,11 @@ public abstract class ClassUtils { * Check whether the specified class name is a CGLIB-generated class. * @param className the class name to check * @see #CGLIB_CLASS_SEPARATOR - * @deprecated as of 5.2, in favor of custom (possibly narrower) checks + * @deprecated in favor of custom (possibly narrower) checks * or simply a check for containing {@link #CGLIB_CLASS_SEPARATOR} */ - @Deprecated + @Deprecated(since = "5.2") + @Contract("null -> false") public static boolean isCglibProxyClassName(@Nullable String className) { return (className != null && className.contains(CGLIB_CLASS_SEPARATOR)); } @@ -1006,6 +1016,7 @@ public abstract class ClassUtils { * @param value the value to introspect * @return the qualified name of the class */ + @Contract("null -> null") public static @Nullable String getDescriptiveType(@Nullable Object value) { if (value == null) { return null; @@ -1029,6 +1040,7 @@ public abstract class ClassUtils { * @param clazz the class to check * @param typeName the type name to match */ + @Contract("_, null -> false") public static boolean matchesTypeName(Class clazz, @Nullable String typeName) { return (typeName != null && (typeName.equals(clazz.getTypeName()) || typeName.equals(clazz.getSimpleName()))); @@ -1387,7 +1399,7 @@ public abstract class ClassUtils { * @see #getPubliclyAccessibleMethodIfPossible(Method, Class) * @deprecated in favor of {@link #getInterfaceMethodIfPossible(Method, Class)} */ - @Deprecated + @Deprecated(since = "5.2") public static Method getInterfaceMethodIfPossible(Method method) { return getInterfaceMethodIfPossible(method, null); } diff --git a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java index f92169d5f2..bab4651257 100644 --- a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java @@ -45,6 +45,7 @@ import org.springframework.lang.Contract; * @author Juergen Hoeller * @author Rob Harrop * @author Arjen Poutsma + * @author Sam Brannen * @since 1.1.3 */ public abstract class CollectionUtils { @@ -103,7 +104,7 @@ public abstract class CollectionUtils { *

This differs from the regular {@link LinkedHashMap} constructor * which takes an initial capacity relative to a load factor but is * aligned with Spring's own {@link LinkedCaseInsensitiveMap} and - * {@link LinkedMultiValueMap} constructor semantics as of 5.3. + * {@link LinkedMultiValueMap} constructor semantics. * @param expectedSize the expected number of elements (with a corresponding * capacity to be derived so that no resize/rehash operations are needed) * @since 5.3 @@ -195,12 +196,15 @@ public abstract class CollectionUtils { /** - * Check whether the given Iterator contains the given element. - * @param iterator the Iterator to check + * Check whether the given {@link Iterator} contains the given element. + * @param iterator the {@code Iterator} to check * @param element the element to look for * @return {@code true} if found, {@code false} otherwise */ - public static boolean contains(@Nullable Iterator iterator, Object element) { + @Contract("null, _ -> false") + public static boolean contains(@Nullable Iterator iterator, + @Nullable Object element) { + if (iterator != null) { while (iterator.hasNext()) { Object candidate = iterator.next(); @@ -213,12 +217,15 @@ public abstract class CollectionUtils { } /** - * Check whether the given Enumeration contains the given element. - * @param enumeration the Enumeration to check + * Check whether the given {@link Enumeration} contains the given element. + * @param enumeration the {@code Enumeration} to check * @param element the element to look for * @return {@code true} if found, {@code false} otherwise */ - public static boolean contains(@Nullable Enumeration enumeration, Object element) { + @Contract("null, _ -> false") + public static boolean contains(@Nullable Enumeration enumeration, + @Nullable Object element) { + if (enumeration != null) { while (enumeration.hasMoreElements()) { Object candidate = enumeration.nextElement(); @@ -231,14 +238,17 @@ public abstract class CollectionUtils { } /** - * Check whether the given Collection contains the given element instance. + * Check whether the given {@link Collection} contains the given element instance. *

Enforces the given instance to be present, rather than returning * {@code true} for an equal element as well. - * @param collection the Collection to check + * @param collection the {@code Collection} to check * @param element the element to look for * @return {@code true} if found, {@code false} otherwise */ - public static boolean containsInstance(@Nullable Collection collection, Object element) { + @Contract("null, _ -> false") + public static boolean containsInstance(@Nullable Collection collection, + @Nullable Object element) { + if (collection != null) { for (Object candidate : collection) { if (candidate == element) { @@ -252,12 +262,22 @@ public abstract class CollectionUtils { /** * Return {@code true} if any element in '{@code candidates}' is * contained in '{@code source}'; otherwise returns {@code false}. - * @param source the source Collection + * @param source the source {@link Collection} * @param candidates the candidates to search for * @return whether any of the candidates has been found */ - public static boolean containsAny(Collection source, Collection candidates) { - return findFirstMatch(source, candidates) != null; + public static boolean containsAny(Collection source, + Collection candidates) { + + if (isEmpty(source) || isEmpty(candidates)) { + return false; + } + for (Object candidate : candidates) { + if (source.contains(candidate)) { + return true; + } + } + return false; } /** @@ -289,6 +309,7 @@ public abstract class CollectionUtils { * or {@code null} if none or more than one such value found */ @SuppressWarnings("unchecked") + @Contract("null, _ -> null") public static @Nullable T findValueOfType(@Nullable Collection collection, @Nullable Class type) { if (isEmpty(collection)) { return null; @@ -386,6 +407,7 @@ public abstract class CollectionUtils { * @see LinkedHashMap#keySet() * @see java.util.LinkedHashSet */ + @Contract("null -> null") public static @Nullable T firstElement(@Nullable Set set) { if (isEmpty(set)) { return null; @@ -408,6 +430,7 @@ public abstract class CollectionUtils { * @return the first element, or {@code null} if none * @since 5.2.3 */ + @Contract("null -> null") public static @Nullable T firstElement(@Nullable List list) { if (isEmpty(list)) { return null; @@ -425,6 +448,7 @@ public abstract class CollectionUtils { * @see LinkedHashMap#keySet() * @see java.util.LinkedHashSet */ + @Contract("null -> null") public static @Nullable T lastElement(@Nullable Set set) { if (isEmpty(set)) { return null; @@ -448,6 +472,7 @@ public abstract class CollectionUtils { * @return the last element, or {@code null} if none * @since 5.0.3 */ + @Contract("null -> null") public static @Nullable T lastElement(@Nullable List list) { if (isEmpty(list)) { return null; diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java b/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java index 46da8e430c..cf54df78e9 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.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. @@ -105,6 +105,7 @@ public abstract class ConcurrencyThrottleSupport implements Serializable { /** * To be invoked before the main execution logic of concrete subclasses. *

This implementation applies the concurrency throttle. + * @see #onLimitReached() * @see #afterAccess() */ protected void beforeAccess() { @@ -113,29 +114,12 @@ public abstract class ConcurrencyThrottleSupport implements Serializable { "Currently no invocations allowed - concurrency limit set to NO_CONCURRENCY"); } if (this.concurrencyLimit > 0) { - boolean debug = logger.isDebugEnabled(); this.concurrencyLock.lock(); try { - boolean interrupted = false; - while (this.concurrencyCount >= this.concurrencyLimit) { - if (interrupted) { - throw new IllegalStateException("Thread was interrupted while waiting for invocation access, " + - "but concurrency limit still does not allow for entering"); - } - if (debug) { - logger.debug("Concurrency count " + this.concurrencyCount + - " has reached limit " + this.concurrencyLimit + " - blocking"); - } - try { - this.concurrencyCondition.await(); - } - catch (InterruptedException ex) { - // Re-interrupt current thread, to allow other threads to react. - Thread.currentThread().interrupt(); - interrupted = true; - } + if (this.concurrencyCount >= this.concurrencyLimit) { + onLimitReached(); } - if (debug) { + if (logger.isDebugEnabled()) { logger.debug("Entering throttle at concurrency count " + this.concurrencyCount); } this.concurrencyCount++; @@ -146,6 +130,33 @@ public abstract class ConcurrencyThrottleSupport implements Serializable { } } + /** + * Triggered by {@link #beforeAccess()} when the concurrency limit has been reached. + * The default implementation blocks until the concurrency count allows for entering. + * @since 6.2.6 + */ + protected void onLimitReached() { + boolean interrupted = false; + while (this.concurrencyCount >= this.concurrencyLimit) { + if (interrupted) { + throw new IllegalStateException("Thread was interrupted while waiting for invocation access, " + + "but concurrency limit still does not allow for entering"); + } + if (logger.isDebugEnabled()) { + logger.debug("Concurrency count " + this.concurrencyCount + + " has reached limit " + this.concurrencyLimit + " - blocking"); + } + try { + this.concurrencyCondition.await(); + } + catch (InterruptedException ex) { + // Re-interrupt current thread, to allow other threads to react. + Thread.currentThread().interrupt(); + interrupted = true; + } + } + } + /** * To be invoked after the main execution logic of concrete subclasses. * @see #beforeAccess() diff --git a/spring-core/src/main/java/org/springframework/util/FileCopyUtils.java b/spring-core/src/main/java/org/springframework/util/FileCopyUtils.java index 388ab5e341..c2c729a25d 100644 --- a/spring-core/src/main/java/org/springframework/util/FileCopyUtils.java +++ b/spring-core/src/main/java/org/springframework/util/FileCopyUtils.java @@ -229,8 +229,7 @@ public abstract class FileCopyUtils { try { closeable.close(); } - catch (IOException ex) { - // ignore + catch (IOException ignored) { } } diff --git a/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java b/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java index c857588d18..2bbe365c8d 100644 --- a/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java +++ b/spring-core/src/main/java/org/springframework/util/FileSystemUtils.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. @@ -28,6 +28,8 @@ import java.util.EnumSet; import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; + import static java.nio.file.FileVisitOption.FOLLOW_LINKS; /** @@ -54,6 +56,7 @@ public abstract class FileSystemUtils { * @return {@code true} if the {@code File} was successfully deleted, * otherwise {@code false} */ + @Contract("null -> false") public static boolean deleteRecursively(@Nullable File root) { if (root == null) { return false; @@ -76,6 +79,7 @@ public abstract class FileSystemUtils { * @throws IOException in the case of I/O errors * @since 5.0 */ + @Contract("null -> false") public static boolean deleteRecursively(@Nullable Path root) throws IOException { if (root == null) { return false; diff --git a/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java b/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java index 8faf71ea1c..d383a1c922 100644 --- a/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java +++ b/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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,8 +35,7 @@ import java.util.Map; * @param the key type * @param the value element type */ -public class LinkedMultiValueMap extends MultiValueMapAdapter // new public base class in 5.3 - implements Serializable, Cloneable { +public class LinkedMultiValueMap extends MultiValueMapAdapter implements Serializable, Cloneable { private static final long serialVersionUID = 3801124242820219131L; diff --git a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java index 34a2be4e30..1496a2e838 100644 --- a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java @@ -172,6 +172,7 @@ public abstract class ObjectUtils { * if the {@code Optional} is empty, or simply the given object as-is * @since 5.0 */ + @Contract("null -> null") public static @Nullable Object unwrapOptional(@Nullable Object obj) { if (obj instanceof Optional optional) { Object result = optional.orElse(null); @@ -188,6 +189,7 @@ public abstract class ObjectUtils { * @param element the element to check for * @return whether the element has been found in the given array */ + @Contract("null, _ -> false") public static boolean containsElement(@Nullable Object @Nullable [] array, @Nullable Object element) { if (array == null) { return false; @@ -410,10 +412,10 @@ public abstract class ObjectUtils { } /** - * Return a hash code for the given object; typically the value of - * {@code Object#hashCode()}}. If the object is an array, - * this method will delegate to any of the {@code Arrays.hashCode} - * methods. If the object is {@code null}, this method returns 0. + * Return a hash code for the given object, typically the value of + * {@link Object#hashCode()}. If the object is an array, this method + * will delegate to one of the {@code Arrays.hashCode} methods. If + * the object is {@code null}, this method returns {@code 0}. * @see Object#hashCode() * @see Arrays */ 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 dc4afcb6d3..b4c0d56902 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. @@ -18,6 +18,8 @@ package org.springframework.util; import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; + /** * Utility methods for simple pattern matching, in particular for Spring's typical * {@code xxx*}, {@code *xxx}, {@code *xxx*}, and {@code xxx*yyy} pattern styles. @@ -36,14 +38,28 @@ public abstract class PatternMatchUtils { * @param str the String to match * @return whether the String matches the given pattern */ + @Contract("null, _ -> false; _, null -> false") 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 + */ + @Contract("null, _ -> false; _, null -> false") + 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 +68,43 @@ public abstract class PatternMatchUtils { } 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; } /** @@ -83,6 +117,7 @@ public abstract class PatternMatchUtils { * @param str the String to match * @return whether the String matches any of the given patterns */ + @Contract("null, _ -> false; _, null -> false") public static boolean simpleMatch(String @Nullable [] patterns, @Nullable String str) { if (patterns != null) { for (String pattern : patterns) { @@ -94,4 +129,20 @@ public abstract class PatternMatchUtils { return false; } + /** + * Variant of {@link #simpleMatch(String[], String)} that ignores upper/lower case. + * @since 6.1.20 + */ + @Contract("null, _ -> false; _, null -> false") + public static boolean simpleMatchIgnoreCase(String @Nullable [] 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/ReflectionUtils.java b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java index e5dced175e..80ad6c5ba3 100644 --- a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java @@ -29,6 +29,8 @@ import java.util.Map; import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; + /** * Simple utility class for working with the reflection API and handling * reflection exceptions. @@ -137,6 +139,7 @@ public abstract class ReflectionUtils { * @param ex the exception to rethrow * @throws RuntimeException the rethrown exception */ + @Contract("_ -> fail") public static void rethrowRuntimeException(@Nullable Throwable ex) { if (ex instanceof RuntimeException runtimeException) { throw runtimeException; @@ -158,6 +161,7 @@ public abstract class ReflectionUtils { * @param throwable the exception to rethrow * @throws Exception the rethrown exception (in case of a checked exception) */ + @Contract("_ -> fail") public static void rethrowException(@Nullable Throwable throwable) throws Exception { if (throwable instanceof Exception exception) { throw exception; @@ -501,6 +505,7 @@ public abstract class ReflectionUtils { * Determine whether the given method is an "equals" method. * @see java.lang.Object#equals(Object) */ + @Contract("null -> false") public static boolean isEqualsMethod(@Nullable Method method) { return (method != null && method.getParameterCount() == 1 && method.getName().equals("equals") && method.getParameterTypes()[0] == Object.class); @@ -510,6 +515,7 @@ public abstract class ReflectionUtils { * Determine whether the given method is a "hashCode" method. * @see java.lang.Object#hashCode() */ + @Contract("null -> false") public static boolean isHashCodeMethod(@Nullable Method method) { return (method != null && method.getParameterCount() == 0 && method.getName().equals("hashCode")); } @@ -518,6 +524,7 @@ public abstract class ReflectionUtils { * Determine whether the given method is a "toString" method. * @see java.lang.Object#toString() */ + @Contract("null -> false") public static boolean isToStringMethod(@Nullable Method method) { return (method != null && method.getParameterCount() == 0 && method.getName().equals("toString")); } @@ -525,6 +532,7 @@ public abstract class ReflectionUtils { /** * Determine whether the given method is originally declared by {@link java.lang.Object}. */ + @Contract("null -> false") public static boolean isObjectMethod(@Nullable Method method) { return (method != null && (method.getDeclaringClass() == Object.class || isEqualsMethod(method) || isHashCodeMethod(method) || isToStringMethod(method))); @@ -585,6 +593,7 @@ public abstract class ReflectionUtils { * @param type the type of the field (may be {@code null} if name is specified) * @return the corresponding Field object, or {@code null} if not found */ + @Contract("_, null, null -> fail") public static @Nullable Field findField(Class clazz, @Nullable String name, @Nullable Class type) { Assert.notNull(clazz, "Class must not be null"); Assert.isTrue(name != null || type != null, "Either name or type of the field must be specified"); diff --git a/spring-core/src/main/java/org/springframework/util/ResourceUtils.java b/spring-core/src/main/java/org/springframework/util/ResourceUtils.java index 7cf9c53cc7..ea544470ca 100644 --- a/spring-core/src/main/java/org/springframework/util/ResourceUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ResourceUtils.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. @@ -28,6 +28,8 @@ import java.util.Locale; import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; + /** * Utility methods for resolving resource locations to files in the * file system. Mainly for internal use within the framework. @@ -104,6 +106,7 @@ public abstract class ResourceUtils { * @see java.net.URL * @see #toURL(String) */ + @Contract("null -> false") public static boolean isUrl(@Nullable String resourceLocation) { if (resourceLocation == null) { return false; diff --git a/spring-core/src/main/java/org/springframework/util/SerializationUtils.java b/spring-core/src/main/java/org/springframework/util/SerializationUtils.java index a8f34a7908..79c58b1e7b 100644 --- a/spring-core/src/main/java/org/springframework/util/SerializationUtils.java +++ b/spring-core/src/main/java/org/springframework/util/SerializationUtils.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. @@ -25,6 +25,8 @@ import java.io.Serializable; import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; + /** * Static utilities for serialization and deserialization using * null") public static byte @Nullable [] serialize(@Nullable Object object) { if (object == null) { return null; @@ -72,7 +75,8 @@ public abstract class SerializationUtils { *

Prefer the use of an external tool (that serializes to JSON, XML, or * any other format) which is regularly checked and updated for not allowing RCE. */ - @Deprecated + @Deprecated(since = "6.0") + @Contract("null -> null") public static @Nullable Object deserialize(byte @Nullable [] bytes) { if (bytes == null) { return null; diff --git a/spring-core/src/main/java/org/springframework/util/StreamUtils.java b/spring-core/src/main/java/org/springframework/util/StreamUtils.java index c169d3cc77..c35c7599ab 100644 --- a/spring-core/src/main/java/org/springframework/util/StreamUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StreamUtils.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. @@ -27,12 +27,12 @@ import java.nio.charset.Charset; import org.jspecify.annotations.Nullable; -import org.springframework.lang.Contract; - /** - * Simple utility methods for dealing with streams. The copy methods of this class are - * similar to those defined in {@link FileCopyUtils} except that all affected streams are - * left open when done. All copy methods use a block size of 8192 bytes. + * Simple utility methods for dealing with streams. + * + *

The copy methods of this class are similar to those defined in + * {@link FileCopyUtils} except that all affected streams are left open when done. + * All copy methods use a block size of {@value #BUFFER_SIZE} bytes. * *

Mainly for use within the framework, but also useful for application code. * @@ -191,14 +191,14 @@ public abstract class StreamUtils { } /** - * Drain the remaining content of the given InputStream. - *

Leaves the InputStream open when done. - * @param in the InputStream to drain - * @return the number of bytes read + * Drain the remaining content of the given {@link InputStream}. + *

Leaves the {@code InputStream} open when done. + * @param in the {@code InputStream} to drain + * @return the number of bytes read, or {@code 0} if the supplied + * {@code InputStream} is {@code null} or empty * @throws IOException in case of I/O errors * @since 4.3 */ - @Contract("null -> fail") public static int drain(@Nullable InputStream in) throws IOException { if (in == null) { return 0; @@ -240,7 +240,7 @@ public abstract class StreamUtils { } - private static class NonClosingInputStream extends FilterInputStream { + private static final class NonClosingInputStream extends FilterInputStream { public NonClosingInputStream(InputStream in) { super(in); @@ -249,10 +249,30 @@ public abstract class StreamUtils { @Override public void close() throws IOException { } + + @Override + public byte[] readAllBytes() throws IOException { + return in.readAllBytes(); + } + + @Override + public byte[] readNBytes(int len) throws IOException { + return in.readNBytes(len); + } + + @Override + public int readNBytes(byte[] b, int off, int len) throws IOException { + return in.readNBytes(b, off, len); + } + + @Override + public long transferTo(OutputStream out) throws IOException { + return in.transferTo(out); + } } - private static class NonClosingOutputStream extends FilterOutputStream { + private static final class NonClosingOutputStream extends FilterOutputStream { public NonClosingOutputStream(OutputStream out) { super(out); diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index 8347831226..a8d63b1d28 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -16,7 +16,6 @@ package org.springframework.util; -import java.io.ByteArrayOutputStream; import java.nio.charset.Charset; import java.util.ArrayDeque; import java.util.ArrayList; @@ -25,6 +24,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.Enumeration; +import java.util.HexFormat; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -105,10 +105,11 @@ public abstract class StringUtils { * {@link #hasLength(String)} or {@link #hasText(String)} instead. * @param str the candidate object (possibly a {@code String}) * @since 3.2.1 - * @deprecated as of 5.3, in favor of {@link #hasLength(String)} and - * {@link #hasText(String)} (or {@link ObjectUtils#isEmpty(Object)}) + * @deprecated in favor of {@link #hasLength(String)} and {@link #hasText(String)} + * (or {@link ObjectUtils#isEmpty(Object)}) */ - @Deprecated + @Deprecated(since = "5.3") + @Contract("null -> true") public static boolean isEmpty(@Nullable Object str) { return (str == null || "".equals(str)); } @@ -210,6 +211,7 @@ public abstract class StringUtils { * contains at least 1 whitespace character * @see Character#isWhitespace */ + @Contract("null -> false") public static boolean containsWhitespace(@Nullable CharSequence str) { if (!hasLength(str)) { return false; @@ -231,6 +233,7 @@ public abstract class StringUtils { * contains at least 1 whitespace character * @see #containsWhitespace(CharSequence) */ + @Contract("null -> false") public static boolean containsWhitespace(@Nullable String str) { return containsWhitespace((CharSequence) str); } @@ -366,6 +369,7 @@ public abstract class StringUtils { * @param singleCharacter the character to compare to * @since 5.2.9 */ + @Contract("null, _ -> false") public static boolean matchesCharacter(@Nullable String str, char singleCharacter) { return (str != null && str.length() == 1 && str.charAt(0) == singleCharacter); } @@ -377,6 +381,7 @@ public abstract class StringUtils { * @param prefix the prefix to look for * @see java.lang.String#startsWith */ + @Contract("null, _ -> false; _, null -> false") public static boolean startsWithIgnoreCase(@Nullable String str, @Nullable String prefix) { return (str != null && prefix != null && str.length() >= prefix.length() && str.regionMatches(true, 0, prefix, 0, prefix.length())); @@ -389,6 +394,7 @@ public abstract class StringUtils { * @param suffix the suffix to look for * @see java.lang.String#endsWith */ + @Contract("null, _ -> false; _, null -> false") public static boolean endsWithIgnoreCase(@Nullable String str, @Nullable String suffix) { return (str != null && suffix != null && str.length() >= suffix.length() && str.regionMatches(true, str.length() - suffix.length(), suffix, 0, suffix.length())); @@ -516,6 +522,7 @@ public abstract class StringUtils { * @return the quoted {@code String} (for example, "'myString'"), * or {@code null} if the input was {@code null} */ + @Contract("null -> null; !null -> !null") public static @Nullable String quote(@Nullable String str) { return (str != null ? "'" + str + "'" : null); } @@ -527,6 +534,7 @@ public abstract class StringUtils { * @return the quoted {@code String} (for example, "'myString'"), * or the input object as-is if not a {@code String} */ + @Contract("null -> null; !null -> !null") public static @Nullable Object quoteIfString(@Nullable Object obj) { return (obj instanceof String str ? quote(str) : obj); } @@ -613,11 +621,20 @@ public abstract class StringUtils { } /** - * Extract the filename from the given Java resource path, - * for example, {@code "mypath/myfile.txt" → "myfile.txt"}. + * Extract the filename from the given Java resource path. + *

Examples: + *

* @param path the file path (may be {@code null}) - * @return the extracted filename, or {@code null} if none + * @return the extracted filename, the original path if it does not contain a + * forward slash ({@code "/"}), or {@code null} if the supplied path is {@code null} */ + @Contract("null -> null; !null -> !null") public static @Nullable String getFilename(@Nullable String path) { if (path == null) { return null; @@ -628,11 +645,22 @@ public abstract class StringUtils { } /** - * Extract the filename extension from the given Java resource path, - * for example, "mypath/myfile.txt" → "txt". + * Extract the filename extension from the given Java resource path. + *

Examples: + *

    + *
  • {@code "my/path/myfile.txt"} → {@code "txt"} + *
  • {@code "myfile.txt"} → {@code "txt"} + *
  • {@code "my/path/myfile."} → {@code ""} + *
  • {@code "myfile"} → {@code null} + *
  • {@code ""} → {@code null} + *
  • {@code null} → {@code null} + *
* @param path the file path (may be {@code null}) - * @return the extracted filename extension, or {@code null} if none + * @return the extracted filename extension (potentially an empty string), or + * {@code null} if the provided path is {@code null} or does not contain a dot + * ({@code "."}) */ + @Contract("null -> null") public static @Nullable String getFilenameExtension(@Nullable String path) { if (path == null) { return null; @@ -800,54 +828,64 @@ public abstract class StringUtils { } /** - * Decode the given encoded URI component value. Based on the following rules: - *
    - *
  • Alphanumeric characters {@code "a"} through {@code "z"}, {@code "A"} through {@code "Z"}, - * and {@code "0"} through {@code "9"} stay the same.
  • - *
  • Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.
  • - *
  • A sequence "{@code %xy}" is interpreted as a hexadecimal representation of the character.
  • - *
  • For all other characters (including those already decoded), the output is undefined.
  • - *
- * @param source the encoded String - * @param charset the character set + * Decode the given encoded URI component value by replacing each + * "{@code %xy}" sequence with a hexadecimal representation of the + * character in the specified character encoding, leaving other characters + * unmodified. + * @param source the encoded URI component value + * @param charset the character encoding to use to decode the "{@code %xy}" + * sequences * @return the decoded value - * @throws IllegalArgumentException when the given source contains invalid encoded sequences + * @throws IllegalArgumentException if the given source contains invalid encoded + * sequences * @since 5.0 - * @see java.net.URLDecoder#decode(String, String) + * @see java.net.URLDecoder#decode(String, String) java.net.URLDecoder#decode + * for HTML form decoding */ public static String uriDecode(String source, Charset charset) { int length = source.length(); - if (length == 0) { + int firstPercentIndex = source.indexOf('%'); + if (length == 0 || firstPercentIndex < 0) { return source; } - Assert.notNull(charset, "Charset must not be null"); - ByteArrayOutputStream baos = new ByteArrayOutputStream(length); - boolean changed = false; - for (int i = 0; i < length; i++) { - int ch = source.charAt(i); + StringBuilder output = new StringBuilder(length); + output.append(source, 0, firstPercentIndex); + byte[] bytes = null; + int i = firstPercentIndex; + while (i < length) { + char ch = source.charAt(i); if (ch == '%') { - if (i + 2 < length) { - char hex1 = source.charAt(i + 1); - char hex2 = source.charAt(i + 2); - int u = Character.digit(hex1, 16); - int l = Character.digit(hex2, 16); - if (u == -1 || l == -1) { - throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); + try { + if (bytes == null) { + bytes = new byte[(length - i) / 3]; } - baos.write((char) ((u << 4) + l)); - i += 2; - changed = true; + + int pos = 0; + while (i + 2 < length && ch == '%') { + bytes[pos++] = (byte) HexFormat.fromHexDigits(source, i + 1, i + 3); + i += 3; + if (i < length) { + ch = source.charAt(i); + } + } + + if (i < length && ch == '%') { + throw new IllegalArgumentException("Incomplete trailing escape (%) pattern"); + } + + output.append(new String(bytes, 0, pos, charset)); } - else { + catch (NumberFormatException ex) { throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); } } else { - baos.write(ch); + output.append(ch); + i++; } } - return (changed ? StreamUtils.copyToString(baos, charset) : source); + return output.toString(); } /** @@ -1004,6 +1042,7 @@ public abstract class StringUtils { * @param array2 the second array (can be {@code null}) * @return the new array ({@code null} if both given arrays were {@code null}) */ + @Contract("null, _ -> param2; _, null -> param1") public static String @Nullable [] concatenateStringArrays(String @Nullable [] array1, String @Nullable [] array2) { if (ObjectUtils.isEmpty(array1)) { return array2; @@ -1075,6 +1114,7 @@ public abstract class StringUtils { * index 1 being after the delimiter (neither element includes the delimiter); * or {@code null} if the delimiter wasn't found in the given input {@code String} */ + @Contract("null, _ -> null; _, null -> null") public static String @Nullable [] split(@Nullable String toSplit, @Nullable String delimiter) { if (!hasLength(toSplit) || !hasLength(delimiter)) { return null; @@ -1117,8 +1157,9 @@ public abstract class StringUtils { * @return a {@code Properties} instance representing the array contents, * or {@code null} if the array to process was {@code null} or empty */ + @Contract("null, _, _ -> null") public static @Nullable Properties splitArrayElementsIntoProperties( - String[] array, String delimiter, @Nullable String charsToDelete) { + String @Nullable [] array, String delimiter, @Nullable String charsToDelete) { if (ObjectUtils.isEmpty(array)) { return null; diff --git a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java index 802a456764..c0f5b2ae3e 100644 --- a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java +++ b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.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,16 +35,19 @@ import org.jspecify.annotations.Nullable; */ public abstract class SystemPropertyUtils { - /** Prefix for system property placeholders: {@value}. */ + /** Prefix for property placeholders: {@value}. */ public static final String PLACEHOLDER_PREFIX = "${"; - /** Suffix for system property placeholders: {@value}. */ + /** Suffix for property placeholders: {@value}. */ public static final String PLACEHOLDER_SUFFIX = "}"; - /** Value separator for system property placeholders: {@value}. */ + /** Value separator for property placeholders: {@value}. */ public static final String VALUE_SEPARATOR = ":"; - /** Default escape character: {@code '\'}. */ + /** + * Escape character for property placeholders: {@code '\'}. + * @since 6.2 + */ public static final Character ESCAPE_CHARACTER = '\\'; diff --git a/spring-core/src/main/java/org/springframework/util/TypeUtils.java b/spring-core/src/main/java/org/springframework/util/TypeUtils.java index e45d1e011d..c14007b538 100644 --- a/spring-core/src/main/java/org/springframework/util/TypeUtils.java +++ b/spring-core/src/main/java/org/springframework/util/TypeUtils.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,8 @@ import java.lang.reflect.WildcardType; import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; + /** * Utility to work with generic type parameters. * @@ -210,6 +212,7 @@ public abstract class TypeUtils { return (upperBounds.length == 0 ? IMPLICIT_UPPER_BOUNDS : upperBounds); } + @Contract("_, null -> true; null, _ -> false") public static boolean isAssignableBound(@Nullable Type lhsType, @Nullable Type rhsType) { if (rhsType == null) { return true; 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 5cd39685fa..79836518a1 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 class ExponentialBackOff implements BackOff { return this.maxAttempts; } + @Override public BackOffExecution start() { return new ExponentialBackOffExecution(); @@ -225,6 +227,7 @@ public class ExponentialBackOff implements BackOff { .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 b4d80c4812..233283ee05 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. @@ -16,6 +16,8 @@ package org.springframework.util.backoff; +import java.time.Duration; + /** * A simple {@link BackOff} implementation that provides a fixed interval * between two attempts and a maximum number of retries. @@ -35,21 +37,46 @@ public class FixedBackOff implements BackOff { */ public static final long UNLIMITED_ATTEMPTS = Long.MAX_VALUE; + private long interval = DEFAULT_INTERVAL; private long maxAttempts = UNLIMITED_ATTEMPTS; /** - * Create an instance with an interval of {@value #DEFAULT_INTERVAL} - * ms and an unlimited number of attempts. + * Create an instance with an interval of {@value #DEFAULT_INTERVAL} ms and + * an unlimited number of attempts. + * @see #setInterval(long) + * @see #setMaxAttempts(long) */ public FixedBackOff() { } /** - * Create an instance. + * Create an instance with the supplied interval and an unlimited number of + * attempts. + * @param interval the interval between two attempts in milliseconds + * @since 7.0 + * @see #setMaxAttempts(long) + */ + public FixedBackOff(long interval) { + this.interval = interval; + } + + /** + * Create an instance with the supplied interval and an unlimited number of + * attempts. * @param interval the interval between two attempts + * @since 7.0 + * @see #setMaxAttempts(long) + */ + public FixedBackOff(Duration interval) { + this.interval = interval.toMillis(); + } + + /** + * Create an instance with the supplied interval and maximum number of attempts. + * @param interval the interval between two attempts in milliseconds * @param maxAttempts the maximum number of attempts */ public FixedBackOff(long interval, long maxAttempts) { @@ -73,19 +100,20 @@ public class FixedBackOff implements BackOff { } /** - * Set the maximum number of attempts in milliseconds. + * Set the maximum number of attempts. */ public void setMaxAttempts(long maxAttempts) { this.maxAttempts = maxAttempts; } /** - * Return the maximum number of attempts in milliseconds. + * Return the maximum number of attempts. */ public long getMaxAttempts() { return this.maxAttempts; } + @Override public BackOffExecution start() { return new FixedBackOffExecution(); diff --git a/spring-core/src/main/java/org/springframework/util/function/SupplierUtils.java b/spring-core/src/main/java/org/springframework/util/function/SupplierUtils.java index e268cf5783..d593f8b408 100644 --- a/spring-core/src/main/java/org/springframework/util/function/SupplierUtils.java +++ b/spring-core/src/main/java/org/springframework/util/function/SupplierUtils.java @@ -37,7 +37,7 @@ public abstract class SupplierUtils { * @param supplier the supplier to resolve * @return the supplier's result, or {@code null} if none */ - @Contract("null -> null; !null -> !null") + @Contract("null -> null") public static @Nullable T resolve(@Nullable Supplier supplier) { return (supplier != null ? supplier.get() : null); } @@ -49,6 +49,7 @@ public abstract class SupplierUtils { * @return a supplier's result or the given Object as-is * @since 6.1.4 */ + @Contract("null -> null") public static @Nullable Object resolve(@Nullable Object candidate) { return (candidate instanceof Supplier supplier ? supplier.get() : candidate); } diff --git a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationMetadata.java b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationMetadata.java index abc6040873..a55fea4169 100644 --- a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationMetadata.java +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationMetadata.java @@ -21,6 +21,7 @@ import java.lang.classfile.Annotation; import java.lang.classfile.AnnotationElement; import java.lang.classfile.AnnotationValue; import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute; +import java.lang.constant.ClassDesc; import java.lang.reflect.Array; import java.util.Collections; import java.util.LinkedHashMap; @@ -97,8 +98,9 @@ abstract class ClassFileAnnotationMetadata { } private static String fromTypeDescriptor(String descriptor) { - return descriptor.substring(1, descriptor.length() - 1) - .replace('/', '.'); + ClassDesc classDesc = ClassDesc.ofDescriptor(descriptor); + return classDesc.isPrimitive() ? classDesc.displayName() : + classDesc.packageName() + "." + classDesc.displayName(); } private static Object parseArrayValue(String className, @org.jetbrains.annotations.Nullable ClassLoader classLoader, AnnotationValue.OfArray arrayValue) { diff --git a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileClassMetadata.java b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileClassMetadata.java index 17d274a82f..369511513b 100644 --- a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileClassMetadata.java +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileClassMetadata.java @@ -72,7 +72,7 @@ class ClassFileClassMetadata implements AnnotationMetadata { this.className = className; this.accessFlags = accessFlags; this.enclosingClassName = enclosingClassName; - this.superClassName = superClassName; + this.superClassName = (!className.endsWith(".package-info")) ? superClassName : null; this.independentInnerClass = independentInnerClass; this.interfaceNames = interfaceNames; this.memberClassNames = memberClassNames; @@ -268,18 +268,16 @@ class ClassFileClassMetadata implements AnnotationMetadata { void nestMembers(String currentClassName, InnerClassesAttribute innerClasses) { for (InnerClassInfo classInfo : innerClasses.classes()) { String innerClassName = classInfo.innerClass().name().stringValue(); - // skip parent inner classes - if (!innerClassName.startsWith(currentClassName)) { - continue; - } - // the current class is an inner class - else if (currentClassName.equals(innerClassName)) { + if (currentClassName.equals(innerClassName)) { + // the current class is an inner class this.innerAccessFlags = classInfo.flags(); } - // collecting data about actual inner classes - else { - this.memberClassNames.add(ClassUtils.convertResourcePathToClassName(innerClassName)); - } + classInfo.outerClass().ifPresent(outerClass -> { + if (outerClass.name().stringValue().equals(currentClassName)) { + // collecting data about actual inner classes + this.memberClassNames.add(ClassUtils.convertResourcePathToClassName(innerClassName)); + } + }); } } diff --git a/spring-core/src/test/java/org/springframework/SpringCoreTestSuite.java b/spring-core/src/test/java/org/springframework/SpringCoreTestSuite.java index a2d6234353..33207e5c1c 100644 --- a/spring-core/src/test/java/org/springframework/SpringCoreTestSuite.java +++ b/spring-core/src/test/java/org/springframework/SpringCoreTestSuite.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. @@ -28,7 +28,11 @@ import org.junit.platform.suite.api.Suite; * @author Sam Brannen */ @Suite -@SelectPackages({"org.springframework.core", "org.springframework.util"}) +@SelectPackages({ + "org.springframework.aot", + "org.springframework.core", + "org.springframework.util" +}) @IncludeClassNamePatterns(".*Tests?$") class SpringCoreTestSuite { } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationBackCompatibilityTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationBackCompatibilityTests.java index 9ca47d7f63..434bfe7c93 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationBackCompatibilityTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationBackCompatibilityTests.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. @@ -32,7 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat; class AnnotationBackCompatibilityTests { @Test - void multiplRoutesToMetaAnnotation() { + void multipleRoutesToMetaAnnotation() { Class source = WithMetaMetaTestAnnotation1AndMetaTestAnnotation2.class; // Merged annotation chooses lowest depth MergedAnnotation mergedAnnotation = MergedAnnotations.from(source).get(TestAnnotation.class); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java index 65022cb36b..7733a9c641 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java @@ -984,7 +984,7 @@ class MergedAnnotationsTests { } @Test - void getDirectFromClassgetDirectFromClassMetaMetaAnnotatedClass() { + void getDirectFromClassWithMetaMetaAnnotatedClass() { MergedAnnotation annotation = MergedAnnotations.from( MetaMetaAnnotatedClass.class, SearchStrategy.TYPE_HIERARCHY).get(Component.class); assertThat(annotation.getString("value")).isEqualTo("meta2"); diff --git a/spring-core/src/test/java/org/springframework/core/convert/converter/ConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/converter/ConverterTests.java index 65d519f540..f72f2069cd 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/converter/ConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/converter/ConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -46,7 +46,7 @@ class ConverterTests { } @Test - void andThenCanConvertfromDifferentSourceType() { + void andThenCanConvertFromDifferentSourceType() { Converter length = String::length; assertThat(length.andThen(this.moduloTwo).convert("example")).isEqualTo(1); assertThat(length.andThen(this.addOne).convert("example")).isEqualTo(8); diff --git a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java index f1b35cc7c0..5e07edd8f3 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.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. @@ -44,9 +44,11 @@ import java.util.regex.Pattern; import java.util.stream.Stream; import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.TypeDescriptor; @@ -55,6 +57,7 @@ import org.springframework.util.ClassUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.byLessThan; import static org.assertj.core.api.Assertions.entry; /** @@ -948,29 +951,119 @@ class DefaultConversionServiceTests { assertThat(converted).containsExactly(2, 3, 4); } - @Test - @SuppressWarnings("unchecked") - void convertObjectToOptional() { - Method method = ClassUtils.getMethod(TestEntity.class, "handleOptionalValue", Optional.class); - MethodParameter parameter = new MethodParameter(method, 0); - TypeDescriptor descriptor = new TypeDescriptor(parameter); - Object actual = conversionService.convert("1,2,3", TypeDescriptor.valueOf(String.class), descriptor); - assertThat(actual.getClass()).isEqualTo(Optional.class); - assertThat(((Optional>) actual)).contains(List.of(1, 2, 3)); - } - @Test - void convertObjectToOptionalNull() { - assertThat(conversionService.convert(null, TypeDescriptor.valueOf(Object.class), - TypeDescriptor.valueOf(Optional.class))).isSameAs(Optional.empty()); - assertThat((Object) conversionService.convert(null, Optional.class)).isSameAs(Optional.empty()); - } + @Nested + class OptionalConversionTests { - @Test - void convertExistingOptional() { - assertThat(conversionService.convert(Optional.empty(), TypeDescriptor.valueOf(Object.class), - TypeDescriptor.valueOf(Optional.class))).isSameAs(Optional.empty()); - assertThat((Object) conversionService.convert(Optional.empty(), Optional.class)).isSameAs(Optional.empty()); + private static final TypeDescriptor rawOptionalType = TypeDescriptor.valueOf(Optional.class); + + + @Test + @SuppressWarnings("unchecked") + void convertObjectToOptional() { + Method method = ClassUtils.getMethod(getClass(), "handleOptionalList", Optional.class); + MethodParameter parameter = new MethodParameter(method, 0); + TypeDescriptor descriptor = new TypeDescriptor(parameter); + Object actual = conversionService.convert("1,2,3", TypeDescriptor.valueOf(String.class), descriptor); + assertThat(((Optional>) actual)).contains(List.of(1, 2, 3)); + } + + @Test + void convertNullToOptional() { + assertThat((Object) conversionService.convert(null, Optional.class)).isSameAs(Optional.empty()); + assertThat(conversionService.convert(null, TypeDescriptor.valueOf(Object.class), rawOptionalType)) + .isSameAs(Optional.empty()); + } + + @Test + void convertNullOptionalToNull() { + assertThat(conversionService.convert(null, rawOptionalType, TypeDescriptor.valueOf(Object.class))).isNull(); + } + + @Test // gh-34544 + void convertEmptyOptionalToNull() { + Optional empty = Optional.empty(); + + assertThat(conversionService.convert(empty, Object.class)).isNull(); + assertThat(conversionService.convert(empty, String.class)).isNull(); + + assertThat(conversionService.convert(empty, rawOptionalType, TypeDescriptor.valueOf(Object.class))).isNull(); + assertThat(conversionService.convert(empty, rawOptionalType, TypeDescriptor.valueOf(String.class))).isNull(); + assertThat(conversionService.convert(empty, rawOptionalType, TypeDescriptor.valueOf(Integer[].class))).isNull(); + assertThat(conversionService.convert(empty, rawOptionalType, TypeDescriptor.valueOf(List.class))).isNull(); + } + + @Test + void convertEmptyOptionalToOptional() { + assertThat((Object) conversionService.convert(Optional.empty(), Optional.class)).isSameAs(Optional.empty()); + assertThat(conversionService.convert(Optional.empty(), TypeDescriptor.valueOf(Object.class), rawOptionalType)) + .isSameAs(Optional.empty()); + } + + @Test // gh-34544 + @SuppressWarnings("unchecked") + void convertOptionalToOptionalWithoutConversionOfContainedObject() { + assertThat(conversionService.convert(Optional.of(42), Optional.class)).contains(42); + + assertThat(conversionService.convert(Optional.of("enigma"), Optional.class)).contains("enigma"); + assertThat((Optional) conversionService.convert(Optional.of("enigma"), rawOptionalType, rawOptionalType)) + .contains("enigma"); + } + + @Test // gh-34544 + @SuppressWarnings("unchecked") + void convertOptionalToOptionalWithConversionOfContainedObject() { + TypeDescriptor integerOptionalType = + new TypeDescriptor(ResolvableType.forClassWithGenerics(Optional.class, Integer.class), null, null); + TypeDescriptor stringOptionalType = + new TypeDescriptor(ResolvableType.forClassWithGenerics(Optional.class, String.class), null, null); + + assertThat((Optional) conversionService.convert(Optional.of(42), integerOptionalType, stringOptionalType)) + .contains("42"); + } + + @Test // gh-34544 + @SuppressWarnings("unchecked") + void convertOptionalToObjectWithoutConversionOfContainedObject() { + assertThat(conversionService.convert(Optional.of("enigma"), String.class)).isEqualTo("enigma"); + assertThat(conversionService.convert(Optional.of(42), Integer.class)).isEqualTo(42); + assertThat(conversionService.convert(Optional.of(new int[] {1, 2, 3}), int[].class)).containsExactly(1, 2, 3); + assertThat(conversionService.convert(Optional.of(new Integer[] {1, 2, 3}), Integer[].class)).containsExactly(1, 2, 3); + assertThat(conversionService.convert(Optional.of(List.of(1, 2, 3)), List.class)).containsExactly(1, 2, 3); + } + + @Test // gh-34544 + @SuppressWarnings("unchecked") + void convertOptionalToObjectWithConversionOfContainedObject() { + assertThat(conversionService.convert(Optional.of(42), String.class)).isEqualTo("42"); + assertThat(conversionService.convert(Optional.of(3.14F), Double.class)).isCloseTo(3.14, byLessThan(0.001)); + assertThat(conversionService.convert(Optional.of(new int[] {1, 2, 3}), Integer[].class)).containsExactly(1, 2, 3); + assertThat(conversionService.convert(Optional.of(List.of(1, 2, 3)), Set.class)).containsExactly(1, 2, 3); + } + + @Test // gh-34544 + @SuppressWarnings("unchecked") + void convertNestedOptionalsToObject() { + assertThat(conversionService.convert(Optional.of(Optional.of("unwrap me twice")), String.class)) + .isEqualTo("unwrap me twice"); + } + + @Test // gh-34544 + @SuppressWarnings("unchecked") + void convertOptionalToObjectViaTypeDescriptorForMethodParameter() { + Method method = ClassUtils.getMethod(getClass(), "handleList", List.class); + MethodParameter parameter = new MethodParameter(method, 0); + TypeDescriptor descriptor = new TypeDescriptor(parameter); + + Optional> source = Optional.of(List.of(1, 2, 3)); + assertThat((List) conversionService.convert(source, rawOptionalType, descriptor)).containsExactly(1, 2, 3); + } + + public void handleList(List value) { + } + + public void handleOptionalList(Optional> value) { + } } @@ -1068,9 +1161,6 @@ class DefaultConversionServiceTests { public static TestEntity findTestEntity(Long id) { return new TestEntity(id); } - - public void handleOptionalValue(Optional> value) { - } } diff --git a/spring-core/src/test/java/org/springframework/core/env/AbstractPropertyResolverTests.java b/spring-core/src/test/java/org/springframework/core/env/AbstractPropertyResolverTests.java new file mode 100644 index 0000000000..81d8e0ad6e --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/env/AbstractPropertyResolverTests.java @@ -0,0 +1,120 @@ +/* + * 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.core.env; + +import java.util.stream.IntStream; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.SpringProperties; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.springframework.core.env.AbstractPropertyResolver.DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME; +import static org.springframework.core.env.AbstractPropertyResolver.UNDEFINED_ESCAPE_CHARACTER; + +/** + * Unit tests for {@link AbstractPropertyResolver}. + * + * @author Sam Brannen + * @since 6.2.7 + */ +class AbstractPropertyResolverTests { + + @BeforeEach + void resetStateBeforeEachTest() { + resetState(); + } + + @AfterAll + static void resetState() { + AbstractPropertyResolver.defaultEscapeCharacter = UNDEFINED_ESCAPE_CHARACTER; + setSpringProperty(null); + } + + + @Test + void getDefaultEscapeCharacterWithSpringPropertySetToCharacterMinValue() { + setSpringProperty("" + Character.MIN_VALUE); + + assertThatIllegalArgumentException() + .isThrownBy(AbstractPropertyResolver::getDefaultEscapeCharacter) + .withMessage("Value for property [%s] must not be Character.MIN_VALUE", + DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME); + + assertThat(AbstractPropertyResolver.defaultEscapeCharacter).isEqualTo(UNDEFINED_ESCAPE_CHARACTER); + } + + @Test + void getDefaultEscapeCharacterWithSpringPropertySetToXyz() { + setSpringProperty("XYZ"); + + assertThatIllegalArgumentException() + .isThrownBy(AbstractPropertyResolver::getDefaultEscapeCharacter) + .withMessage("Value [XYZ] for property [%s] must be a single character or an empty string", + DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME); + + assertThat(AbstractPropertyResolver.defaultEscapeCharacter).isEqualTo(UNDEFINED_ESCAPE_CHARACTER); + } + + @Test + void getDefaultEscapeCharacterWithSpringPropertySetToEmptyString() { + setSpringProperty(""); + assertEscapeCharacter(null); + } + + @Test + void getDefaultEscapeCharacterWithoutSpringPropertySet() { + assertEscapeCharacter('\\'); + } + + @Test + void getDefaultEscapeCharacterWithSpringPropertySetToBackslash() { + setSpringProperty("\\"); + assertEscapeCharacter('\\'); + } + + @Test + void getDefaultEscapeCharacterWithSpringPropertySetToTilde() { + setSpringProperty("~"); + assertEscapeCharacter('~'); + } + + @Test + void getDefaultEscapeCharacterFromMultipleThreads() { + setSpringProperty("~"); + + IntStream.range(1, 32).parallel().forEach(__ -> + assertThat(AbstractPropertyResolver.getDefaultEscapeCharacter()).isEqualTo('~')); + + assertThat(AbstractPropertyResolver.defaultEscapeCharacter).isEqualTo('~'); + } + + + private static void setSpringProperty(String value) { + SpringProperties.setProperty(DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME, value); + } + + private static void assertEscapeCharacter(@Nullable Character expected) { + assertThat(AbstractPropertyResolver.getDefaultEscapeCharacter()).isEqualTo(expected); + assertThat(AbstractPropertyResolver.defaultEscapeCharacter).isEqualTo(expected); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java b/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java index 23654dfe10..514052e47c 100644 --- a/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.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,6 +21,7 @@ import java.util.Map; import java.util.Properties; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.core.convert.ConverterNotFoundException; @@ -38,18 +39,15 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; */ class PropertySourcesPropertyResolverTests { - private Properties testProperties; + private final Properties testProperties = new Properties(); - private MutablePropertySources propertySources; + private final MutablePropertySources propertySources = new MutablePropertySources(); - private ConfigurablePropertyResolver propertyResolver; + private final PropertySourcesPropertyResolver propertyResolver = new PropertySourcesPropertyResolver(propertySources); @BeforeEach void setUp() { - propertySources = new MutablePropertySources(); - propertyResolver = new PropertySourcesPropertyResolver(propertySources); - testProperties = new Properties(); propertySources.addFirst(new PropertiesPropertySource("testProperties", testProperties)); } @@ -77,14 +75,12 @@ class PropertySourcesPropertyResolverTests { @Test void getProperty_propertySourceSearchOrderIsFIFO() { - MutablePropertySources sources = new MutablePropertySources(); - PropertyResolver resolver = new PropertySourcesPropertyResolver(sources); - sources.addFirst(new MockPropertySource("ps1").withProperty("pName", "ps1Value")); - assertThat(resolver.getProperty("pName")).isEqualTo("ps1Value"); - sources.addFirst(new MockPropertySource("ps2").withProperty("pName", "ps2Value")); - assertThat(resolver.getProperty("pName")).isEqualTo("ps2Value"); - sources.addFirst(new MockPropertySource("ps3").withProperty("pName", "ps3Value")); - assertThat(resolver.getProperty("pName")).isEqualTo("ps3Value"); + propertySources.addFirst(new MockPropertySource("ps1").withProperty("pName", "ps1Value")); + assertThat(propertyResolver.getProperty("pName")).isEqualTo("ps1Value"); + propertySources.addFirst(new MockPropertySource("ps2").withProperty("pName", "ps2Value")); + assertThat(propertyResolver.getProperty("pName")).isEqualTo("ps2Value"); + propertySources.addFirst(new MockPropertySource("ps3").withProperty("pName", "ps3Value")); + assertThat(propertyResolver.getProperty("pName")).isEqualTo("ps3Value"); } @Test @@ -115,8 +111,8 @@ class PropertySourcesPropertyResolverTests { class TestType { } - assertThatExceptionOfType(ConverterNotFoundException.class).isThrownBy(() -> - propertyResolver.getProperty("foo", TestType.class)); + assertThatExceptionOfType(ConverterNotFoundException.class) + .isThrownBy(() -> propertyResolver.getProperty("foo", TestType.class)); } @Test @@ -127,7 +123,6 @@ class PropertySourcesPropertyResolverTests { HashMap map = new HashMap<>(); map.put(key, value1); // before construction - MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MapPropertySource("testProperties", map)); PropertyResolver propertyResolver = new PropertySourcesPropertyResolver(propertySources); assertThat(propertyResolver.getProperty(key)).isEqualTo(value1); @@ -138,7 +133,6 @@ class PropertySourcesPropertyResolverTests { @Test void getProperty_doesNotCache_addNewKeyPostConstruction() { HashMap map = new HashMap<>(); - MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MapPropertySource("testProperties", map)); PropertyResolver propertyResolver = new PropertySourcesPropertyResolver(propertySources); assertThat(propertyResolver.getProperty("foo")).isNull(); @@ -148,10 +142,9 @@ class PropertySourcesPropertyResolverTests { @Test void getPropertySources_replacePropertySource() { - propertySources = new MutablePropertySources(); - propertyResolver = new PropertySourcesPropertyResolver(propertySources); propertySources.addLast(new MockPropertySource("local").withProperty("foo", "localValue")); propertySources.addLast(new MockPropertySource("system").withProperty("foo", "systemValue")); + assertThat(propertySources).hasSize(3); // 'local' was added first so has precedence assertThat(propertyResolver.getProperty("foo")).isEqualTo("localValue"); @@ -162,7 +155,7 @@ class PropertySourcesPropertyResolverTests { // 'system' now has precedence assertThat(propertyResolver.getProperty("foo")).isEqualTo("newValue"); - assertThat(propertySources).hasSize(2); + assertThat(propertySources).hasSize(3); } @Test @@ -170,81 +163,65 @@ class PropertySourcesPropertyResolverTests { testProperties.put("exists", "xyz"); assertThat(propertyResolver.getRequiredProperty("exists")).isEqualTo("xyz"); - assertThatIllegalStateException().isThrownBy(() -> - propertyResolver.getRequiredProperty("bogus")); + assertThatIllegalStateException().isThrownBy(() -> propertyResolver.getRequiredProperty("bogus")); } @Test void getRequiredProperty_withStringArrayConversion() { testProperties.put("exists", "abc,123"); - assertThat(propertyResolver.getRequiredProperty("exists", String[].class)).isEqualTo(new String[] { "abc", "123" }); + assertThat(propertyResolver.getRequiredProperty("exists", String[].class)).containsExactly("abc", "123"); - assertThatIllegalStateException().isThrownBy(() -> - propertyResolver.getRequiredProperty("bogus", String[].class)); + assertThatIllegalStateException().isThrownBy(() -> propertyResolver.getRequiredProperty("bogus", String[].class)); } @Test void resolvePlaceholders() { - MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); - PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); - assertThat(resolver.resolvePlaceholders("Replace this ${key}")).isEqualTo("Replace this value"); + assertThat(propertyResolver.resolvePlaceholders("Replace this ${key}")).isEqualTo("Replace this value"); } @Test void resolvePlaceholders_withUnresolvable() { - MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); - PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); - assertThat(resolver.resolvePlaceholders("Replace this ${key} plus ${unknown}")) + assertThat(propertyResolver.resolvePlaceholders("Replace this ${key} plus ${unknown}")) .isEqualTo("Replace this value plus ${unknown}"); } @Test void resolvePlaceholders_withDefaultValue() { - MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); - PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); - assertThat(resolver.resolvePlaceholders("Replace this ${key} plus ${unknown:defaultValue}")) + assertThat(propertyResolver.resolvePlaceholders("Replace this ${key} plus ${unknown:defaultValue}")) .isEqualTo("Replace this value plus defaultValue"); } @Test void resolvePlaceholders_withNullInput() { - assertThatIllegalArgumentException().isThrownBy(() -> - new PropertySourcesPropertyResolver(new MutablePropertySources()).resolvePlaceholders(null)); + assertThatIllegalArgumentException().isThrownBy(() -> propertyResolver.resolvePlaceholders(null)); } @Test void resolveRequiredPlaceholders() { - MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); - PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); - assertThat(resolver.resolveRequiredPlaceholders("Replace this ${key}")).isEqualTo("Replace this value"); + assertThat(propertyResolver.resolveRequiredPlaceholders("Replace this ${key}")).isEqualTo("Replace this value"); } @Test void resolveRequiredPlaceholders_withUnresolvable() { - MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); - PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); - assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> - resolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown}")); + assertThatExceptionOfType(PlaceholderResolutionException.class) + .isThrownBy(() -> propertyResolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown}")); } @Test void resolveRequiredPlaceholders_withDefaultValue() { - MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); - PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); - assertThat(resolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown:defaultValue}")) + assertThat(propertyResolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown:defaultValue}")) .isEqualTo("Replace this value plus defaultValue"); } @Test void resolveRequiredPlaceholders_withNullInput() { - assertThatIllegalArgumentException().isThrownBy(() -> - new PropertySourcesPropertyResolver(new MutablePropertySources()).resolveRequiredPlaceholders(null)); + assertThatIllegalArgumentException().isThrownBy(() -> propertyResolver.resolveRequiredPlaceholders(null)); } @Test @@ -256,17 +233,17 @@ class PropertySourcesPropertyResolverTests { propertyResolver.setRequiredProperties("foo", "bar"); // neither foo nor bar properties are present -> validating should throw - assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy( - propertyResolver::validateRequiredProperties) - .withMessage("The following properties were declared as required " + - "but could not be resolved: [foo, bar]"); + assertThatExceptionOfType(MissingRequiredPropertiesException.class) + .isThrownBy(propertyResolver::validateRequiredProperties) + .withMessage("The following properties were declared as required " + + "but could not be resolved: [foo, bar]"); // add foo property -> validation should fail only on missing 'bar' property testProperties.put("foo", "fooValue"); - assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy( - propertyResolver::validateRequiredProperties) - .withMessage("The following properties were declared as required " + - "but could not be resolved: [bar]"); + assertThatExceptionOfType(MissingRequiredPropertiesException.class) + .isThrownBy(propertyResolver::validateRequiredProperties) + .withMessage("The following properties were declared as required " + + "but could not be resolved: [bar]"); // add bar property -> validation should pass, even with an empty string value testProperties.put("bar", ""); @@ -291,13 +268,13 @@ class PropertySourcesPropertyResolverTests { assertThat(pr.getProperty("p2")).isEqualTo("v2"); assertThat(pr.getProperty("p3")).isEqualTo("v1:v2"); assertThat(pr.getProperty("p4")).isEqualTo("v1:v2"); - assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> - pr.getProperty("p5")) - .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); + assertThatExceptionOfType(PlaceholderResolutionException.class) + .isThrownBy(() -> pr.getProperty("p5")) + .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); assertThat(pr.getProperty("p6")).isEqualTo("v1:v2:def"); - assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> - pr.getProperty("pL")) - .withMessageContaining("Circular"); + assertThatExceptionOfType(PlaceholderResolutionException.class) + .isThrownBy(() -> pr.getProperty("pL")) + .withMessageContaining("Circular"); } @Test @@ -349,9 +326,9 @@ class PropertySourcesPropertyResolverTests { // placeholders nested within the value of "p4" are unresolvable and cause an // exception by default - assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> - pr.getProperty("p4")) - .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); + assertThatExceptionOfType(PlaceholderResolutionException.class) + .isThrownBy(() -> pr.getProperty("p4")) + .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); // relax the treatment of unresolvable nested placeholders pr.setIgnoreUnresolvableNestedPlaceholders(true); @@ -361,9 +338,58 @@ class PropertySourcesPropertyResolverTests { // resolve[Nested]Placeholders methods behave as usual regardless the value of // ignoreUnresolvableNestedPlaceholders assertThat(pr.resolvePlaceholders("${p1}:${p2}:${bogus}")).isEqualTo("v1:v2:${bogus}"); - assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> - pr.resolveRequiredPlaceholders("${p1}:${p2}:${bogus}")) - .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); + assertThatExceptionOfType(PlaceholderResolutionException.class) + .isThrownBy(() -> pr.resolveRequiredPlaceholders("${p1}:${p2}:${bogus}")) + .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); + } + + + @Nested + class EscapedPlaceholderTests { + + @Test // gh-34720 + void escapedPlaceholdersAreNotEvaluated() { + testProperties.put("prop1", "value1"); + testProperties.put("prop2", "value2\\${prop1}"); + + assertThat(propertyResolver.getProperty("prop2")).isEqualTo("value2${prop1}"); + } + + @Test // gh-34720 + void escapedPlaceholdersAreNotEvaluatedWithCharSequenceValues() { + testProperties.put("prop1", "value1"); + testProperties.put("prop2", new StringBuilder("value2\\${prop1}")); + + assertThat(propertyResolver.getProperty("prop2")).isEqualTo("value2${prop1}"); + } + + @Test // gh-34720 + void multipleEscapedPlaceholdersArePreserved() { + testProperties.put("prop1", "value1"); + testProperties.put("prop2", "value2"); + testProperties.put("complex", "start\\${prop1}middle\\${prop2}end"); + + assertThat(propertyResolver.getProperty("complex")).isEqualTo("start${prop1}middle${prop2}end"); + } + + @Test // gh-34720 + void doubleBackslashesAreProcessedCorrectly() { + testProperties.put("prop1", "value1"); + testProperties.put("doubleEscaped", "value2\\\\${prop1}"); + + assertThat(propertyResolver.getProperty("doubleEscaped")).isEqualTo("value2\\${prop1}"); + } + + @Test // gh-34720 + void escapedPlaceholdersInNestedPropertiesAreNotEvaluated() { + testProperties.put("p1", "v1"); + testProperties.put("p2", "v2"); + testProperties.put("escaped", "prefix-\\${p1}"); + testProperties.put("nested", "${escaped}-${p2}"); + + assertThat(propertyResolver.getProperty("nested")).isEqualTo("prefix-${p1}-v2"); + } + } } diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java index a91aab01d3..d20bcf64df 100644 --- a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java @@ -16,6 +16,7 @@ package org.springframework.core.io.buffer; +import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; @@ -341,6 +342,48 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests { assertThat(len).isEqualTo(3); assertThat(bytes).containsExactly('c', 'd', 'e'); + buffer.readPosition(0); + inputStream = buffer.asInputStream(); + assertThat(inputStream.readAllBytes()).asString().isEqualTo("abcde"); + assertThat(inputStream.available()).isEqualTo(0); + assertThat(inputStream.readAllBytes()).isEmpty(); + + buffer.readPosition(0); + inputStream = buffer.asInputStream(); + inputStream.mark(5); + assertThat(inputStream.readNBytes(0)).isEmpty(); + assertThat(inputStream.readNBytes(1000)).asString().isEqualTo("abcde"); + inputStream.reset(); + assertThat(inputStream.readNBytes(3)).asString().isEqualTo("abc"); + assertThat(inputStream.readNBytes(2)).asString().isEqualTo("de"); + assertThat(inputStream.readNBytes(10)).isEmpty(); + + buffer.readPosition(0); + inputStream = buffer.asInputStream(); + inputStream.mark(5); + assertThat(inputStream.skip(1)).isEqualTo(1); + assertThat(inputStream.readAllBytes()).asString().isEqualTo("bcde"); + assertThat(inputStream.skip(10)).isEqualTo(0); + assertThat(inputStream.available()).isEqualTo(0); + inputStream.reset(); + assertThat(inputStream.skip(100)).isEqualTo(5); + assertThat(inputStream.available()).isEqualTo(0); + + buffer.readPosition(0); + inputStream = buffer.asInputStream(); + inputStream.mark(5); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + assertThat(inputStream.transferTo(out)).isEqualTo(5); + assertThat(out.toByteArray()).asString().isEqualTo("abcde"); + assertThat(inputStream.available()).isEqualTo(0); + out.reset(); + inputStream.reset(); + assertThat(inputStream.read()).isEqualTo('a'); + assertThat(inputStream.transferTo(out)).isEqualTo(4); + assertThat(out.toByteArray()).asString().isEqualTo("bcde"); + assertThat(inputStream.available()).isEqualTo(0); + assertThat(inputStream.transferTo(OutputStream.nullOutputStream())).isEqualTo(0); + release(buffer); } 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 af1d12e0b4..bde76e830c 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. @@ -44,6 +44,8 @@ import java.util.stream.Collectors; import java.util.zip.ZipEntry; import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -132,6 +134,7 @@ class PathMatchingResourcePatternResolverTests { 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"); } @@ -297,8 +300,8 @@ class PathMatchingResourcePatternResolverTests { @Test void rootPatternRetrievalInJarFiles() throws IOException { assertThat(resolver.getResources("classpath*:aspectj*.dtd")).extracting(Resource::getFilename) - .as("Could not find aspectj_1_5_0.dtd in the root of the aspectjweaver jar") - .containsExactly("aspectj_1_5_0.dtd"); + .as("Could not find aspectj_1_5_0.dtd in the root of the aspectjweaver jar") + .containsExactly("aspectj_1_5_0.dtd"); } } @@ -309,6 +312,16 @@ class PathMatchingResourcePatternResolverTests { @TempDir Path temp; + @BeforeAll + static void suppressJarCaches() { + URLConnection.setDefaultUseCaches("jar", false); + } + + @AfterAll + static void restoreJarCaches() { + URLConnection.setDefaultUseCaches("jar", true); + } + @Test void javaDashJarFindsClassPathManifestEntries() throws Exception { Path lib = this.temp.resolve("lib"); @@ -332,8 +345,22 @@ class PathMatchingResourcePatternResolverTests { StreamUtils.copy("test", StandardCharsets.UTF_8, jar); jar.closeEntry(); } + assertThat(new FileSystemResource(path).exists()).isTrue(); assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isTrue(); + assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/file.txt").exists()).isTrue(); + assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/none.txt").exists()).isFalse(); + assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + "X" + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isFalse(); + assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + "X" + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/file.txt").exists()).isFalse(); + assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + "X" + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/none.txt").exists()).isFalse(); + + Resource resource = new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/file.txt"); + try (InputStream is = resource.getInputStream()) { + assertThat(resource.exists()).isTrue(); + assertThat(resource.createRelative("file.txt").exists()).isTrue(); + assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isTrue(); + is.readAllBytes(); + } } private void writeApplicationJar(Path path) throws Exception { diff --git a/spring-core/src/test/java/org/springframework/core/retry/MaxAttemptsDefaultRetryPolicyTests.java b/spring-core/src/test/java/org/springframework/core/retry/MaxAttemptsDefaultRetryPolicyTests.java new file mode 100644 index 0000000000..0f05e4b251 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/retry/MaxAttemptsDefaultRetryPolicyTests.java @@ -0,0 +1,188 @@ +/* + * 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.core.retry; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.FileSystemException; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Max attempts tests for {@link DefaultRetryPolicy} and its {@link RetryExecution}. + * + * @author Mahmoud Ben Hassine + * @author Sam Brannen + * @since 7.0 + */ +class MaxAttemptsDefaultRetryPolicyTests { + + @Test + void invalidMaxAttempts() { + assertThatIllegalArgumentException() + .isThrownBy(() -> RetryPolicy.withMaxAttempts(0)) + .withMessage("Max attempts must be greater than zero"); + assertThatIllegalArgumentException() + .isThrownBy(() -> RetryPolicy.withMaxAttempts(-1)) + .withMessage("Max attempts must be greater than zero"); + } + + @Test + void maxAttempts() { + var retryPolicy = RetryPolicy.withMaxAttempts(2); + var retryExecution = retryPolicy.start(); + var throwable = mock(Throwable.class); + + assertThat(retryExecution.shouldRetry(throwable)).isTrue(); + assertThat(retryExecution.shouldRetry(throwable)).isTrue(); + + assertThat(retryExecution.shouldRetry(throwable)).isFalse(); + assertThat(retryExecution.shouldRetry(throwable)).isFalse(); + } + + @Test + void maxAttemptsAndPredicate() { + var retryPolicy = RetryPolicy.builder() + .maxAttempts(4) + .predicate(NumberFormatException.class::isInstance) + .build(); + + var retryExecution = retryPolicy.start(); + + // 4 retries + assertThat(retryExecution.shouldRetry(new NumberFormatException())).isTrue(); + assertThat(retryExecution.shouldRetry(new IllegalStateException())).isFalse(); + assertThat(retryExecution.shouldRetry(new IllegalStateException())).isFalse(); + assertThat(retryExecution.shouldRetry(new CustomNumberFormatException())).isTrue(); + + // After policy exhaustion + assertThat(retryExecution.shouldRetry(new NumberFormatException())).isFalse(); + assertThat(retryExecution.shouldRetry(new IllegalStateException())).isFalse(); + } + + @Test + void maxAttemptsWithIncludesAndExcludes() { + var policy = RetryPolicy.builder() + .maxAttempts(6) + .includes(RuntimeException.class, IOException.class) + .excludes(FileNotFoundException.class, CustomFileSystemException.class) + .build(); + + var retryExecution = policy.start(); + + // 6 retries + assertThat(retryExecution.shouldRetry(new IOException())).isTrue(); + assertThat(retryExecution.shouldRetry(new RuntimeException())).isTrue(); + assertThat(retryExecution.shouldRetry(new FileNotFoundException())).isFalse(); + assertThat(retryExecution.shouldRetry(new FileSystemException("file"))).isTrue(); + assertThat(retryExecution.shouldRetry(new CustomFileSystemException("file"))).isFalse(); + assertThat(retryExecution.shouldRetry(new IOException())).isTrue(); + + // After policy exhaustion + assertThat(retryExecution.shouldRetry(new IOException())).isFalse(); + } + + @Test + void toStringImplementations() { + var policy = RetryPolicy.builder() + .maxAttempts(6) + .includes(RuntimeException.class, IOException.class) + .excludes(FileNotFoundException.class, CustomFileSystemException.class) + .build(); + + assertThat(policy).asString().isEqualTo(""" + DefaultRetryPolicy[\ + maxAttempts=6, \ + includes=[java.lang.RuntimeException, java.io.IOException], \ + excludes=[java.io.FileNotFoundException, \ + org.springframework.core.retry.MaxAttemptsDefaultRetryPolicyTests.CustomFileSystemException]]"""); + + var template = """ + DefaultRetryPolicyExecution[\ + maxAttempts=6, \ + retryCount=%d, \ + includes=[java.lang.RuntimeException, java.io.IOException], \ + excludes=[java.io.FileNotFoundException, \ + org.springframework.core.retry.MaxAttemptsDefaultRetryPolicyTests.CustomFileSystemException]]"""; + var retryExecution = policy.start(); + var count = 0; + + assertThat(retryExecution).asString().isEqualTo(template, count++); + retryExecution.shouldRetry(new IOException()); + assertThat(retryExecution).asString().isEqualTo(template, count++); + retryExecution.shouldRetry(new IOException()); + assertThat(retryExecution).asString().isEqualTo(template, count++); + } + + @Test + void toStringImplementationsWithPredicateAsClass() { + var policy = RetryPolicy.builder() + .maxAttempts(1) + .predicate(new NumberFormatExceptionMatcher()) + .build(); + assertThat(policy).asString() + .isEqualTo("DefaultRetryPolicy[maxAttempts=1, predicate=NumberFormatExceptionMatcher]"); + + var retryExecution = policy.start(); + assertThat(retryExecution).asString() + .isEqualTo("DefaultRetryPolicyExecution[maxAttempts=1, retryCount=0, predicate=NumberFormatExceptionMatcher]"); + } + + @Test + void toStringImplementationsWithPredicateAsLambda() { + var policy = RetryPolicy.builder() + .maxAttempts(2) + .predicate(NumberFormatException.class::isInstance) + .build(); + assertThat(policy).asString() + .matches("DefaultRetryPolicy\\[maxAttempts=2, predicate=MaxAttemptsDefaultRetryPolicyTests.+?Lambda.+?]"); + + var retryExecution = policy.start(); + assertThat(retryExecution).asString() + .matches("DefaultRetryPolicyExecution\\[maxAttempts=2, retryCount=0, predicate=MaxAttemptsDefaultRetryPolicyTests.+?Lambda.+?]"); + + retryExecution.shouldRetry(new NumberFormatException()); + assertThat(retryExecution).asString() + .matches("DefaultRetryPolicyExecution\\[maxAttempts=2, retryCount=1, predicate=MaxAttemptsDefaultRetryPolicyTests.+?Lambda.+?]"); + + retryExecution.shouldRetry(new NumberFormatException()); + assertThat(retryExecution).asString() + .matches("DefaultRetryPolicyExecution\\[maxAttempts=2, retryCount=2, predicate=MaxAttemptsDefaultRetryPolicyTests.+?Lambda.+?]"); + + retryExecution.shouldRetry(new NumberFormatException()); + assertThat(retryExecution).asString() + .matches("DefaultRetryPolicyExecution\\[maxAttempts=2, retryCount=3, predicate=MaxAttemptsDefaultRetryPolicyTests.+?Lambda.+?]"); + } + + + @SuppressWarnings("serial") + private static class CustomNumberFormatException extends NumberFormatException { + } + + @SuppressWarnings("serial") + private static class CustomFileSystemException extends FileSystemException { + + CustomFileSystemException(String file) { + super(file); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/retry/MaxDurationDefaultRetryPolicyTests.java b/spring-core/src/test/java/org/springframework/core/retry/MaxDurationDefaultRetryPolicyTests.java new file mode 100644 index 0000000000..7b3cda6d3c --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/retry/MaxDurationDefaultRetryPolicyTests.java @@ -0,0 +1,65 @@ +/* + * 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.core.retry; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import static java.time.Duration.ofSeconds; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Max duration tests for {@link DefaultRetryPolicy} and its {@link RetryExecution}. + * + * @author Mahmoud Ben Hassine + * @author Sam Brannen + * @since 7.0 + */ +class MaxDurationDefaultRetryPolicyTests { + + @Test + void invalidMaxDuration() { + assertThatIllegalArgumentException() + .isThrownBy(() -> RetryPolicy.withMaxDuration(Duration.ZERO)) + .withMessage("Max duration must be positive"); + assertThatIllegalArgumentException() + .isThrownBy(() -> RetryPolicy.withMaxDuration(ofSeconds(-1))) + .withMessage("Max duration must be positive"); + } + + @Test + void toStringImplementations() { + var policy1 = RetryPolicy.withMaxDuration(ofSeconds(3)); + var policy2 = RetryPolicy.builder() + .maxDuration(ofSeconds(1)) + .predicate(new NumberFormatExceptionMatcher()) + .build(); + + assertThat(policy1).asString() + .isEqualTo("DefaultRetryPolicy[maxDuration=3000ms]"); + assertThat(policy2).asString() + .isEqualTo("DefaultRetryPolicy[maxDuration=1000ms, predicate=NumberFormatExceptionMatcher]"); + + assertThat(policy1.start()).asString() + .matches("DefaultRetryPolicyExecution\\[maxDuration=3000ms, retryStartTime=.+]"); + assertThat(policy2.start()).asString() + .matches("DefaultRetryPolicyExecution\\[maxDuration=1000ms, retryStartTime=.+, predicate=NumberFormatExceptionMatcher]"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/retry/NumberFormatExceptionMatcher.java b/spring-core/src/test/java/org/springframework/core/retry/NumberFormatExceptionMatcher.java new file mode 100644 index 0000000000..3249d49982 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/retry/NumberFormatExceptionMatcher.java @@ -0,0 +1,34 @@ +/* + * 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.core.retry; + +import java.util.function.Predicate; + +/** + * Predicate that matches {@link NumberFormatException}. + * + * @author Sam Brannen + * @since 7.0 + */ +class NumberFormatExceptionMatcher implements Predicate { + + @Override + public boolean test(Throwable throwable) { + return (throwable instanceof NumberFormatException); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java b/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java new file mode 100644 index 0000000000..8ac5ad919d --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java @@ -0,0 +1,245 @@ +/* + * 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.core.retry; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments.ArgumentSet; +import org.junit.jupiter.params.provider.FieldSource; + +import org.springframework.util.backoff.FixedBackOff; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.InstanceOfAssertFactories.array; +import static org.junit.jupiter.params.provider.Arguments.argumentSet; + +/** + * Tests for {@link RetryTemplate}. + * + * @author Mahmoud Ben Hassine + * @author Sam Brannen + * @since 7.0 + */ +class RetryTemplateTests { + + private final RetryTemplate retryTemplate = new RetryTemplate(); + + + @BeforeEach + void configureTemplate() { + this.retryTemplate.setBackOffPolicy(new FixedBackOff(Duration.ofMillis(10))); + } + + @Test + void retryWithImmediateSuccess() throws Exception { + AtomicInteger invocationCount = new AtomicInteger(); + Retryable retryable = () -> { + invocationCount.incrementAndGet(); + return "always succeeds"; + }; + + assertThat(invocationCount).hasValue(0); + assertThat(retryTemplate.execute(retryable)).isEqualTo("always succeeds"); + assertThat(invocationCount).hasValue(1); + } + + @Test + void retryWithSuccessAfterInitialFailures() throws Exception { + AtomicInteger invocationCount = new AtomicInteger(); + Retryable retryable = () -> { + if (invocationCount.incrementAndGet() <= 2) { + throw new Exception("Boom!"); + } + return "finally succeeded"; + }; + + assertThat(invocationCount).hasValue(0); + assertThat(retryTemplate.execute(retryable)).isEqualTo("finally succeeded"); + assertThat(invocationCount).hasValue(3); + } + + @Test + void retryWithExhaustedPolicy() { + var invocationCount = new AtomicInteger(); + var exception = new RuntimeException("Boom!"); + + var retryable = new Retryable<>() { + @Override + public String execute() { + invocationCount.incrementAndGet(); + throw exception; + } + + @Override + public String getName() { + return "test"; + } + }; + + assertThat(invocationCount).hasValue(0); + assertThatExceptionOfType(RetryException.class) + .isThrownBy(() -> retryTemplate.execute(retryable)) + .withMessage("Retry policy for operation 'test' exhausted; aborting execution") + .withCause(exception); + // 4 = 1 initial invocation + 3 retry attempts + assertThat(invocationCount).hasValue(4); + } + + @Test + void retryWithFailingRetryableAndCustomRetryPolicyWithMultiplePredicates() { + var invocationCount = new AtomicInteger(); + var exception = new NumberFormatException("Boom!"); + + var retryable = new Retryable<>() { + @Override + public String execute() { + invocationCount.incrementAndGet(); + throw exception; + } + + @Override + public String getName() { + return "always fails"; + } + }; + + var retryPolicy = RetryPolicy.builder() + .maxAttempts(5) + .maxDuration(Duration.ofMillis(100)) + .predicate(NumberFormatException.class::isInstance) + .predicate(t -> t.getMessage().equals("Boom!")) + .build(); + + retryTemplate.setRetryPolicy(retryPolicy); + + assertThat(invocationCount).hasValue(0); + assertThatExceptionOfType(RetryException.class) + .isThrownBy(() -> retryTemplate.execute(retryable)) + .withMessage("Retry policy for operation 'always fails' exhausted; aborting execution") + .withCause(exception); + // 6 = 1 initial invocation + 5 retry attempts + assertThat(invocationCount).hasValue(6); + } + + @Test + void retryWithExceptionIncludes() { + var invocationCount = new AtomicInteger(); + + var retryable = new Retryable<>() { + @Override + public String execute() throws Exception { + return switch (invocationCount.incrementAndGet()) { + case 1 -> throw new FileNotFoundException(); + case 2 -> throw new IOException(); + case 3 -> throw new IllegalStateException(); + default -> "success"; + }; + } + + @Override + public String getName() { + return "test"; + } + }; + + var retryPolicy = RetryPolicy.builder() + .maxAttempts(Integer.MAX_VALUE) + .includes(IOException.class) + .build(); + + retryTemplate.setRetryPolicy(retryPolicy); + + assertThat(invocationCount).hasValue(0); + assertThatExceptionOfType(RetryException.class) + .isThrownBy(() -> retryTemplate.execute(retryable)) + .withMessage("Retry policy for operation 'test' exhausted; aborting execution") + .withCauseExactlyInstanceOf(IllegalStateException.class) + .extracting(Throwable::getSuppressed, array(Throwable[].class)) + .satisfiesExactly( + suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(IOException.class), + suppressed2 -> assertThat(suppressed2).isExactlyInstanceOf(FileNotFoundException.class) + ); + // 3 = 1 initial invocation + 2 retry attempts + assertThat(invocationCount).hasValue(3); + } + + static final List includesAndExcludesRetryPolicies = List.of( + argumentSet("Excludes", + RetryPolicy.builder() + .maxAttempts(Integer.MAX_VALUE) + .excludes(FileNotFoundException.class) + .build()), + argumentSet("Includes & Excludes", + RetryPolicy.builder() + .maxAttempts(Integer.MAX_VALUE) + .includes(IOException.class) + .excludes(FileNotFoundException.class) + .build()) + ); + + @ParameterizedTest + @FieldSource("includesAndExcludesRetryPolicies") + void retryWithIncludesAndExcludesRetryPolicies(RetryPolicy retryPolicy) { + retryTemplate.setRetryPolicy(retryPolicy); + + var invocationCount = new AtomicInteger(); + + var retryable = new Retryable<>() { + @Override + public String execute() throws Exception { + return switch (invocationCount.incrementAndGet()) { + case 1 -> throw new IOException(); + case 2 -> throw new IOException(); + case 3 -> throw new CustomFileNotFoundException(); + default -> "success"; + }; + } + + @Override + public String getName() { + return "test"; + } + }; + + assertThat(invocationCount).hasValue(0); + assertThatExceptionOfType(RetryException.class) + .isThrownBy(() -> retryTemplate.execute(retryable)) + .withMessage("Retry policy for operation 'test' exhausted; aborting execution") + .withCauseExactlyInstanceOf(CustomFileNotFoundException.class) + .extracting(Throwable::getSuppressed, array(Throwable[].class)) + .satisfiesExactly( + suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(IOException.class), + suppressed2 -> assertThat(suppressed2).isExactlyInstanceOf(IOException.class) + ); + // 3 = 1 initial invocation + 2 retry attempts + assertThat(invocationCount).hasValue(3); + } + + + @SuppressWarnings("serial") + private static class CustomFileNotFoundException extends FileNotFoundException { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/retry/support/CompositeRetryListenerTests.java b/spring-core/src/test/java/org/springframework/core/retry/support/CompositeRetryListenerTests.java new file mode 100644 index 0000000000..64dcdda363 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/retry/support/CompositeRetryListenerTests.java @@ -0,0 +1,92 @@ +/* + * 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.core.retry.support; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.retry.RetryExecution; +import org.springframework.core.retry.RetryListener; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link CompositeRetryListener}. + * + * @author Mahmoud Ben Hassine + * @author Sam Brannen + * @since 7.0 + */ +class CompositeRetryListenerTests { + + private final RetryListener listener1 = mock(); + private final RetryListener listener2 = mock(); + private final RetryListener listener3 = mock(); + private final RetryExecution retryExecution = mock(); + + private final CompositeRetryListener compositeRetryListener = + new CompositeRetryListener(List.of(listener1, listener2)); + + + @BeforeEach + void addListener() { + compositeRetryListener.addListener(listener3); + } + + @Test + void beforeRetry() { + compositeRetryListener.beforeRetry(retryExecution); + + verify(listener1).beforeRetry(retryExecution); + verify(listener2).beforeRetry(retryExecution); + verify(listener3).beforeRetry(retryExecution); + } + + @Test + void onRetrySuccess() { + Object result = new Object(); + compositeRetryListener.onRetrySuccess(retryExecution, result); + + verify(listener1).onRetrySuccess(retryExecution, result); + verify(listener2).onRetrySuccess(retryExecution, result); + verify(listener3).onRetrySuccess(retryExecution, result); + } + + @Test + void onRetryFailure() { + Exception exception = new Exception(); + compositeRetryListener.onRetryFailure(retryExecution, exception); + + verify(listener1).onRetryFailure(retryExecution, exception); + verify(listener2).onRetryFailure(retryExecution, exception); + verify(listener3).onRetryFailure(retryExecution, exception); + } + + @Test + void onRetryPolicyExhaustion() { + Exception exception = new Exception(); + compositeRetryListener.onRetryPolicyExhaustion(retryExecution, exception); + + verify(listener1).onRetryPolicyExhaustion(retryExecution, exception); + verify(listener2).onRetryPolicyExhaustion(retryExecution, exception); + verify(listener3).onRetryPolicyExhaustion(retryExecution, exception); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/serializer/SerializerTests.java b/spring-core/src/test/java/org/springframework/core/serializer/SerializerTests.java index f26f9b5b7b..02d671ee81 100644 --- a/spring-core/src/test/java/org/springframework/core/serializer/SerializerTests.java +++ b/spring-core/src/test/java/org/springframework/core/serializer/SerializerTests.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. @@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.serializer.support.SerializationDelegate; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link Serializer}, {@link Deserializer}, and {@link SerializationDelegate}. diff --git a/spring-core/src/test/java/org/springframework/core/task/SimpleAsyncTaskExecutorTests.java b/spring-core/src/test/java/org/springframework/core/task/SimpleAsyncTaskExecutorTests.java index c7f4bd9d3b..27ea6a7053 100644 --- a/spring-core/src/test/java/org/springframework/core/task/SimpleAsyncTaskExecutorTests.java +++ b/spring-core/src/test/java/org/springframework/core/task/SimpleAsyncTaskExecutorTests.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 org.junit.jupiter.api.Test; import org.springframework.util.ConcurrencyThrottleSupport; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -31,6 +32,23 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; */ class SimpleAsyncTaskExecutorTests { + @Test + void isActiveUntilClose() { + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(); + assertThat(executor.isActive()).isTrue(); + assertThat(executor.isThrottleActive()).isFalse(); + executor.close(); + assertThat(executor.isActive()).isFalse(); + assertThat(executor.isThrottleActive()).isFalse(); + } + + @Test + void throwsExceptionWhenSuppliedWithNullRunnable() { + try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) { + assertThatIllegalArgumentException().isThrownBy(() -> executor.execute(null)); + } + } + @Test void cannotExecuteWhenConcurrencyIsSwitchedOff() { try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) { @@ -41,35 +59,34 @@ class SimpleAsyncTaskExecutorTests { } @Test - void throttleIsNotActiveByDefault() { + void taskRejectedWhenConcurrencyLimitReached() { try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) { - assertThat(executor.isThrottleActive()).as("Concurrency throttle must not default to being active (on)").isFalse(); + executor.setConcurrencyLimit(1); + executor.setRejectTasksWhenLimitReached(true); + assertThat(executor.isThrottleActive()).isTrue(); + executor.execute(new NoOpRunnable()); + assertThatExceptionOfType(TaskRejectedException.class).isThrownBy(() -> executor.execute(new NoOpRunnable())); } } @Test void threadNameGetsSetCorrectly() { - final String customPrefix = "chankPop#"; - final Object monitor = new Object(); - SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(customPrefix); - ThreadNameHarvester task = new ThreadNameHarvester(monitor); - executeAndWait(executor, task, monitor); - assertThat(task.getThreadName()).startsWith(customPrefix); + String customPrefix = "chankPop#"; + Object monitor = new Object(); + try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(customPrefix)) { + ThreadNameHarvester task = new ThreadNameHarvester(monitor); + executeAndWait(executor, task, monitor); + assertThat(task.getThreadName()).startsWith(customPrefix); + } } @Test void threadFactoryOverridesDefaults() { - final Object monitor = new Object(); - SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(runnable -> new Thread(runnable, "test")); - ThreadNameHarvester task = new ThreadNameHarvester(monitor); - executeAndWait(executor, task, monitor); - assertThat(task.getThreadName()).isEqualTo("test"); - } - - @Test - void throwsExceptionWhenSuppliedWithNullRunnable() { - try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) { - assertThatIllegalArgumentException().isThrownBy(() -> executor.execute(null)); + Object monitor = new Object(); + try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(runnable -> new Thread(runnable, "test"))) { + ThreadNameHarvester task = new ThreadNameHarvester(monitor); + executeAndWait(executor, task, monitor); + assertThat(task.getThreadName()).isEqualTo("test"); } } @@ -89,7 +106,12 @@ class SimpleAsyncTaskExecutorTests { @Override public void run() { - // no-op + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } } } diff --git a/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java index c1e2d7a78e..35ad18fdf7 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java @@ -161,6 +161,12 @@ public abstract class AbstractAnnotationMetadataTests { assertThat(get(TestSubInterface.class).getSuperClassName()).isIn(null, "java.lang.Object"); } + @Test + void getSuperClassNameWhenPackageInfoReturnsNull() throws Exception { + Class packageClass = Class.forName(getClass().getPackageName() + ".package-info"); + assertThat(get(packageClass).getSuperClassName()).isNull(); + } + @Test void getInterfaceNamesWhenHasInterfacesReturnsNames() { assertThat(get(TestSubclass.class).getInterfaceNames()).containsExactly(TestInterface.class.getName()); @@ -178,6 +184,13 @@ public abstract class AbstractAnnotationMetadataTests { TestMemberClass.TestMemberClassInnerClass.class.getName(), TestMemberClass.TestMemberClassInnerInterface.class.getName()); } + @Test + void getMemberClassNamesWhenHasNestedMemberClassesReturnsOnlyFirstLevel() { + assertThat(get(TestNestedMemberClass.class).getMemberClassNames()).containsOnly( + TestNestedMemberClass.TestMemberClassInnerClassA.class.getName(), + TestNestedMemberClass.TestMemberClassInnerClassB.class.getName()); + } + @Test void getMemberClassNamesWhenHasNoMemberClassesReturnsEmptyArray() { assertThat(get(TestClass.class).getMemberClassNames()).isEmpty(); @@ -214,6 +227,22 @@ public abstract class AbstractAnnotationMetadataTests { } + public static class TestNestedMemberClass { + + public static class TestMemberClassInnerClassA { + + public static class TestMemberClassInnerClassAA { + + } + + } + + public static class TestMemberClassInnerClassB { + + } + + } + } @Nested @@ -279,6 +308,14 @@ public abstract class AbstractAnnotationMetadataTests { assertThat(attributes.get("mv").get(0)).isEqualTo(values); } + @Test + void getAnnotationAttributeIntType() { + MultiValueMap attributes = + get(WithIntType.class).getAllAnnotationAttributes(ComplexAttributes.class.getName()); + assertThat(attributes).containsOnlyKeys("names", "count", "type", "subAnnotation"); + assertThat(attributes.get("type")).contains(int.class); + } + @Test void getRepeatableReturnsAttributes() { MultiValueMap attributes = @@ -416,12 +453,19 @@ public abstract class AbstractAnnotationMetadataTests { } + @ComplexAttributes(names = {"first", "second"}, count = TestEnum.ONE, type = TestEnum.class, subAnnotation = @SubAnnotation(name="spring")) @Metadata(mv = {42}) public static class WithComplexAttributeTypes { } + @ComplexAttributes(names = "void", count = TestEnum.ONE, type = int.class, + subAnnotation = @SubAnnotation(name="spring")) + public static class WithIntType { + + } + @Retention(RetentionPolicy.RUNTIME) public @interface ComplexAttributes { diff --git a/spring-core/src/test/java/org/springframework/core/type/classreading/DefaultAnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/classreading/DefaultAnnotationMetadataTests.java new file mode 100644 index 0000000000..1a2b01ed9b --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/classreading/DefaultAnnotationMetadataTests.java @@ -0,0 +1,42 @@ +/* + * 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.core.type.classreading; + +import org.springframework.core.type.AbstractAnnotationMetadataTests; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Tests for {@link SimpleAnnotationMetadata} and + * {@link SimpleAnnotationMetadataReadingVisitor} on Java < 24, + * and for the ClassFile API variant on Java >= 24. + * + * @author Phillip Webb + */ +class DefaultAnnotationMetadataTests extends AbstractAnnotationMetadataTests { + + @Override + protected AnnotationMetadata get(Class source) { + try { + return MetadataReaderFactory.create(source.getClassLoader()) + .getMetadataReader(source.getName()).getAnnotationMetadata(); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataTests.java index 27b53e3de4..43e9fc8481 100644 --- a/spring-core/src/test/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataTests.java @@ -21,8 +21,7 @@ import org.springframework.core.type.AnnotationMetadata; /** * Tests for {@link SimpleAnnotationMetadata} and - * {@link SimpleAnnotationMetadataReadingVisitor} on Java < 24, - * and for the ClassFile API variant on Java >= 24. + * {@link SimpleAnnotationMetadataReadingVisitor}. * * @author Phillip Webb */ @@ -31,8 +30,9 @@ class SimpleAnnotationMetadataTests extends AbstractAnnotationMetadataTests { @Override protected AnnotationMetadata get(Class source) { try { - return MetadataReaderFactory.create(source.getClassLoader()) - .getMetadataReader(source.getName()).getAnnotationMetadata(); + return new SimpleMetadataReaderFactory( + source.getClassLoader()).getMetadataReader( + source.getName()).getAnnotationMetadata(); } catch (Exception ex) { throw new IllegalStateException(ex); diff --git a/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java b/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java index e1352e5eec..6de3602cbf 100644 --- a/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java +++ b/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.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. @@ -491,8 +491,8 @@ class AntPathMatcherTests { assertThat(comparator.compare("/hotels/{hotel}/bookings/{booking}", "/hotels/{hotel}/booking")).isEqualTo(1); // SPR-10550 - assertThat(comparator.compare("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}", "/**")).isEqualTo(-1); - assertThat(comparator.compare("/**", "/hotels/{hotel}/bookings/{booking}/cutomers/{customer}")).isEqualTo(1); + assertThat(comparator.compare("/hotels/{hotel}/bookings/{booking}/customers/{customer}", "/**")).isEqualTo(-1); + assertThat(comparator.compare("/**", "/hotels/{hotel}/bookings/{booking}/customers/{customer}")).isEqualTo(1); assertThat(comparator.compare("/**", "/**")).isEqualTo(0); assertThat(comparator.compare("/hotels/{hotel}", "/hotels/*")).isEqualTo(-1); @@ -505,8 +505,8 @@ class AntPathMatcherTests { assertThat(comparator.compare("/hotels/{hotel}", "/hotels/{hotel}.*")).isEqualTo(2); // SPR-6741 - assertThat(comparator.compare("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}", "/hotels/**")).isEqualTo(-1); - assertThat(comparator.compare("/hotels/**", "/hotels/{hotel}/bookings/{booking}/cutomers/{customer}")).isEqualTo(1); + assertThat(comparator.compare("/hotels/{hotel}/bookings/{booking}/customers/{customer}", "/hotels/**")).isEqualTo(-1); + assertThat(comparator.compare("/hotels/**", "/hotels/{hotel}/bookings/{booking}/customers/{customer}")).isEqualTo(1); assertThat(comparator.compare("/hotels/foo/bar/**", "/hotels/{hotel}")).isEqualTo(1); assertThat(comparator.compare("/hotels/{hotel}", "/hotels/foo/bar/**")).isEqualTo(-1); diff --git a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java index 107e0bd16b..7c89894c68 100644 --- a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.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. @@ -228,7 +228,7 @@ class ClassUtilsTests { @Test void getShortNameAsProperty() { - String shortName = ClassUtils.getShortNameAsProperty(this.getClass()); + String shortName = ClassUtils.getShortNameAsProperty(getClass()); assertThat(shortName).as("Class name did not match").isEqualTo("classUtilsTests"); } diff --git a/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java b/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java index bfbdeadaa1..0322b34d09 100644 --- a/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.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,10 +18,10 @@ package org.springframework.util; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; -import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -42,6 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Rob Harrop * @author Juergen Hoeller * @author Rick Evans + * @author Sam Brannen */ class CollectionUtilsTests { @@ -100,19 +101,24 @@ class CollectionUtilsTests { } @Test - void contains() { + void containsWithIterator() { assertThat(CollectionUtils.contains((Iterator) null, "myElement")).isFalse(); - assertThat(CollectionUtils.contains((Enumeration) null, "myElement")).isFalse(); - assertThat(CollectionUtils.contains(new ArrayList().iterator(), "myElement")).isFalse(); - assertThat(CollectionUtils.contains(new Hashtable().keys(), "myElement")).isFalse(); + assertThat(CollectionUtils.contains(List.of().iterator(), "myElement")).isFalse(); - List list = new ArrayList<>(); - list.add("myElement"); + List list = Arrays.asList("myElement", null); assertThat(CollectionUtils.contains(list.iterator(), "myElement")).isTrue(); + assertThat(CollectionUtils.contains(list.iterator(), null)).isTrue(); + } - Hashtable ht = new Hashtable<>(); - ht.put("myElement", "myValue"); - assertThat(CollectionUtils.contains(ht.keys(), "myElement")).isTrue(); + @Test + void containsWithEnumeration() { + assertThat(CollectionUtils.contains((Enumeration) null, "myElement")).isFalse(); + assertThat(CollectionUtils.contains(Collections.enumeration(List.of()), "myElement")).isFalse(); + + List list = Arrays.asList("myElement", null); + Enumeration enumeration = Collections.enumeration(list); + assertThat(CollectionUtils.contains(enumeration, "myElement")).isTrue(); + assertThat(CollectionUtils.contains(enumeration, null)).isTrue(); } @Test @@ -128,39 +134,49 @@ class CollectionUtilsTests { candidates.add("abc"); assertThat(CollectionUtils.containsAny(source, candidates)).isTrue(); + candidates.remove("def"); assertThat(CollectionUtils.containsAny(source, candidates)).isTrue(); + candidates.remove("abc"); assertThat(CollectionUtils.containsAny(source, candidates)).isFalse(); + + source.add(null); + assertThat(CollectionUtils.containsAny(source, candidates)).isFalse(); + + candidates.add(null); + assertThat(CollectionUtils.containsAny(source, candidates)).isTrue(); } @Test void containsInstanceWithNullCollection() { - assertThat(CollectionUtils.containsInstance(null, this)).as("Must return false if supplied Collection argument is null").isFalse(); + assertThat(CollectionUtils.containsInstance(null, this)).isFalse(); } @Test void containsInstanceWithInstancesThatAreEqualButDistinct() { - List list = new ArrayList<>(); - list.add(new Instance("fiona")); - assertThat(CollectionUtils.containsInstance(list, new Instance("fiona"))).as("Must return false if instance is not in the supplied Collection argument").isFalse(); + List list = List.of(new Instance("fiona")); + assertThat(CollectionUtils.containsInstance(list, new Instance("fiona"))).isFalse(); } @Test void containsInstanceWithSameInstance() { - List list = new ArrayList<>(); - list.add(new Instance("apple")); - Instance instance = new Instance("fiona"); - list.add(instance); - assertThat(CollectionUtils.containsInstance(list, instance)).as("Must return true if instance is in the supplied Collection argument").isTrue(); + Instance fiona = new Instance("fiona"); + Instance apple = new Instance("apple"); + + List list = List.of(fiona, apple); + assertThat(CollectionUtils.containsInstance(list, fiona)).isTrue(); } @Test void containsInstanceWithNullInstance() { - List list = new ArrayList<>(); - list.add(new Instance("apple")); - list.add(new Instance("fiona")); - assertThat(CollectionUtils.containsInstance(list, null)).as("Must return false if null instance is supplied").isFalse(); + Instance fiona = new Instance("fiona"); + + List list = List.of(fiona); + assertThat(CollectionUtils.containsInstance(list, null)).isFalse(); + + list = Arrays.asList(fiona, null); + assertThat(CollectionUtils.containsInstance(list, null)).isTrue(); } @Test @@ -176,31 +192,40 @@ class CollectionUtilsTests { candidates.add("abc"); assertThat(CollectionUtils.findFirstMatch(source, candidates)).isEqualTo("def"); + + source.clear(); + source.add(null); + assertThat(CollectionUtils.findFirstMatch(source, candidates)).isNull(); + + candidates.add(null); + assertThat(CollectionUtils.findFirstMatch(source, candidates)).isNull(); } @Test void findValueOfType() { - List integerList = new ArrayList<>(); - integerList.add(1); - assertThat(CollectionUtils.findValueOfType(integerList, Integer.class)).isEqualTo(1); + assertThat(CollectionUtils.findValueOfType(List.of(1), Integer.class)).isEqualTo(1); - Set integerSet = new HashSet<>(); - integerSet.add(2); - assertThat(CollectionUtils.findValueOfType(integerSet, Integer.class)).isEqualTo(2); + assertThat(CollectionUtils.findValueOfType(Set.of(2), Integer.class)).isEqualTo(2); + } + + @Test + void findValueOfTypeWithNullType() { + assertThat(CollectionUtils.findValueOfType(List.of(1), (Class) null)).isEqualTo(1); + } + + @Test + void findValueOfTypeWithNullCollection() { + assertThat(CollectionUtils.findValueOfType(null, Integer.class)).isNull(); } @Test void findValueOfTypeWithEmptyCollection() { - List emptyList = new ArrayList<>(); - assertThat(CollectionUtils.findValueOfType(emptyList, Integer.class)).isNull(); + assertThat(CollectionUtils.findValueOfType(List.of(), Integer.class)).isNull(); } @Test void findValueOfTypeWithMoreThanOneValue() { - List integerList = new ArrayList<>(); - integerList.add(1); - integerList.add(2); - assertThat(CollectionUtils.findValueOfType(integerList, Integer.class)).isNull(); + assertThat(CollectionUtils.findValueOfType(List.of(1, 2), Integer.class)).isNull(); } @Test @@ -381,7 +406,7 @@ class CollectionUtilsTests { if (this == rhs) { return true; } - if (rhs == null || this.getClass() != rhs.getClass()) { + if (rhs == null || getClass() != rhs.getClass()) { return false; } Instance instance = (Instance) rhs; diff --git a/spring-core/src/test/java/org/springframework/util/FileSystemUtilsTests.java b/spring-core/src/test/java/org/springframework/util/FileSystemUtilsTests.java index c121d1a337..c5bcb66574 100644 --- a/spring-core/src/test/java/org/springframework/util/FileSystemUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/FileSystemUtilsTests.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,19 +18,22 @@ package org.springframework.util; import java.io.File; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import static org.assertj.core.api.Assertions.assertThat; /** + * Tests for {@link FileSystemUtils}. + * * @author Rob Harrop + * @author Sam Brannen */ class FileSystemUtilsTests { @Test - void deleteRecursively() throws Exception { - File root = new File("./tmp/root"); + void deleteRecursively(@TempDir File tempDir) throws Exception { + File root = new File(tempDir, "root"); File child = new File(root, "child"); File grandchild = new File(child, "grandchild"); @@ -53,8 +56,8 @@ class FileSystemUtilsTests { } @Test - void copyRecursively() throws Exception { - File src = new File("./tmp/src"); + void copyRecursively(@TempDir File tempDir) throws Exception { + File src = new File(tempDir, "src"); File child = new File(src, "child"); File grandchild = new File(child, "grandchild"); @@ -68,7 +71,7 @@ class FileSystemUtilsTests { assertThat(grandchild).exists(); assertThat(bar).exists(); - File dest = new File("./dest"); + File dest = new File(tempDir, "/dest"); FileSystemUtils.copyRecursively(src, dest); assertThat(dest).exists(); @@ -78,17 +81,4 @@ class FileSystemUtilsTests { assertThat(src).doesNotExist(); } - - @AfterEach - void tearDown() { - File tmp = new File("./tmp"); - if (tmp.exists()) { - FileSystemUtils.deleteRecursively(tmp); - } - File dest = new File("./dest"); - if (dest.exists()) { - FileSystemUtils.deleteRecursively(dest); - } - } - } 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 b4618c090d..d2ef171a30 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 @@ class PatternMatchUtilsTests { 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 @@ class PatternMatchUtilsTests { 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 @@ class PatternMatchUtilsTests { 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 @@ class PatternMatchUtilsTests { 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/PlaceholderParserTests.java b/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java index 53182acf88..b16f741517 100644 --- a/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java +++ b/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java @@ -16,7 +16,7 @@ package org.springframework.util; -import java.util.Properties; +import java.util.Map; import java.util.stream.Stream; import org.junit.jupiter.api.Nested; @@ -36,13 +36,13 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests for {@link PlaceholderParser}. * * @author Stephane Nicoll + * @author Sam Brannen */ class PlaceholderParserTests { @@ -54,11 +54,11 @@ class PlaceholderParserTests { @ParameterizedTest(name = "{0} -> {1}") @MethodSource("placeholders") void placeholderIsReplaced(String text, String expected) { - Properties properties = new Properties(); - properties.setProperty("firstName", "John"); - properties.setProperty("nested0", "first"); - properties.setProperty("nested1", "Name"); - assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + Map properties = Map.of( + "firstName", "John", + "nested0", "first", + "nested1", "Name"); + assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected); } static Stream placeholders() { @@ -79,13 +79,13 @@ class PlaceholderParserTests { @ParameterizedTest(name = "{0} -> {1}") @MethodSource("nestedPlaceholders") void nestedPlaceholdersAreReplaced(String text, String expected) { - Properties properties = new Properties(); - properties.setProperty("p1", "v1"); - properties.setProperty("p2", "v2"); - properties.setProperty("p3", "${p1}:${p2}"); // nested placeholders - properties.setProperty("p4", "${p3}"); // deeply nested placeholders - properties.setProperty("p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder - assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + Map properties = Map.of( + "p1", "v1", + "p2", "v2", + "p3", "${p1}:${p2}", // nested placeholders + "p4", "${p3}", // deeply nested placeholders + "p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder + assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected); } static Stream nestedPlaceholders() { @@ -101,19 +101,15 @@ class PlaceholderParserTests { @Test void parseWithSinglePlaceholder() { PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John"); - assertThat(this.parser.replacePlaceholders("${firstName}", resolver)) - .isEqualTo("John"); - verify(resolver).resolvePlaceholder("firstName"); - verifyNoMoreInteractions(resolver); + assertThat(this.parser.replacePlaceholders("${firstName}", resolver)).isEqualTo("John"); + verifyPlaceholderResolutions(resolver, "firstName"); } @Test void parseWithPlaceholderAndPrefixText() { PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John"); - assertThat(this.parser.replacePlaceholders("This is ${firstName}", resolver)) - .isEqualTo("This is John"); - verify(resolver).resolvePlaceholder("firstName"); - verifyNoMoreInteractions(resolver); + assertThat(this.parser.replacePlaceholders("This is ${firstName}", resolver)).isEqualTo("This is John"); + verifyPlaceholderResolutions(resolver, "firstName"); } @Test @@ -121,31 +117,25 @@ class PlaceholderParserTests { PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John", "lastName", "Smith"); assertThat(this.parser.replacePlaceholders("User: ${firstName} - ${lastName}.", resolver)) .isEqualTo("User: John - Smith."); - verify(resolver).resolvePlaceholder("firstName"); - verify(resolver).resolvePlaceholder("lastName"); - verifyNoMoreInteractions(resolver); + verifyPlaceholderResolutions(resolver, "firstName", "lastName"); } @Test void parseWithNestedPlaceholderInKey() { - PlaceholderResolver resolver = mockPlaceholderResolver( - "nested", "Name", "firstName", "John"); - assertThat(this.parser.replacePlaceholders("${first${nested}}", resolver)) - .isEqualTo("John"); + PlaceholderResolver resolver = mockPlaceholderResolver("nested", "Name", "firstName", "John"); + assertThat(this.parser.replacePlaceholders("${first${nested}}", resolver)).isEqualTo("John"); verifyPlaceholderResolutions(resolver, "nested", "firstName"); } @Test void parseWithMultipleNestedPlaceholdersInKey() { - PlaceholderResolver resolver = mockPlaceholderResolver( - "nested0", "first", "nested1", "Name", "firstName", "John"); - assertThat(this.parser.replacePlaceholders("${${nested0}${nested1}}", resolver)) - .isEqualTo("John"); + PlaceholderResolver resolver = mockPlaceholderResolver("nested0", "first", "nested1", "Name", "firstName", "John"); + assertThat(this.parser.replacePlaceholders("${${nested0}${nested1}}", resolver)).isEqualTo("John"); verifyPlaceholderResolutions(resolver, "nested0", "nested1", "firstName"); } @Test - void placeholdersWithSeparatorAreHandledAsIs() { + void placeholderValueContainingSeparatorIsHandledAsIs() { PlaceholderResolver resolver = mockPlaceholderResolver("my:test", "value"); assertThat(this.parser.replacePlaceholders("${my:test}", resolver)).isEqualTo("value"); verifyPlaceholderResolutions(resolver, "my:test"); @@ -153,17 +143,20 @@ class PlaceholderParserTests { @Test void placeholdersWithoutEscapeCharAreNotEscaped() { - PlaceholderResolver resolver = mockPlaceholderResolver("test", "value"); - assertThat(this.parser.replacePlaceholders("\\${test}", resolver)).isEqualTo("\\value"); - verifyPlaceholderResolutions(resolver, "test"); + PlaceholderResolver resolver = mockPlaceholderResolver("p1", "v1", "p2", "v2", "p3", "v3", "p4", "v4"); + assertThat(this.parser.replacePlaceholders("\\${p1}", resolver)).isEqualTo("\\v1"); + assertThat(this.parser.replacePlaceholders("\\\\${p2}", resolver)).isEqualTo("\\\\v2"); + assertThat(this.parser.replacePlaceholders("\\${p3}\\", resolver)).isEqualTo("\\v3\\"); + assertThat(this.parser.replacePlaceholders("a\\${p4}\\z", resolver)).isEqualTo("a\\v4\\z"); + verifyPlaceholderResolutions(resolver, "p1", "p2", "p3", "p4"); } @Test - void textWithInvalidPlaceholderIsMerged() { + void textWithInvalidPlaceholderSyntaxIsMerged() { String text = "test${of${with${and${"; ParsedValue parsedValue = this.parser.parse(text); - assertThat(parsedValue.parts()).singleElement().isInstanceOfSatisfying( - TextPart.class, textPart -> assertThat(textPart.text()).isEqualTo(text)); + assertThat(parsedValue.parts()).singleElement().isInstanceOfSatisfying(TextPart.class, + textPart -> assertThat(textPart.text()).isEqualTo(text)); } } @@ -176,11 +169,11 @@ class PlaceholderParserTests { @ParameterizedTest(name = "{0} -> {1}") @MethodSource("placeholders") void placeholderIsReplaced(String text, String expected) { - Properties properties = new Properties(); - properties.setProperty("firstName", "John"); - properties.setProperty("nested0", "first"); - properties.setProperty("nested1", "Name"); - assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + Map properties = Map.of( + "firstName", "John", + "nested0", "first", + "nested1", "Name"); + assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected); } static Stream placeholders() { @@ -199,14 +192,14 @@ class PlaceholderParserTests { @ParameterizedTest(name = "{0} -> {1}") @MethodSource("nestedPlaceholders") void nestedPlaceholdersAreReplaced(String text, String expected) { - Properties properties = new Properties(); - properties.setProperty("p1", "v1"); - properties.setProperty("p2", "v2"); - properties.setProperty("p3", "${p1}:${p2}"); // nested placeholders - properties.setProperty("p4", "${p3}"); // deeply nested placeholders - properties.setProperty("p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder - properties.setProperty("p6", "${p1}:${p2}:${bogus:def}"); // unresolvable w/ default - assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + Map properties = Map.of( + "p1", "v1", + "p2", "v2", + "p3", "${p1}:${p2}", // nested placeholders + "p4", "${p3}", // deeply nested placeholders + "p5", "${p1}:${p2}:${bogus}", // unresolvable placeholder + "p6", "${p1}:${p2}:${bogus:def}"); // unresolvable w/ default + assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected); } static Stream nestedPlaceholders() { @@ -225,11 +218,11 @@ class PlaceholderParserTests { @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.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + Map properties = Map.of( + "prefix://my-service", "example-service", + "px", "prefix", + "p1", "${prefix://my-service}"); + assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected); } static Stream exactMatchPlaceholders() { @@ -242,74 +235,55 @@ class PlaceholderParserTests { @Test void parseWithKeyEqualsToText() { PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "Steve"); - assertThat(this.parser.replacePlaceholders("${firstName}", resolver)) - .isEqualTo("Steve"); + assertThat(this.parser.replacePlaceholders("${firstName}", resolver)).isEqualTo("Steve"); verifyPlaceholderResolutions(resolver, "firstName"); } @Test void parseWithHardcodedFallback() { PlaceholderResolver resolver = mockPlaceholderResolver(); - assertThat(this.parser.replacePlaceholders("${firstName:Steve}", resolver)) - .isEqualTo("Steve"); + assertThat(this.parser.replacePlaceholders("${firstName:Steve}", resolver)).isEqualTo("Steve"); verifyPlaceholderResolutions(resolver, "firstName:Steve", "firstName"); } @Test void parseWithNestedPlaceholderInKeyUsingFallback() { PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John"); - assertThat(this.parser.replacePlaceholders("${first${invalid:Name}}", resolver)) - .isEqualTo("John"); + assertThat(this.parser.replacePlaceholders("${first${invalid:Name}}", resolver)).isEqualTo("John"); verifyPlaceholderResolutions(resolver, "invalid:Name", "invalid", "firstName"); } @Test void parseWithFallbackUsingPlaceholder() { PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John"); - assertThat(this.parser.replacePlaceholders("${invalid:${firstName}}", resolver)) - .isEqualTo("John"); + assertThat(this.parser.replacePlaceholders("${invalid:${firstName}}", resolver)).isEqualTo("John"); verifyPlaceholderResolutions(resolver, "invalid", "firstName"); } } - @Nested // Tests with the use of the escape character + /** + * Tests that use the escape character. + */ + @Nested class EscapedTests { private final PlaceholderParser parser = new PlaceholderParser("${", "}", ":", '\\', true); - @ParameterizedTest(name = "{0} -> {1}") - @MethodSource("escapedInNestedPlaceholders") - void escapedSeparatorInNestedPlaceholder(String text, String expected) { - Properties properties = new Properties(); - properties.setProperty("app.environment", "qa"); - properties.setProperty("app.service", "protocol"); - properties.setProperty("protocol://host/qa/name", "protocol://example.com/qa/name"); - properties.setProperty("service/host/qa/name", "https://example.com/qa/name"); - properties.setProperty("service/host/qa/name:value", "https://example.com/qa/name-value"); - assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); - } - - static Stream escapedInNestedPlaceholders() { - return Stream.of( - Arguments.of("${protocol\\://host/${app.environment}/name}", "protocol://example.com/qa/name"), - Arguments.of("${${app.service}\\://host/${app.environment}/name}", "protocol://example.com/qa/name"), - Arguments.of("${service/host/${app.environment}/name:\\value}", "https://example.com/qa/name"), - Arguments.of("${service/host/${name\\:value}/}", "${service/host/${name:value}/}")); - } - @ParameterizedTest(name = "{0} -> {1}") @MethodSource("escapedPlaceholders") void escapedPlaceholderIsNotReplaced(String text, String expected) { - PlaceholderResolver resolver = mockPlaceholderResolver( - "firstName", "John", "nested0", "first", "nested1", "Name", + Map properties = Map.of( + "firstName", "John", "${test}", "John", - "p1", "v1", "p2", "\\${p1:default}", "p3", "${p2}", + "p1", "v1", + "p2", "\\${p1:default}", + "p3", "${p2}", "p4", "adc${p0:\\${p1}}", "p5", "adc${\\${p0}:${p1}}", "p6", "adc${p0:def\\${p1}}", "p7", "adc\\${"); - assertThat(this.parser.replacePlaceholders(text, resolver)).isEqualTo(expected); + assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected); } static Stream escapedPlaceholders() { @@ -324,18 +298,21 @@ class PlaceholderParserTests { Arguments.of("${p4}", "adc${p1}"), Arguments.of("${p5}", "adcv1"), Arguments.of("${p6}", "adcdef${p1}"), - Arguments.of("${p7}", "adc\\${")); - + Arguments.of("${p7}", "adc\\${"), + // Double backslash + Arguments.of("DOMAIN\\\\${user.name}", "DOMAIN\\${user.name}"), + // Triple backslash + Arguments.of("triple\\\\\\${backslash}", "triple\\\\${backslash}"), + // Multiple escaped placeholders + Arguments.of("start\\${prop1}middle\\${prop2}end", "start${prop1}middle${prop2}end") + ); } @ParameterizedTest(name = "{0} -> {1}") @MethodSource("escapedSeparators") void escapedSeparatorIsNotReplaced(String text, String expected) { - Properties properties = new Properties(); - properties.setProperty("first:Name", "John"); - properties.setProperty("nested0", "first"); - properties.setProperty("nested1", "Name"); - assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + Map properties = Map.of("first:Name", "John"); + assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected); } static Stream escapedSeparators() { @@ -345,6 +322,26 @@ class PlaceholderParserTests { ); } + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("escapedSeparatorsInNestedPlaceholders") + void escapedSeparatorInNestedPlaceholderIsNotReplaced(String text, String expected) { + Map properties = Map.of( + "app.environment", "qa", + "app.service", "protocol", + "protocol://host/qa/name", "protocol://example.com/qa/name", + "service/host/qa/name", "https://example.com/qa/name", + "service/host/qa/name:value", "https://example.com/qa/name-value"); + assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected); + } + + static Stream escapedSeparatorsInNestedPlaceholders() { + return Stream.of( + Arguments.of("${protocol\\://host/${app.environment}/name}", "protocol://example.com/qa/name"), + Arguments.of("${${app.service}\\://host/${app.environment}/name}", "protocol://example.com/qa/name"), + Arguments.of("${service/host/${app.environment}/name:\\value}", "https://example.com/qa/name"), + Arguments.of("${service/host/${name\\:value}/}", "${service/host/${name:value}/}")); + } + } @Nested @@ -354,34 +351,38 @@ class PlaceholderParserTests { @Test void textWithCircularReference() { - PlaceholderResolver resolver = mockPlaceholderResolver("pL", "${pR}", "pR", "${pL}"); - assertThatThrownBy(() -> this.parser.replacePlaceholders("${pL}", resolver)) + Map properties = Map.of( + "pL", "${pR}", + "pR", "${pL}"); + assertThatThrownBy(() -> this.parser.replacePlaceholders("${pL}", properties::get)) .isInstanceOf(PlaceholderResolutionException.class) .hasMessage("Circular placeholder reference 'pL' in value \"${pL}\" <-- \"${pR}\" <-- \"${pL}\""); } @Test void unresolvablePlaceholderIsReported() { - PlaceholderResolver resolver = mockPlaceholderResolver(); assertThatExceptionOfType(PlaceholderResolutionException.class) - .isThrownBy(() -> this.parser.replacePlaceholders("${bogus}", resolver)) - .withMessage("Could not resolve placeholder 'bogus' in value \"${bogus}\"") + .isThrownBy(() -> this.parser.replacePlaceholders("X${bogus}Z", placeholderName -> null)) + .withMessage("Could not resolve placeholder 'bogus' in value \"X${bogus}Z\"") .withNoCause(); } @Test void unresolvablePlaceholderInNestedPlaceholderIsReportedWithChain() { - PlaceholderResolver resolver = mockPlaceholderResolver("p1", "v1", "p2", "v2", + Map properties = Map.of( + "p1", "v1", + "p2", "v2", "p3", "${p1}:${p2}:${bogus}"); assertThatExceptionOfType(PlaceholderResolutionException.class) - .isThrownBy(() -> this.parser.replacePlaceholders("${p3}", resolver)) + .isThrownBy(() -> this.parser.replacePlaceholders("${p3}", properties::get)) .withMessage("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\" <-- \"${p3}\"") .withNoCause(); } } - PlaceholderResolver mockPlaceholderResolver(String... pairs) { + + private static PlaceholderResolver mockPlaceholderResolver(String... pairs) { if (pairs.length % 2 == 1) { throw new IllegalArgumentException("size must be even, it is a set of key=value pairs"); } @@ -394,7 +395,7 @@ class PlaceholderParserTests { return resolver; } - void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) { + private static void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) { InOrder ordered = inOrder(mock); for (String placeholder : placeholders) { ordered.verify(mock).resolvePlaceholder(placeholder); diff --git a/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java b/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java index c5e321188e..297b5128db 100644 --- a/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/StringUtilsTests.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. @@ -359,11 +359,11 @@ class StringUtilsTests { assertThat(StringUtils.getFilename(null)).isNull(); assertThat(StringUtils.getFilename("")).isEmpty(); assertThat(StringUtils.getFilename("myfile")).isEqualTo("myfile"); - assertThat(StringUtils.getFilename("mypath/myfile")).isEqualTo("myfile"); + assertThat(StringUtils.getFilename("my/path/myfile")).isEqualTo("myfile"); assertThat(StringUtils.getFilename("myfile.")).isEqualTo("myfile."); assertThat(StringUtils.getFilename("mypath/myfile.")).isEqualTo("myfile."); assertThat(StringUtils.getFilename("myfile.txt")).isEqualTo("myfile.txt"); - assertThat(StringUtils.getFilename("mypath/myfile.txt")).isEqualTo("myfile.txt"); + assertThat(StringUtils.getFilename("my/path/myfile.txt")).isEqualTo("myfile.txt"); } @Test diff --git a/spring-core/src/test/kotlin/org/springframework/aot/hint/BindingReflectionHintsRegistrarKotlinTests.kt b/spring-core/src/test/kotlin/org/springframework/aot/hint/BindingReflectionHintsRegistrarKotlinTests.kt index 40d9b33610..3cc2eaa022 100644 --- a/spring-core/src/test/kotlin/org/springframework/aot/hint/BindingReflectionHintsRegistrarKotlinTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/aot/hint/BindingReflectionHintsRegistrarKotlinTests.kt @@ -80,6 +80,12 @@ class BindingReflectionHintsRegistrarKotlinTests { bindingRegistrar.registerReflectionHints(hints.reflection(), SampleClass::class.java) assertThat(RuntimeHintsPredicates.reflection().onType(SampleClass::class.java)).accepts(hints) } + + @Test + fun `Register reflection hints on serializer function with parameter`() { + bindingRegistrar.registerReflectionHints(hints.reflection(), SampleResult::class.java) + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(SampleResult.Companion::class.java, "serializer")).accepts(hints) + } } @kotlinx.serialization.Serializable @@ -88,3 +94,17 @@ class SampleSerializableClass(val name: String) data class SampleDataClass(val name: String, val isNonNullable: Boolean, val isNullable: Boolean?) class SampleClass(val name: String) + +@kotlinx.serialization.Serializable +data class SampleResult ( + val code: Int, + val message: String, + val data: T, +) { + companion object { + private const val SUCCESS: Int = 200 + private const val FAILURE: Int = 500 + fun success(message: String, data: T) = SampleResult(code = SUCCESS, message = message, data = data) + fun failure(message: String, data: T) = SampleResult(code = FAILURE, message = message, data = data) + } +} diff --git a/spring-core/src/test/kotlin/org/springframework/core/BridgeMethodResolverKotlinTests.kt b/spring-core/src/test/kotlin/org/springframework/core/BridgeMethodResolverKotlinTests.kt index bb94dd3867..f1c8eabc1e 100644 --- a/spring-core/src/test/kotlin/org/springframework/core/BridgeMethodResolverKotlinTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/core/BridgeMethodResolverKotlinTests.kt @@ -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,7 +22,7 @@ import org.junit.jupiter.api.Test /** * Kotlin tests for [BridgeMethodResolver]. * - * @author Sebastien Deleuzes + * @author Sebastien Deleuze */ class BridgeMethodResolverKotlinTests { diff --git a/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt b/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt index 31ebb74927..24a98d61cc 100644 --- a/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt @@ -199,6 +199,15 @@ class CoroutinesUtilsTests { } } + @Test + fun invokeSuspendingFunctionWithNestedValueClassParameter() { + val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithNestedValueClassParameter") } + val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, "foo", null) as Mono + runBlocking { + Assertions.assertThat(mono.awaitSingle()).isEqualTo("foo") + } + } + @Test fun invokeSuspendingFunctionWithValueClassReturnValue() { val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClassReturnValue") } @@ -328,6 +337,11 @@ class CoroutinesUtilsTests { return value.value } + suspend fun suspendingFunctionWithNestedValueClassParameter(value: NestedValueClass): String { + delay(1) + return value.value.value + } + suspend fun suspendingFunctionWithValueClassReturnValue(): ValueClass { delay(1) return ValueClass("foo") @@ -382,6 +396,9 @@ class CoroutinesUtilsTests { @JvmInline value class ValueClass(val value: String) + @JvmInline + value class NestedValueClass(val value: ValueClass) + @JvmInline value class ValueClassWithInit(val value: String) { init { 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 a4cbd1f22e..f1404079bb 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 @@ import org.springframework.core.env.PropertySource; * 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 class MockPropertySource extends PropertiesPropertySource {
 
 	/**
 	 * 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/CodeFlow.java b/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java index aee26ea3c7..43bd339ebe 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java @@ -687,9 +687,8 @@ public class CodeFlow implements Opcodes { * Determine whether the given number is to be considered as an integer * for the purposes of a numeric operation at the bytecode level. * @param number the number to check - * @return {@code true} if it is an {@link Integer}, {@link Short} or {@link Byte} + * @return {@code true} if it is an {@link Integer}, {@link Short}, or {@link Byte} */ - @Contract("null -> false") public static boolean isIntegerForNumericOp(Number number) { return (number instanceof Integer || number instanceof Short || number instanceof Byte); } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java index e205f7c94d..3fb1b2cbfe 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.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,127 +16,116 @@ package org.springframework.expression.spel; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; +import java.util.Optional; +import java.util.Set; -import org.junit.jupiter.api.BeforeEach; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.core.MethodParameter; -import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.expression.TypeConverter; import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; /** - * Expression evaluation where the TypeConverter plugged in is the + * Expression evaluation where the {@link TypeConverter} plugged in uses the * {@link org.springframework.core.convert.support.GenericConversionService}. * * @author Andy Clement * @author Dave Syer + * @author Sam Brannen */ class ExpressionWithConversionTests extends AbstractExpressionTests { - private static List listOfString = new ArrayList<>(); - private static TypeDescriptor typeDescriptorForListOfString = null; - private static List listOfInteger = new ArrayList<>(); - private static TypeDescriptor typeDescriptorForListOfInteger = null; + private static final List listOfString = List.of("1", "2", "3"); + private static final List listOfInteger = List.of(4, 5, 6); - static { - listOfString.add("1"); - listOfString.add("2"); - listOfString.add("3"); - listOfInteger.add(4); - listOfInteger.add(5); - listOfInteger.add(6); - } - - @BeforeEach - void setUp() throws Exception { - ExpressionWithConversionTests.typeDescriptorForListOfString = new TypeDescriptor(ExpressionWithConversionTests.class.getDeclaredField("listOfString")); - ExpressionWithConversionTests.typeDescriptorForListOfInteger = new TypeDescriptor(ExpressionWithConversionTests.class.getDeclaredField("listOfInteger")); - } + private static final TypeDescriptor typeDescriptorForListOfString = + new TypeDescriptor(ReflectionUtils.findField(ExpressionWithConversionTests.class, "listOfString")); + private static final TypeDescriptor typeDescriptorForListOfInteger = + new TypeDescriptor(ReflectionUtils.findField(ExpressionWithConversionTests.class, "listOfInteger")); /** * Test the service can convert what we are about to use in the expression evaluation tests. */ - @Test - void testConversionsAvailable() { - TypeConvertorUsingConversionService tcs = new TypeConvertorUsingConversionService(); + @BeforeAll + @SuppressWarnings("unchecked") + static void verifyConversionsAreSupportedByStandardTypeConverter() { + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + TypeConverter typeConverter = evaluationContext.getTypeConverter(); - // ArrayList containing List to List - Class clazz = typeDescriptorForListOfString.getElementTypeDescriptor().getType(); - assertThat(clazz).isEqualTo(String.class); - List l = (List) tcs.convertValue(listOfInteger, TypeDescriptor.forObject(listOfInteger), typeDescriptorForListOfString); - assertThat(l).isNotNull(); + // List to List + assertThat(typeDescriptorForListOfString.getElementTypeDescriptor().getType()) + .isEqualTo(String.class); + List strings = (List) typeConverter.convertValue(listOfInteger, + typeDescriptorForListOfInteger, typeDescriptorForListOfString); + assertThat(strings).containsExactly("4", "5", "6"); - // ArrayList containing List to List - clazz = typeDescriptorForListOfInteger.getElementTypeDescriptor().getType(); - assertThat(clazz).isEqualTo(Integer.class); - - l = (List) tcs.convertValue(listOfString, TypeDescriptor.forObject(listOfString), typeDescriptorForListOfString); - assertThat(l).isNotNull(); + // List to List + assertThat(typeDescriptorForListOfInteger.getElementTypeDescriptor().getType()) + .isEqualTo(Integer.class); + List integers = (List) typeConverter.convertValue(listOfString, + typeDescriptorForListOfString, typeDescriptorForListOfInteger); + assertThat(integers).containsExactly(1, 2, 3); } + @Test - void testSetParameterizedList() { + void setParameterizedList() { StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); + Expression e = parser.parseExpression("listOfInteger.size()"); assertThat(e.getValue(context, Integer.class)).isZero(); - context.setTypeConverter(new TypeConvertorUsingConversionService()); + // Assign a List to the List field - the component elements should be converted - parser.parseExpression("listOfInteger").setValue(context,listOfString); + parser.parseExpression("listOfInteger").setValue(context, listOfString); // size now 3 assertThat(e.getValue(context, Integer.class)).isEqualTo(3); - Class clazz = parser.parseExpression("listOfInteger[1].getClass()").getValue(context, Class.class); // element type correctly Integer + // element type correctly Integer + Class clazz = parser.parseExpression("listOfInteger[1].getClass()").getValue(context, Class.class); assertThat(clazz).isEqualTo(Integer.class); } @Test - void testCoercionToCollectionOfPrimitive() throws Exception { + void coercionToCollectionOfPrimitive() throws Exception { class TestTarget { @SuppressWarnings("unused") public int sum(Collection numbers) { - int total = 0; - for (int i : numbers) { - total += i; - } - return total; + return numbers.stream().reduce(0, (a, b) -> a + b); } } StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + TypeConverter typeConverter = evaluationContext.getTypeConverter(); TypeDescriptor collectionType = new TypeDescriptor(new MethodParameter(TestTarget.class.getDeclaredMethod( "sum", Collection.class), 0)); // The type conversion is possible - assertThat(evaluationContext.getTypeConverter() - .canConvert(TypeDescriptor.valueOf(String.class), collectionType)).isTrue(); + assertThat(typeConverter.canConvert(TypeDescriptor.valueOf(String.class), collectionType)).isTrue(); // ... and it can be done successfully - assertThat(evaluationContext.getTypeConverter().convertValue("1,2,3,4", TypeDescriptor.valueOf(String.class), collectionType).toString()).isEqualTo("[1, 2, 3, 4]"); + assertThat(typeConverter.convertValue("1,2,3,4", TypeDescriptor.valueOf(String.class), collectionType)) + .hasToString("[1, 2, 3, 4]"); evaluationContext.setVariable("target", new TestTarget()); // OK up to here, so the evaluation should be fine... - // ... but this fails - int result = (Integer) parser.parseExpression("#target.sum(#root)").getValue(evaluationContext, "1,2,3,4"); - assertThat(result).as("Wrong result: " + result).isEqualTo(10); - + int sum = parser.parseExpression("#target.sum(#root)").getValue(evaluationContext, "1,2,3,4", int.class); + assertThat(sum).isEqualTo(10); } @Test - void testConvert() { + void convert() { Foo root = new Foo("bar"); - Collection foos = Collections.singletonList("baz"); + Collection foos = Set.of("baz"); StandardEvaluationContext context = new StandardEvaluationContext(root); @@ -163,26 +152,28 @@ class ExpressionWithConversionTests extends AbstractExpressionTests { expression = parser.parseExpression("setFoos(getFoosAsObjects())"); expression.getValue(context); baz = root.getFoos().iterator().next(); - assertThat(baz.value).isEqualTo("baz"); + assertThat(baz.value).isEqualTo("quux"); } + @Test // gh-34544 + void convertOptionalToContainedTargetForMethodInvocations() { + StandardEvaluationContext context = new StandardEvaluationContext(new JediService()); - /** - * Type converter that uses the core conversion service. - */ - private static class TypeConvertorUsingConversionService implements TypeConverter { + // Verify findByName('Yoda') returns an Optional. + Expression expression = parser.parseExpression("findByName('Yoda') instanceof T(java.util.Optional)"); + assertThat(expression.getValue(context, Boolean.class)).isTrue(); - private final ConversionService service = new DefaultConversionService(); + // Verify we can pass a Jedi directly to greet(). + expression = parser.parseExpression("greet(findByName('Yoda').get())"); + assertThat(expression.getValue(context, String.class)).isEqualTo("Hello, Yoda"); - @Override - public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) { - return this.service.canConvert(sourceType, targetType); - } + // Verify that an Optional will be unwrapped to a Jedi to pass to greet(). + expression = parser.parseExpression("greet(findByName('Yoda'))"); + assertThat(expression.getValue(context, String.class)).isEqualTo("Hello, Yoda"); - @Override - public Object convertValue(Object value, TypeDescriptor sourceType, TypeDescriptor targetType) throws EvaluationException { - return this.service.convert(value, sourceType, targetType); - } + // Verify that an empty Optional will be converted to null to pass to greet(). + expression = parser.parseExpression("greet(findByName(''))"); + assertThat(expression.getValue(context, String.class)).isEqualTo("Hello, null"); } @@ -205,11 +196,28 @@ class ExpressionWithConversionTests extends AbstractExpressionTests { } public Collection getFoosAsStrings() { - return Collections.singletonList("baz"); + return Set.of("baz"); } public Collection getFoosAsObjects() { - return Collections.singletonList("baz"); + return Set.of("quux"); + } + } + + record Jedi(String name) { + } + + static class JediService { + + public Optional findByName(String name) { + if (name.isEmpty()) { + return Optional.empty(); + } + return Optional.of(new Jedi(name)); + } + + public String greet(@Nullable Jedi jedi) { + return "Hello, " + (jedi != null ? jedi.name() : null); } } 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 b0178d144a..a3058c1ab9 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 @@ -276,7 +276,7 @@ class VariableAndFunctionTests extends AbstractExpressionTests { @Test void functionMethodMustBeStatic() throws Exception { - context.registerFunction("nonStatic", this.getClass().getMethod("nonStatic")); + context.registerFunction("nonStatic", getClass().getMethod("nonStatic")); SpelExpression expression = parser.parseRaw("#nonStatic()"); assertThatExceptionOfType(SpelEvaluationException.class) .isThrownBy(() -> expression.getValue(context)) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java index 94fd24f425..b7423196ea 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.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. @@ -367,9 +367,9 @@ public interface JdbcOperations { * @param rse a callback that will extract results * @return an arbitrary result object, as returned by the ResultSetExtractor * @throws DataAccessException if the query fails - * @deprecated as of 5.3, in favor of {@link #query(String, ResultSetExtractor, Object...)} + * @deprecated in favor of {@link #query(String, ResultSetExtractor, Object...)} */ - @Deprecated + @Deprecated(since = "5.3") @Nullable T query(String sql, @Nullable Object @Nullable [] args, ResultSetExtractor rse) throws DataAccessException; /** @@ -438,9 +438,9 @@ public interface JdbcOperations { * only the argument value but also the SQL type and optionally the scale * @param rch a callback that will extract results, one row at a time * @throws DataAccessException if the query fails - * @deprecated as of 5.3, in favor of {@link #query(String, RowCallbackHandler, Object...)} + * @deprecated in favor of {@link #query(String, RowCallbackHandler, Object...)} */ - @Deprecated + @Deprecated(since = "5.3") void query(String sql, @Nullable Object @Nullable [] args, RowCallbackHandler rch) throws DataAccessException; /** @@ -514,9 +514,9 @@ public interface JdbcOperations { * @param rowMapper a callback that will map one object per row * @return the result List, containing mapped objects * @throws DataAccessException if the query fails - * @deprecated as of 5.3, in favor of {@link #query(String, RowMapper, Object...)} + * @deprecated in favor of {@link #query(String, RowMapper, Object...)} */ - @Deprecated + @Deprecated(since = "5.3") List query(String sql, @Nullable Object @Nullable [] args, RowMapper rowMapper) throws DataAccessException; /** @@ -621,9 +621,9 @@ public interface JdbcOperations { * @throws org.springframework.dao.IncorrectResultSizeDataAccessException * if the query does not return exactly one row * @throws DataAccessException if the query fails - * @deprecated as of 5.3, in favor of {@link #queryForObject(String, RowMapper, Object...)} + * @deprecated in favor of {@link #queryForObject(String, RowMapper, Object...)} */ - @Deprecated + @Deprecated(since = "5.3") @Nullable T queryForObject(String sql, @Nullable Object @Nullable [] args, RowMapper rowMapper) throws DataAccessException; /** @@ -685,9 +685,9 @@ public interface JdbcOperations { * if the query does not return a row containing a single column * @throws DataAccessException if the query fails * @see #queryForObject(String, Class) - * @deprecated as of 5.3, in favor of {@link #queryForObject(String, Class, Object...)} + * @deprecated in favor of {@link #queryForObject(String, Class, Object...)} */ - @Deprecated + @Deprecated(since = "5.3") @Nullable T queryForObject(String sql, @Nullable Object @Nullable [] args, Class requiredType) throws DataAccessException; /** @@ -789,9 +789,9 @@ public interface JdbcOperations { * @throws DataAccessException if the query fails * @see #queryForList(String, Class) * @see SingleColumnRowMapper - * @deprecated as of 5.3, in favor of {@link #queryForList(String, Class, Object...)} + * @deprecated in favor of {@link #queryForList(String, Class, Object...)} */ - @Deprecated + @Deprecated(since = "5.3") List queryForList(String sql, @Nullable Object @Nullable [] args, Class elementType) throws DataAccessException; /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index a98804f2d8..227f494da1 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -420,7 +420,7 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { } @Override - public void execute(final String sql) throws DataAccessException { + public void execute(String sql) throws DataAccessException { if (logger.isDebugEnabled()) { logger.debug("Executing SQL statement [" + sql + "]"); } @@ -442,7 +442,7 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { } @Override - public @Nullable T query(final String sql, final ResultSetExtractor rse) throws DataAccessException { + public @Nullable T query(String sql, ResultSetExtractor rse) throws DataAccessException { Assert.notNull(sql, "SQL must not be null"); Assert.notNull(rse, "ResultSetExtractor must not be null"); if (logger.isDebugEnabled()) { @@ -535,7 +535,7 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { } @Override - public int update(final String sql) throws DataAccessException { + public int update(String sql) throws DataAccessException { Assert.notNull(sql, "SQL must not be null"); if (logger.isDebugEnabled()) { logger.debug("Executing SQL update [" + sql + "]"); @@ -561,7 +561,7 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { } @Override - public int[] batchUpdate(final String... sql) throws DataAccessException { + public int[] batchUpdate(String... sql) throws DataAccessException { Assert.notEmpty(sql, "SQL array must not be empty"); if (logger.isDebugEnabled()) { logger.debug("Executing SQL batch update of " + sql.length + " statements"); @@ -701,28 +701,25 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { * @throws DataAccessException if there is any problem */ public @Nullable T query( - PreparedStatementCreator psc, final @Nullable PreparedStatementSetter pss, final ResultSetExtractor rse) + PreparedStatementCreator psc, @Nullable PreparedStatementSetter pss, ResultSetExtractor rse) throws DataAccessException { Assert.notNull(rse, "ResultSetExtractor must not be null"); logger.debug("Executing prepared SQL query"); - return execute(psc, new PreparedStatementCallback<>() { - @Override - public @Nullable T doInPreparedStatement(PreparedStatement ps) throws SQLException { - ResultSet rs = null; - try { - if (pss != null) { - pss.setValues(ps); - } - rs = ps.executeQuery(); - return rse.extractData(rs); + return execute(psc, (PreparedStatementCallback) ps -> { + ResultSet rs = null; + try { + if (pss != null) { + pss.setValues(ps); } - finally { - JdbcUtils.closeResultSet(rs); - if (pss instanceof ParameterDisposer parameterDisposer) { - parameterDisposer.cleanupParameters(); - } + rs = ps.executeQuery(); + return rse.extractData(rs); + } + finally { + JdbcUtils.closeResultSet(rs); + if (pss instanceof ParameterDisposer parameterDisposer) { + parameterDisposer.cleanupParameters(); } } }, true); @@ -743,7 +740,7 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { return query(sql, newArgTypePreparedStatementSetter(args, argTypes), rse); } - @Deprecated + @Deprecated(since = "5.3") @Override public @Nullable T query(String sql, @Nullable Object @Nullable [] args, ResultSetExtractor rse) throws DataAccessException { return query(sql, newArgPreparedStatementSetter(args), rse); @@ -769,7 +766,7 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { query(sql, newArgTypePreparedStatementSetter(args, argTypes), rch); } - @Deprecated + @Deprecated(since = "5.3") @Override public void query(String sql, @Nullable Object @Nullable [] args, RowCallbackHandler rch) throws DataAccessException { query(sql, newArgPreparedStatementSetter(args), rch); @@ -795,15 +792,15 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { return result(query(sql, args, argTypes, new RowMapperResultSetExtractor<>(rowMapper))); } - @Deprecated + @Deprecated(since = "5.3") @Override public List query(String sql, @Nullable Object @Nullable [] args, RowMapper rowMapper) throws DataAccessException { - return result(query(sql, args, new RowMapperResultSetExtractor<>(rowMapper))); + return result(query(sql, newArgPreparedStatementSetter(args), new RowMapperResultSetExtractor<>(rowMapper))); } @Override public List query(String sql, RowMapper rowMapper, @Nullable Object @Nullable ... args) throws DataAccessException { - return result(query(sql, args, new RowMapperResultSetExtractor<>(rowMapper))); + return result(query(sql, newArgPreparedStatementSetter(args), new RowMapperResultSetExtractor<>(rowMapper))); } /** @@ -862,16 +859,16 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { return DataAccessUtils.nullableSingleResult(results); } - @Deprecated + @Deprecated(since = "5.3") @Override public @Nullable T queryForObject(String sql,@Nullable Object @Nullable [] args, RowMapper rowMapper) throws DataAccessException { - List results = query(sql, args, new RowMapperResultSetExtractor<>(rowMapper, 1)); + List results = query(sql, newArgPreparedStatementSetter(args), new RowMapperResultSetExtractor<>(rowMapper, 1)); return DataAccessUtils.nullableSingleResult(results); } @Override public @Nullable T queryForObject(String sql, RowMapper rowMapper, @Nullable Object @Nullable ... args) throws DataAccessException { - List results = query(sql, args, new RowMapperResultSetExtractor<>(rowMapper, 1)); + List results = query(sql, newArgPreparedStatementSetter(args), new RowMapperResultSetExtractor<>(rowMapper, 1)); return DataAccessUtils.nullableSingleResult(results); } @@ -882,15 +879,15 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { return queryForObject(sql, args, argTypes, getSingleColumnRowMapper(requiredType)); } - @Deprecated + @Deprecated(since = "5.3") @Override public @Nullable T queryForObject(String sql, @Nullable Object @Nullable [] args, Class requiredType) throws DataAccessException { - return queryForObject(sql, args, getSingleColumnRowMapper(requiredType)); + return queryForObject(sql, getSingleColumnRowMapper(requiredType), args); } @Override public @Nullable T queryForObject(String sql, Class requiredType, @Nullable Object @Nullable ... args) throws DataAccessException { - return queryForObject(sql, args, getSingleColumnRowMapper(requiredType)); + return queryForObject(sql, getSingleColumnRowMapper(requiredType), args); } @Override @@ -900,7 +897,7 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { @Override public Map queryForMap(String sql, @Nullable Object @Nullable ... args) throws DataAccessException { - return result(queryForObject(sql, args, getColumnMapRowMapper())); + return result(queryForObject(sql, getColumnMapRowMapper(), args)); } @Override @@ -908,15 +905,15 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { return query(sql, args, argTypes, getSingleColumnRowMapper(elementType)); } - @Deprecated + @Deprecated(since = "5.3") @Override public List queryForList(String sql, @Nullable Object @Nullable [] args, Class elementType) throws DataAccessException { - return query(sql, args, getSingleColumnRowMapper(elementType)); + return query(sql, newArgPreparedStatementSetter(args), getSingleColumnRowMapper(elementType)); } @Override public List queryForList(String sql, Class elementType, @Nullable Object @Nullable ... args) throws DataAccessException { - return query(sql, args, getSingleColumnRowMapper(elementType)); + return query(sql, newArgPreparedStatementSetter(args), getSingleColumnRowMapper(elementType)); } @Override @@ -926,7 +923,7 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { @Override public List> queryForList(String sql, @Nullable Object @Nullable ... args) throws DataAccessException { - return query(sql, args, getColumnMapRowMapper()); + return query(sql, newArgPreparedStatementSetter(args), getColumnMapRowMapper()); } @Override @@ -936,10 +933,10 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { @Override public SqlRowSet queryForRowSet(String sql, @Nullable Object @Nullable ... args) throws DataAccessException { - return result(query(sql, args, new SqlRowSetResultSetExtractor())); + return result(query(sql, newArgPreparedStatementSetter(args), new SqlRowSetResultSetExtractor())); } - protected int update(final PreparedStatementCreator psc, final @Nullable PreparedStatementSetter pss) + protected int update(PreparedStatementCreator psc, @Nullable PreparedStatementSetter pss) throws DataAccessException { logger.debug("Executing prepared SQL update"); @@ -969,7 +966,7 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { } @Override - public int update(final PreparedStatementCreator psc, final KeyHolder generatedKeyHolder) + public int update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder) throws DataAccessException { Assert.notNull(generatedKeyHolder, "KeyHolder must not be null"); @@ -1002,8 +999,8 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { } @Override - public int[] batchUpdate(final PreparedStatementCreator psc, final BatchPreparedStatementSetter pss, - final KeyHolder generatedKeyHolder) throws DataAccessException { + public int[] batchUpdate(PreparedStatementCreator psc, BatchPreparedStatementSetter pss, + KeyHolder generatedKeyHolder) throws DataAccessException { int[] result = execute(psc, getPreparedStatementCallback(pss, generatedKeyHolder)); @@ -1012,7 +1009,7 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { } @Override - public int[] batchUpdate(String sql, final BatchPreparedStatementSetter pss) throws DataAccessException { + public int[] batchUpdate(String sql, BatchPreparedStatementSetter pss) throws DataAccessException { if (logger.isDebugEnabled()) { logger.debug("Executing SQL batch update [" + sql + "]"); } @@ -1032,7 +1029,7 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { } @Override - public int[] batchUpdate(String sql, List batchArgs, final int[] argTypes) throws DataAccessException { + public int[] batchUpdate(String sql, List batchArgs, int[] argTypes) throws DataAccessException { if (batchArgs.isEmpty()) { return new int[0]; } @@ -1069,8 +1066,8 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { } @Override - public int[][] batchUpdate(String sql, final Collection batchArgs, final int batchSize, - final ParameterizedPreparedStatementSetter pss) throws DataAccessException { + public int[][] batchUpdate(String sql, Collection batchArgs, int batchSize, + ParameterizedPreparedStatementSetter pss) throws DataAccessException { if (logger.isDebugEnabled()) { logger.debug("Executing SQL batch update [" + sql + "] with a batch size of " + batchSize); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java index 235bf55234..23d26f61ae 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.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. @@ -72,8 +72,8 @@ public interface PreparedStatementCallback { * @throws SQLException if thrown by a JDBC method, to be auto-converted * to a DataAccessException by an SQLExceptionTranslator * @throws DataAccessException in case of custom exceptions - * @see JdbcTemplate#queryForObject(String, Object[], Class) - * @see JdbcTemplate#queryForList(String, Object[]) + * @see JdbcTemplate#queryForObject(String, Class, Object...) + * @see JdbcTemplate#queryForList(String, Object...) */ @Nullable T doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java index 0f3fb0f2a5..bd559b6e93 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.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,7 +85,7 @@ public abstract class StatementCreatorUtils { private static final Map, Integer> javaTypeToSqlTypeMap = new HashMap<>(64); - static @Nullable Boolean shouldIgnoreGetParameterType; + static @Nullable Boolean shouldIgnoreGetParameterType = SpringProperties.checkFlag(IGNORE_GETPARAMETERTYPE_PROPERTY_NAME); static { javaTypeToSqlTypeMap.put(boolean.class, Types.BOOLEAN); @@ -114,11 +114,6 @@ public abstract class StatementCreatorUtils { javaTypeToSqlTypeMap.put(java.sql.Timestamp.class, Types.TIMESTAMP); javaTypeToSqlTypeMap.put(Blob.class, Types.BLOB); javaTypeToSqlTypeMap.put(Clob.class, Types.CLOB); - - String flag = SpringProperties.getProperty(IGNORE_GETPARAMETERTYPE_PROPERTY_NAME); - if (flag != null) { - shouldIgnoreGetParameterType = Boolean.valueOf(flag); - } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReader.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReader.java index c6db333402..e0814b44c2 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReader.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -42,10 +42,10 @@ import org.springframework.util.Assert; * @author Juergen Hoeller * @see #loadBeanDefinitions * @see org.springframework.beans.factory.support.PropertiesBeanDefinitionReader - * @deprecated as of 5.3, in favor of Spring's common bean definition formats - * and/or custom reader implementations + * @deprecated in favor of Spring's common bean definition formats and/or custom + * reader implementations */ -@Deprecated +@Deprecated(since = "5.3") public class JdbcBeanDefinitionReader { private final org.springframework.beans.factory.support.PropertiesBeanDefinitionReader propReader; 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 b0bc446ecd..64be10894b 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 @@ -263,10 +263,9 @@ public abstract class DataSourceUtils { * regarding read-only flag and isolation level. * @param con the Connection to reset * @param previousIsolationLevel the isolation level to restore, if any - * @deprecated as of 5.1.11, in favor of - * {@link #resetConnectionAfterTransaction(Connection, Integer, boolean)} + * @deprecated in favor of {@link #resetConnectionAfterTransaction(Connection, Integer, boolean)} */ - @Deprecated + @Deprecated(since = "5.1.11") public static void resetConnectionAfterTransaction(Connection con, @Nullable Integer previousIsolationLevel) { Assert.notNull(con, "No Connection specified"); try { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java index 313bfb944e..75269e12ac 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.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. @@ -77,6 +77,9 @@ import org.springframework.util.Assert; * You will get the same effect with non-transactional reads, but lazy fetching * of JDBC Connections allows you to still perform reads in transactions. * + *

As of 6.2.6, this DataSource proxy also suppresses a rollback attempt + * in case of a timeout where the connection has been closed in the meantime. + * *

NOTE: This DataSource proxy needs to return wrapped Connections * (which implement the {@link ConnectionProxy} interface) in order to handle * lazy fetching of an actual JDBC Connection. Use {@link Connection#unwrap} @@ -425,11 +428,19 @@ public class LazyConnectionDataSourceProxy extends DelegatingDataSource { return null; } - // Target Connection already fetched, - // or target Connection necessary for current operation -> - // invoke method on target connection. + + // Target Connection already fetched, or target Connection necessary for current operation + // -> invoke method on target connection. try { - return method.invoke(getTargetConnection(method), args); + Connection conToUse = getTargetConnection(method); + + if ("rollback".equals(method.getName()) && conToUse.isClosed()) { + // Connection closed in the meantime, probably due to a resource timeout. Since a + // rollback attempt typically happens right before close, we leniently suppress it. + return null; + } + + return method.invoke(conToUse, args); } catch (InvocationTargetException ex) { throw ex.getTargetException(); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/DatabaseStartupValidator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/DatabaseStartupValidator.java index 19cde00c58..0654bcb803 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/DatabaseStartupValidator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/DatabaseStartupValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -75,9 +75,9 @@ public class DatabaseStartupValidator implements InitializingBean { /** * Set the SQL query string to use for validation. - * @deprecated as of 5.3, in favor of the JDBC 4.0 connection validation + * @deprecated in favor of the JDBC 4.0 connection validation */ - @Deprecated + @Deprecated(since = "5.3") public void setValidationQuery(String validationQuery) { this.validationQuery = validationQuery; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java index 84a37f8bcf..3054008313 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.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. @@ -375,11 +375,11 @@ public abstract class JdbcUtils { * @throws MetaDataAccessException if we couldn't access the DatabaseMetaData * or failed to invoke the specified method * @see java.sql.DatabaseMetaData - * @deprecated as of 5.2.9, in favor of + * @deprecated in favor of * {@link #extractDatabaseMetaData(DataSource, DatabaseMetaDataCallback)} * with a lambda expression or method reference and a generically typed result */ - @Deprecated + @Deprecated(since = "5.2.9") @SuppressWarnings("unchecked") public static T extractDatabaseMetaData(DataSource dataSource, final String metaDataMethodName) throws MetaDataAccessException { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughBlob.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughBlob.java index 4ca1e69d1a..0ed06daecf 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughBlob.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughBlob.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. @@ -31,7 +31,7 @@ import org.jspecify.annotations.Nullable; * @author Juergen Hoeller * @since 2.5.3 */ -@Deprecated +@Deprecated(since = "6.2") class PassThroughBlob implements Blob { private byte @Nullable [] content; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughClob.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughClob.java index 5c7ca574cd..47324befff 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughClob.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughClob.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,7 +39,7 @@ import org.springframework.util.FileCopyUtils; * @author Juergen Hoeller * @since 2.5.3 */ -@Deprecated +@Deprecated(since = "6.2") class PassThroughClob implements Clob { private @Nullable String content; diff --git a/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt b/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt index f6480ab751..e29bb0f01b 100644 --- a/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt +++ b/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt @@ -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,8 @@ inline fun JdbcOperations.queryForObject(sql: String, args: Array JdbcOperations.queryForObject(sql: String, args: Array): T? = - queryForObject(sql, args, T::class.java as Class<*>) as T + queryForObject(sql, T::class.java as Class<*>, args) as T /** * Extension for [JdbcOperations.queryForList] providing a `queryForList("...")` variant. @@ -88,10 +86,8 @@ inline fun JdbcOperations.queryForList(sql: String, args: Arra * @author Mario Arias * @since 5.0 */ -@Suppress("DEPRECATION") -// TODO Replace by the vararg variant in Spring Framework 6 inline fun JdbcOperations.queryForList(sql: String, args: Array): List = - queryForList(sql, args, T::class.java) + queryForList(sql, T::class.java, args) /** diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java index 99daa91494..84c2f7981f 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.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,8 +16,11 @@ package org.springframework.jdbc.core.simple; +import java.util.List; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassRelativeResourceLoader; @@ -144,6 +147,86 @@ class JdbcClientIntegrationTests { } + @Nested // gh-34768 + class ReusedNamedParameterTests { + + private static final String QUERY1 = """ + select * from users + where + first_name in ('Bogus', :name) or + last_name in (:name, 'Bogus') + order by last_name + """; + + private static final String QUERY2 = """ + select * from users + where + first_name in (:names) or + last_name in (:names) + order by last_name + """; + + + @BeforeEach + void insertTestUsers() { + jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("John", "John").update(); + jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("John", "Smith").update(); + jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("Smith", "Smith").update(); + assertNumUsers(4); + } + + @Test + void selectWithReusedNamedParameter() { + List users = jdbcClient.sql(QUERY1) + .param("name", "John") + .query(User.class) + .list(); + + assertResults(users); + } + + @Test + void selectWithReusedNamedParameterFromBeanProperties() { + List users = jdbcClient.sql(QUERY1) + .paramSource(new Name("John")) + .query(User.class) + .list(); + + assertResults(users); + } + + @Test + void selectWithReusedNamedParameterList() { + List users = jdbcClient.sql(QUERY2) + .param("names", List.of("John", "Bogus")) + .query(User.class) + .list(); + + assertResults(users); + } + + @Test + void selectWithReusedNamedParameterListFromBeanProperties() { + List users = jdbcClient.sql(QUERY2) + .paramSource(new Names(List.of("John", "Bogus"))) + .query(User.class) + .list(); + + assertResults(users); + } + + + private static void assertResults(List users) { + assertThat(users).containsExactly(new User(2, "John", "John"), new User(3, "John", "Smith")); + } + + record Name(String name) {} + + record Names(List names) {} + + } + + private void assertNumUsers(long count) { long numUsers = this.jdbcClient.sql("select count(id) from users").query(Long.class).single(); assertThat(numUsers).isEqualTo(count); diff --git a/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensionsTests.kt b/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensionsTests.kt index 6ed59b8a46..894033e9bb 100644 --- a/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensionsTests.kt +++ b/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensionsTests.kt @@ -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. @@ -67,12 +67,11 @@ class JdbcOperationsExtensionsTests { } @Test - @Suppress("DEPRECATION") fun `queryForObject with reified type parameters and args`() { val args = arrayOf(3, 4) - every { template.queryForObject(sql, args, any>()) } returns 2 + every { template.queryForObject(sql, any>(), args) } returns 2 assertThat(template.queryForObject(sql, args)).isEqualTo(2) - verify { template.queryForObject(sql, args, any>()) } + verify { template.queryForObject(sql, any>(), args) } } @Test @@ -94,13 +93,12 @@ class JdbcOperationsExtensionsTests { } @Test - @Suppress("DEPRECATION") fun `queryForList with reified type parameters and args`() { val list = listOf(1, 2, 3) val args = arrayOf(3, 4) - every { template.queryForList(sql, args, any>()) } returns list + every { template.queryForList(sql, any>(), args) } returns list template.queryForList(sql, args) - verify { template.queryForList(sql, args, any>()) } + verify { template.queryForList(sql, any>(), args) } } @Test diff --git a/spring-jms/spring-jms.gradle b/spring-jms/spring-jms.gradle index 31da2fb302..9014b09663 100644 --- a/spring-jms/spring-jms.gradle +++ b/spring-jms/spring-jms.gradle @@ -14,6 +14,7 @@ dependencies { optional("io.micrometer:micrometer-jakarta9") optional("jakarta.resource:jakarta.resource-api") optional("jakarta.transaction:jakarta.transaction-api") + optional("tools.jackson.core:jackson-databind") testImplementation(testFixtures(project(":spring-beans"))) testImplementation(testFixtures(project(":spring-tx"))) testImplementation("jakarta.jms:jakarta.jms-api") diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java b/spring-jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java index ffce4224a1..3556fd3e68 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ import org.springframework.util.StringUtils; * given user credentials to every standard methods that can also be used with * authentication, this {@code createConnection()} and {@code createContext()}. In * other words, it is implicitly invoking {@code createConnection(username, password)} or - * {@code createContext(username, password)}} on the target. All other methods simply + * {@code createContext(username, password)} on the target. All other methods simply * delegate to the corresponding methods of the target ConnectionFactory. * *

Can be used to proxy a target JNDI ConnectionFactory that does not have user diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java index ac26a7089d..b9d856a5c1 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.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. @@ -194,7 +194,7 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe private boolean virtualThreads = false; - private BackOff backOff = new FixedBackOff(DEFAULT_RECOVERY_INTERVAL, Long.MAX_VALUE); + private BackOff backOff = new FixedBackOff(DEFAULT_RECOVERY_INTERVAL); private int cacheLevel = CACHE_AUTO; @@ -278,8 +278,8 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe * between recovery attempts. If the {@link BackOffExecution} implementation * returns {@link BackOffExecution#STOP}, this listener container will not further * attempt to recover. - *

The {@link #setRecoveryInterval(long) recovery interval} is ignored - * when this property is set. + *

Note that setting the {@linkplain #setRecoveryInterval(long) recovery + * interval} overrides this property. * @since 4.1 */ public void setBackOff(BackOff backOff) { @@ -288,15 +288,17 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe /** * Specify the interval between recovery attempts, in milliseconds. - * The default is 5000 ms, that is, 5 seconds. This is a convenience method - * to create a {@link FixedBackOff} with the specified interval. - *

For more recovery options, consider specifying a {@link BackOff} - * instance instead. + *

The default is 5000 ms, that is, 5 seconds. + *

This is a convenience method to create a {@link FixedBackOff} with the + * specified interval. For more recovery options, consider specifying a + * {@link #setBackOff(BackOff) BackOff} instance instead. Note, however, that + * explicitly setting the {@link #setBackOff(BackOff) BackOff} overrides this + * property. * @see #setBackOff(BackOff) * @see #handleListenerSetupFailure */ public void setRecoveryInterval(long recoveryInterval) { - this.backOff = new FixedBackOff(recoveryInterval, Long.MAX_VALUE); + this.backOff = new FixedBackOff(recoveryInterval); } /** diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/JacksonJsonMessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/JacksonJsonMessageConverter.java new file mode 100644 index 0000000000..75867407bd --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/JacksonJsonMessageConverter.java @@ -0,0 +1,488 @@ +/* + * 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.jms.support.converter; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonView; +import jakarta.jms.BytesMessage; +import jakarta.jms.JMSException; +import jakarta.jms.Message; +import jakarta.jms.Session; +import jakarta.jms.TextMessage; +import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectWriter; +import tools.jackson.databind.cfg.MapperBuilder; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.core.MethodParameter; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Message converter that uses Jackson 3.x to convert messages to and from JSON. + * + *

Maps an object to a {@link BytesMessage}, or to a {@link TextMessage} if the + * {@link #setTargetType targetType} is set to {@link MessageType#TEXT}. + * Converts from a {@link TextMessage} or {@link BytesMessage} to an object. + * + *

The default constructor loads {@link tools.jackson.databind.JacksonModule}s + * found by {@link MapperBuilder#findModules(ClassLoader)}. + * + * @author Sebastien Deleuze + * @since 7.0 + */ +public class JacksonJsonMessageConverter implements SmartMessageConverter, BeanClassLoaderAware { + + /** + * The default encoding used for writing to text messages: UTF-8. + */ + public static final String DEFAULT_ENCODING = "UTF-8"; + + + private final ObjectMapper objectMapper; + + private MessageType targetType = MessageType.BYTES; + + private @Nullable String encoding; + + private @Nullable String encodingPropertyName; + + private @Nullable String typeIdPropertyName; + + private Map> idClassMappings = new HashMap<>(); + + private final Map, String> classIdMappings = new HashMap<>(); + + private @Nullable ClassLoader beanClassLoader; + + + /** + * Construct a new instance with a {@link JsonMapper} customized with the + * {@link tools.jackson.databind.JacksonModule}s found by + * {@link MapperBuilder#findModules(ClassLoader)}. + */ + public JacksonJsonMessageConverter() { + this.objectMapper = JsonMapper.builder().findAndAddModules(JacksonJsonMessageConverter.class.getClassLoader()).build(); + } + + /** + * Construct a new instance with the provided {@link ObjectMapper}. + * @see JsonMapper#builder() + * @see MapperBuilder#findModules(ClassLoader) + */ + public JacksonJsonMessageConverter(ObjectMapper objectMapper) { + Assert.notNull(objectMapper, "ObjectMapper must not be null"); + this.objectMapper = objectMapper; + } + + /** + * Specify whether {@link #toMessage(Object, Session)} should marshal to a + * {@link BytesMessage} or a {@link TextMessage}. + *

The default is {@link MessageType#BYTES}, i.e. this converter marshals to + * a {@link BytesMessage}. Note that the default version of this converter + * supports {@link MessageType#BYTES} and {@link MessageType#TEXT} only. + * @see MessageType#BYTES + * @see MessageType#TEXT + */ + public void setTargetType(MessageType targetType) { + Assert.notNull(targetType, "MessageType must not be null"); + this.targetType = targetType; + } + + /** + * Specify the encoding to use when converting to and from text-based + * message body content. The default encoding will be "UTF-8". + *

When reading from a text-based message, an encoding may have been + * suggested through a special JMS property which will then be preferred + * over the encoding set on this MessageConverter instance. + * @see #setEncodingPropertyName + */ + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + /** + * Specify the name of the JMS message property that carries the encoding from + * bytes to String and back is BytesMessage is used during the conversion process. + *

Default is none. Setting this property is optional; if not set, UTF-8 will + * be used for decoding any incoming bytes message. + * @see #setEncoding + */ + public void setEncodingPropertyName(String encodingPropertyName) { + this.encodingPropertyName = encodingPropertyName; + } + + /** + * Specify the name of the JMS message property that carries the type id for the + * contained object: either a mapped id value or a raw Java class name. + *

Default is none. NOTE: This property needs to be set in order to allow + * for converting from an incoming message to a Java object. + * @see #setTypeIdMappings + */ + public void setTypeIdPropertyName(String typeIdPropertyName) { + this.typeIdPropertyName = typeIdPropertyName; + } + + /** + * Specify mappings from type ids to Java classes, if desired. + * This allows for synthetic ids in the type id message property, + * instead of transferring Java class names. + *

Default is no custom mappings, i.e. transferring raw Java class names. + * @param typeIdMappings a Map with type id values as keys and Java classes as values + */ + public void setTypeIdMappings(Map> typeIdMappings) { + this.idClassMappings = new HashMap<>(); + typeIdMappings.forEach((id, clazz) -> { + this.idClassMappings.put(id, clazz); + this.classIdMappings.put(clazz, id); + }); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + + @Override + public Message toMessage(Object object, Session session) throws JMSException, MessageConversionException { + Message message; + try { + message = switch (this.targetType) { + case TEXT -> mapToTextMessage(object, session, this.objectMapper.writer()); + case BYTES -> mapToBytesMessage(object, session, this.objectMapper.writer()); + default -> mapToMessage(object, session, this.objectMapper.writer(), this.targetType); + }; + } + catch (IOException ex) { + throw new MessageConversionException("Could not map JSON object [" + object + "]", ex); + } + setTypeIdOnMessage(object, message); + return message; + } + + @Override + public Message toMessage(Object object, Session session, @Nullable Object conversionHint) + throws JMSException, MessageConversionException { + + return toMessage(object, session, getSerializationView(conversionHint)); + } + + /** + * Convert a Java object to a JMS Message using the specified json view + * and the supplied session to create the message object. + * @param object the object to convert + * @param session the Session to use for creating a JMS Message + * @param jsonView the view to use to filter the content + * @return the JMS Message + * @throws JMSException if thrown by JMS API methods + * @throws MessageConversionException in case of conversion failure + */ + public Message toMessage(Object object, Session session, @Nullable Class jsonView) + throws JMSException, MessageConversionException { + + if (jsonView != null) { + return toMessage(object, session, this.objectMapper.writerWithView(jsonView)); + } + else { + return toMessage(object, session, this.objectMapper.writer()); + } + } + + @Override + public Object fromMessage(Message message) throws JMSException, MessageConversionException { + try { + JavaType targetJavaType = getJavaTypeForMessage(message); + return convertToObject(message, targetJavaType); + } + catch (IOException ex) { + throw new MessageConversionException("Failed to convert JSON message content", ex); + } + } + + protected Message toMessage(Object object, Session session, ObjectWriter objectWriter) + throws JMSException, MessageConversionException { + + Message message; + try { + message = switch (this.targetType) { + case TEXT -> mapToTextMessage(object, session, objectWriter); + case BYTES -> mapToBytesMessage(object, session, objectWriter); + default -> mapToMessage(object, session, objectWriter, this.targetType); + }; + } + catch (IOException ex) { + throw new MessageConversionException("Could not map JSON object [" + object + "]", ex); + } + setTypeIdOnMessage(object, message); + return message; + } + + + /** + * Map the given object to a {@link TextMessage}. + * @param object the object to be mapped + * @param session current JMS session + * @param objectWriter the writer to use + * @return the resulting message + * @throws JMSException if thrown by JMS methods + * @throws IOException in case of I/O errors + * @see Session#createBytesMessage + */ + protected TextMessage mapToTextMessage(Object object, Session session, ObjectWriter objectWriter) + throws JMSException, IOException { + + StringWriter writer = new StringWriter(1024); + objectWriter.writeValue(writer, object); + return session.createTextMessage(writer.toString()); + } + + /** + * Map the given object to a {@link BytesMessage}. + * @param object the object to be mapped + * @param session current JMS session + * @param objectWriter the writer to use + * @return the resulting message + * @throws JMSException if thrown by JMS methods + * @throws IOException in case of I/O errors + * @see Session#createBytesMessage + */ + protected BytesMessage mapToBytesMessage(Object object, Session session, ObjectWriter objectWriter) + throws JMSException, IOException { + + ByteArrayOutputStream bos = new ByteArrayOutputStream(1024); + if (this.encoding != null) { + OutputStreamWriter writer = new OutputStreamWriter(bos, this.encoding); + objectWriter.writeValue(writer, object); + } + else { + // Jackson usually defaults to UTF-8 but can also go straight to bytes, for example, for Smile. + // We use a direct byte array argument for the latter case to work as well. + objectWriter.writeValue(bos, object); + } + + BytesMessage message = session.createBytesMessage(); + message.writeBytes(bos.toByteArray()); + if (this.encodingPropertyName != null) { + message.setStringProperty(this.encodingPropertyName, + (this.encoding != null ? this.encoding : DEFAULT_ENCODING)); + } + return message; + } + + /** + * Template method that allows for custom message mapping. + * Invoked when {@link #setTargetType} is not {@link MessageType#TEXT} or + * {@link MessageType#BYTES}. + *

The default implementation throws an {@link IllegalArgumentException}. + * @param object the object to marshal + * @param session the JMS Session + * @param objectWriter the writer to use + * @param targetType the target message type (other than TEXT or BYTES) + * @return the resulting message + * @throws JMSException if thrown by JMS methods + * @throws IOException in case of I/O errors + */ + protected Message mapToMessage(Object object, Session session, ObjectWriter objectWriter, MessageType targetType) + throws JMSException, IOException { + + throw new IllegalArgumentException("Unsupported message type [" + targetType + + "]. MappingJackson2MessageConverter by default only supports TextMessages and BytesMessages."); + } + + /** + * Set a type id for the given payload object on the given JMS Message. + *

The default implementation consults the configured type id mapping and + * sets the resulting value (either a mapped id or the raw Java class name) + * into the configured type id message property. + * @param object the payload object to set a type id for + * @param message the JMS Message on which to set the type id property + * @throws JMSException if thrown by JMS methods + * @see #getJavaTypeForMessage(Message) + * @see #setTypeIdPropertyName(String) + * @see #setTypeIdMappings(Map) + */ + protected void setTypeIdOnMessage(Object object, Message message) throws JMSException { + if (this.typeIdPropertyName != null) { + String typeId = this.classIdMappings.get(object.getClass()); + if (typeId == null) { + typeId = object.getClass().getName(); + } + message.setStringProperty(this.typeIdPropertyName, typeId); + } + } + + /** + * Convenience method to dispatch to converters for individual message types. + */ + private Object convertToObject(Message message, JavaType targetJavaType) throws JMSException, IOException { + if (message instanceof TextMessage textMessage) { + return convertFromTextMessage(textMessage, targetJavaType); + } + else if (message instanceof BytesMessage bytesMessage) { + return convertFromBytesMessage(bytesMessage, targetJavaType); + } + else { + return convertFromMessage(message, targetJavaType); + } + } + + /** + * Convert a TextMessage to a Java Object with the specified type. + * @param message the input message + * @param targetJavaType the target type + * @return the message converted to an object + * @throws JMSException if thrown by JMS + * @throws IOException in case of I/O errors + */ + protected Object convertFromTextMessage(TextMessage message, JavaType targetJavaType) + throws JMSException, IOException { + + String body = message.getText(); + return this.objectMapper.readValue(body, targetJavaType); + } + + /** + * Convert a BytesMessage to a Java Object with the specified type. + * @param message the input message + * @param targetJavaType the target type + * @return the message converted to an object + * @throws JMSException if thrown by JMS + * @throws IOException in case of I/O errors + */ + protected Object convertFromBytesMessage(BytesMessage message, JavaType targetJavaType) + throws JMSException, IOException { + + String encoding = this.encoding; + if (this.encodingPropertyName != null && message.propertyExists(this.encodingPropertyName)) { + encoding = message.getStringProperty(this.encodingPropertyName); + } + byte[] bytes = new byte[(int) message.getBodyLength()]; + message.readBytes(bytes); + if (encoding != null) { + try { + String body = new String(bytes, encoding); + return this.objectMapper.readValue(body, targetJavaType); + } + catch (UnsupportedEncodingException ex) { + throw new MessageConversionException("Cannot convert bytes to String", ex); + } + } + else { + // Jackson internally performs encoding detection, falling back to UTF-8. + return this.objectMapper.readValue(bytes, targetJavaType); + } + } + + /** + * Template method that allows for custom message mapping. + * Invoked when {@link #setTargetType} is not {@link MessageType#TEXT} or + * {@link MessageType#BYTES}. + *

The default implementation throws an {@link IllegalArgumentException}. + * @param message the input message + * @param targetJavaType the target type + * @return the message converted to an object + * @throws JMSException if thrown by JMS + * @throws IOException in case of I/O errors + */ + protected Object convertFromMessage(Message message, JavaType targetJavaType) + throws JMSException, IOException { + + throw new IllegalArgumentException("Unsupported message type [" + message.getClass() + + "]. MappingJacksonMessageConverter by default only supports TextMessages and BytesMessages."); + } + + /** + * Determine a Jackson JavaType for the given JMS Message, + * typically parsing a type id message property. + *

The default implementation parses the configured type id property name + * and consults the configured type id mapping. This can be overridden with + * a different strategy, for example, doing some heuristics based on message origin. + * @param message the JMS Message from which to get the type id property + * @throws JMSException if thrown by JMS methods + * @see #setTypeIdOnMessage(Object, Message) + * @see #setTypeIdPropertyName(String) + * @see #setTypeIdMappings(Map) + */ + protected JavaType getJavaTypeForMessage(Message message) throws JMSException { + String typeId = message.getStringProperty(this.typeIdPropertyName); + if (typeId == null) { + throw new MessageConversionException( + "Could not find type id property [" + this.typeIdPropertyName + "] on message [" + + message.getJMSMessageID() + "] from destination [" + message.getJMSDestination() + "]"); + } + Class mappedClass = this.idClassMappings.get(typeId); + if (mappedClass != null) { + return this.objectMapper.constructType(mappedClass); + } + try { + Class typeClass = ClassUtils.forName(typeId, this.beanClassLoader); + return this.objectMapper.constructType(typeClass); + } + catch (Throwable ex) { + throw new MessageConversionException("Failed to resolve type id [" + typeId + "]", ex); + } + } + + /** + * Determine a Jackson serialization view based on the given conversion hint. + * @param conversionHint the conversion hint Object as passed into the + * converter for the current conversion attempt + * @return the serialization view class, or {@code null} if none + */ + protected @Nullable Class getSerializationView(@Nullable Object conversionHint) { + if (conversionHint instanceof MethodParameter methodParam) { + JsonView annotation = methodParam.getParameterAnnotation(JsonView.class); + if (annotation == null) { + annotation = methodParam.getMethodAnnotation(JsonView.class); + if (annotation == null) { + return null; + } + } + return extractViewClass(annotation, conversionHint); + } + else if (conversionHint instanceof JsonView jsonView) { + return extractViewClass(jsonView, conversionHint); + } + else if (conversionHint instanceof Class clazz) { + return clazz; + } + else { + return null; + } + } + + private Class extractViewClass(JsonView annotation, Object conversionHint) { + Class[] classes = annotation.value(); + if (classes.length != 1) { + throw new IllegalArgumentException( + "@JsonView only supported for handler methods with exactly 1 class argument: " + conversionHint); + } + return classes[0]; + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/MappingJackson2MessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/MappingJackson2MessageConverter.java index f45a140f1c..e8449c3c7a 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/MappingJackson2MessageConverter.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/MappingJackson2MessageConverter.java @@ -60,7 +60,9 @@ import org.springframework.util.ClassUtils; * @author Juergen Hoeller * @author Stephane Nicoll * @since 3.1.4 + * @deprecated since 7.0 in favor of {@link JacksonJsonMessageConverter} */ +@Deprecated(since = "7.0", forRemoval = true) public class MappingJackson2MessageConverter implements SmartMessageConverter, BeanClassLoaderAware { /** diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/MessagingMessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/MessagingMessageConverter.java index 117ca437ec..750f3a951e 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/MessagingMessageConverter.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/MessagingMessageConverter.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. @@ -132,7 +132,7 @@ public class MessagingMessageConverter implements MessageConverter, Initializing /** * Create a JMS message for the specified payload and conversionHint. * The conversion hint is an extra object passed to the {@link MessageConverter}, - * for example, the associated {@code MethodParameter} (may be {@code null}}. + * for example, the associated {@code MethodParameter} (may be {@code null}). * @since 4.3 * @see MessageConverter#toMessage(Object, Session) */ diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/SmartMessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/SmartMessageConverter.java index a2cc701b09..9d21509a03 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/SmartMessageConverter.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/SmartMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -40,7 +40,7 @@ public interface SmartMessageConverter extends MessageConverter { * @param object the object to convert * @param session the Session to use for creating a JMS Message * @param conversionHint an extra object passed to the {@link MessageConverter}, - * for example, the associated {@code MethodParameter} (may be {@code null}} + * for example, the associated {@code MethodParameter} (may be {@code null}) * @return the JMS Message * @throws jakarta.jms.JMSException if thrown by JMS API methods * @throws MessageConversionException in case of conversion failure diff --git a/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterTests.java b/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterTests.java index 3ab88d2c92..44916da6f4 100644 --- a/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterTests.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. @@ -36,7 +36,7 @@ import org.springframework.beans.factory.support.StaticListableBeanFactory; import org.springframework.jms.StubTextMessage; import org.springframework.jms.support.JmsHeaders; import org.springframework.jms.support.QosSettings; -import org.springframework.jms.support.converter.MappingJackson2MessageConverter; +import org.springframework.jms.support.converter.JacksonJsonMessageConverter; import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.converter.MessageType; import org.springframework.jms.support.converter.MessagingMessageConverter; @@ -70,7 +70,8 @@ class MessagingMessageListenerAdapterTests { @BeforeEach void setup() { - initializeFactory(factory); + factory.setBeanFactory(new StaticListableBeanFactory()); + factory.afterPropertiesSet(); } @Test @@ -299,7 +300,7 @@ class MessagingMessageListenerAdapterTests { @Test void replyJackson() throws JMSException { TextMessage reply = testReplyWithJackson("replyJackson", - "{\"counter\":42,\"name\":\"Response\",\"description\":\"lengthy description\"}"); + "{\"name\":\"Response\",\"description\":\"lengthy description\",\"counter\":42}"); verify(reply).setObjectProperty("foo", "bar"); } @@ -327,7 +328,7 @@ class MessagingMessageListenerAdapterTests { given(session.createProducer(replyDestination)).willReturn(messageProducer); MessagingMessageListenerAdapter listener = getPayloadInstance("Response", methodName, Message.class); - MappingJackson2MessageConverter messageConverter = new MappingJackson2MessageConverter(); + JacksonJsonMessageConverter messageConverter = new JacksonJsonMessageConverter(); messageConverter.setTargetType(MessageType.TEXT); listener.setMessageConverter(messageConverter); listener.setDefaultResponseDestination(replyDestination); @@ -405,11 +406,6 @@ class MessagingMessageListenerAdapterTests { return adapter; } - private void initializeFactory(DefaultMessageHandlerMethodFactory factory) { - factory.setBeanFactory(new StaticListableBeanFactory()); - factory.afterPropertiesSet(); - } - @SuppressWarnings("unused") private static class SampleBean { diff --git a/spring-jms/src/test/java/org/springframework/jms/support/converter/JacksonJsonMessageConverterTests.java b/spring-jms/src/test/java/org/springframework/jms/support/converter/JacksonJsonMessageConverterTests.java new file mode 100644 index 0000000000..1416fa3ab0 --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/support/converter/JacksonJsonMessageConverterTests.java @@ -0,0 +1,336 @@ +/* + * 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.jms.support.converter; + +import java.io.ByteArrayInputStream; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonView; +import jakarta.jms.BytesMessage; +import jakarta.jms.JMSException; +import jakarta.jms.Session; +import jakarta.jms.TextMessage; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.stubbing.Answer; + +import org.springframework.core.MethodParameter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Sebastien Deleuze + */ +class JacksonJsonMessageConverterTests { + + private JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(); + + private Session sessionMock = mock(); + + + @BeforeEach + void setup() { + converter.setEncodingPropertyName("__encoding__"); + converter.setTypeIdPropertyName("__typeid__"); + } + + + @Test + void toBytesMessage() throws Exception { + BytesMessage bytesMessageMock = mock(); + Date toBeMarshalled = new Date(); + + given(sessionMock.createBytesMessage()).willReturn(bytesMessageMock); + + converter.toMessage(toBeMarshalled, sessionMock); + + verify(bytesMessageMock).setStringProperty("__encoding__", "UTF-8"); + verify(bytesMessageMock).setStringProperty("__typeid__", Date.class.getName()); + verify(bytesMessageMock).writeBytes(isA(byte[].class)); + } + + @Test + void fromBytesMessage() throws Exception { + BytesMessage bytesMessageMock = mock(); + Map unmarshalled = Collections.singletonMap("foo", "bar"); + + byte[] bytes = "{\"foo\":\"bar\"}".getBytes(); + final ByteArrayInputStream byteStream = new ByteArrayInputStream(bytes); + + given(bytesMessageMock.getStringProperty("__typeid__")).willReturn(Object.class.getName()); + given(bytesMessageMock.propertyExists("__encoding__")).willReturn(false); + given(bytesMessageMock.getBodyLength()).willReturn(Long.valueOf(bytes.length)); + given(bytesMessageMock.readBytes(any(byte[].class))).willAnswer( + (Answer) invocation -> byteStream.read((byte[]) invocation.getArguments()[0])); + + Object result = converter.fromMessage(bytesMessageMock); + assertThat(unmarshalled).as("Invalid result").isEqualTo(result); + } + + @Test + void toTextMessageWithObject() throws Exception { + converter.setTargetType(MessageType.TEXT); + TextMessage textMessageMock = mock(); + Date toBeMarshalled = new Date(); + + given(sessionMock.createTextMessage(isA(String.class))).willReturn(textMessageMock); + + converter.toMessage(toBeMarshalled, sessionMock); + verify(textMessageMock).setStringProperty("__typeid__", Date.class.getName()); + } + + @Test + void toTextMessageWithMap() throws Exception { + converter.setTargetType(MessageType.TEXT); + TextMessage textMessageMock = mock(); + Map toBeMarshalled = new HashMap<>(); + toBeMarshalled.put("foo", "bar"); + + given(sessionMock.createTextMessage(isA(String.class))).willReturn(textMessageMock); + + converter.toMessage(toBeMarshalled, sessionMock); + verify(textMessageMock).setStringProperty("__typeid__", HashMap.class.getName()); + } + + @Test + void fromTextMessage() throws Exception { + TextMessage textMessageMock = mock(); + MyBean unmarshalled = new MyBean("bar"); + + String text = "{\"foo\":\"bar\"}"; + given(textMessageMock.getStringProperty("__typeid__")).willReturn(MyBean.class.getName()); + given(textMessageMock.getText()).willReturn(text); + + MyBean result = (MyBean)converter.fromMessage(textMessageMock); + assertThat(unmarshalled).as("Invalid result").isEqualTo(result); + } + + @Test + void fromTextMessageWithUnknownProperty() throws Exception { + TextMessage textMessageMock = mock(); + MyBean unmarshalled = new MyBean("bar"); + + String text = "{\"foo\":\"bar\", \"unknownProperty\":\"value\"}"; + given(textMessageMock.getStringProperty("__typeid__")).willReturn(MyBean.class.getName()); + given(textMessageMock.getText()).willReturn(text); + + MyBean result = (MyBean)converter.fromMessage(textMessageMock); + assertThat(unmarshalled).as("Invalid result").isEqualTo(result); + } + + @Test + void fromTextMessageAsObject() throws Exception { + TextMessage textMessageMock = mock(); + Map unmarshalled = Collections.singletonMap("foo", "bar"); + + String text = "{\"foo\":\"bar\"}"; + given(textMessageMock.getStringProperty("__typeid__")).willReturn(Object.class.getName()); + given(textMessageMock.getText()).willReturn(text); + + Object result = converter.fromMessage(textMessageMock); + assertThat(unmarshalled).as("Invalid result").isEqualTo(result); + } + + @Test + void fromTextMessageAsMap() throws Exception { + TextMessage textMessageMock = mock(); + Map unmarshalled = Collections.singletonMap("foo", "bar"); + + String text = "{\"foo\":\"bar\"}"; + given(textMessageMock.getStringProperty("__typeid__")).willReturn(HashMap.class.getName()); + given(textMessageMock.getText()).willReturn(text); + + Object result = converter.fromMessage(textMessageMock); + assertThat(unmarshalled).as("Invalid result").isEqualTo(result); + } + + @Test + void toTextMessageWithReturnType() throws JMSException, NoSuchMethodException { + Method method = this.getClass().getDeclaredMethod("summary"); + MethodParameter returnType = new MethodParameter(method, -1); + testToTextMessageWithReturnType(returnType); + verify(sessionMock).createTextMessage("{\"name\":\"test\"}"); + } + + @Test + void toTextMessageWithNullReturnType() throws JMSException, NoSuchMethodException { + testToTextMessageWithReturnType(null); + verify(sessionMock).createTextMessage("{\"description\":\"lengthy description\",\"name\":\"test\"}"); + } + + @Test + void toTextMessageWithReturnTypeAndNoJsonView() throws JMSException, NoSuchMethodException { + Method method = this.getClass().getDeclaredMethod("none"); + MethodParameter returnType = new MethodParameter(method, -1); + + testToTextMessageWithReturnType(returnType); + verify(sessionMock).createTextMessage("{\"description\":\"lengthy description\",\"name\":\"test\"}"); + } + + @Test + void toTextMessageWithReturnTypeAndMultipleJsonViews() throws NoSuchMethodException { + Method method = this.getClass().getDeclaredMethod("invalid"); + MethodParameter returnType = new MethodParameter(method, -1); + + assertThatIllegalArgumentException().isThrownBy(() -> + testToTextMessageWithReturnType(returnType)); + } + + private void testToTextMessageWithReturnType(MethodParameter returnType) throws JMSException { + converter.setTargetType(MessageType.TEXT); + TextMessage textMessageMock = mock(); + + MyAnotherBean bean = new MyAnotherBean("test", "lengthy description"); + given(sessionMock.createTextMessage(isA(String.class))).willReturn(textMessageMock); + converter.toMessage(bean, sessionMock, returnType); + verify(textMessageMock).setStringProperty("__typeid__", MyAnotherBean.class.getName()); + } + + @Test + void toTextMessageWithJsonViewClass() throws JMSException { + converter.setTargetType(MessageType.TEXT); + TextMessage textMessageMock = mock(); + + MyAnotherBean bean = new MyAnotherBean("test", "lengthy description"); + given(sessionMock.createTextMessage(isA(String.class))).willReturn(textMessageMock); + + + converter.toMessage(bean, sessionMock, Summary.class); + verify(textMessageMock).setStringProperty("__typeid__", MyAnotherBean.class.getName()); + verify(sessionMock).createTextMessage("{\"name\":\"test\"}"); + } + + @Test + void toTextMessageWithAnotherJsonViewClass() throws JMSException { + converter.setTargetType(MessageType.TEXT); + TextMessage textMessageMock = mock(); + + MyAnotherBean bean = new MyAnotherBean("test", "lengthy description"); + given(sessionMock.createTextMessage(isA(String.class))).willReturn(textMessageMock); + + + converter.toMessage(bean, sessionMock, Full.class); + verify(textMessageMock).setStringProperty("__typeid__", MyAnotherBean.class.getName()); + verify(sessionMock).createTextMessage("{\"description\":\"lengthy description\",\"name\":\"test\"}"); + } + + + @JsonView(Summary.class) + public MyAnotherBean summary() { + return new MyAnotherBean(); + } + + public MyAnotherBean none() { + return new MyAnotherBean(); + } + + @JsonView({Summary.class, Full.class}) + public MyAnotherBean invalid() { + return new MyAnotherBean(); + } + + + public static class MyBean { + + private String foo; + + public MyBean() { + } + + public MyBean(String foo) { + this.foo = foo; + } + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MyBean bean = (MyBean) o; + return Objects.equals(this.foo, bean.foo); + } + + @Override + public int hashCode() { + return foo != null ? foo.hashCode() : 0; + } + } + + + private interface Summary {} + + private interface Full extends Summary {} + + + @SuppressWarnings("unused") + private static class MyAnotherBean { + + @JsonView(Summary.class) + private String name; + + @JsonView(Full.class) + private String description; + + private MyAnotherBean() { + } + + public MyAnotherBean(String name, String description) { + this.name = name; + this.description = description; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + } + +} diff --git a/spring-jms/src/test/java/org/springframework/jms/support/converter/MappingJackson2MessageConverterTests.java b/spring-jms/src/test/java/org/springframework/jms/support/converter/MappingJackson2MessageConverterTests.java index 392060c755..67cd546062 100644 --- a/spring-jms/src/test/java/org/springframework/jms/support/converter/MappingJackson2MessageConverterTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/support/converter/MappingJackson2MessageConverterTests.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. @@ -49,6 +49,7 @@ import static org.mockito.Mockito.verify; * @author Dave Syer * @author Stephane Nicoll */ +@SuppressWarnings("removal") class MappingJackson2MessageConverterTests { private MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); @@ -174,7 +175,7 @@ class MappingJackson2MessageConverterTests { @Test void toTextMessageWithReturnType() throws JMSException, NoSuchMethodException { - Method method = this.getClass().getDeclaredMethod("summary"); + Method method = getClass().getDeclaredMethod("summary"); MethodParameter returnType = new MethodParameter(method, -1); testToTextMessageWithReturnType(returnType); verify(sessionMock).createTextMessage("{\"name\":\"test\"}"); @@ -188,7 +189,7 @@ class MappingJackson2MessageConverterTests { @Test void toTextMessageWithReturnTypeAndNoJsonView() throws JMSException, NoSuchMethodException { - Method method = this.getClass().getDeclaredMethod("none"); + Method method = getClass().getDeclaredMethod("none"); MethodParameter returnType = new MethodParameter(method, -1); testToTextMessageWithReturnType(returnType); @@ -197,7 +198,7 @@ class MappingJackson2MessageConverterTests { @Test void toTextMessageWithReturnTypeAndMultipleJsonViews() throws NoSuchMethodException { - Method method = this.getClass().getDeclaredMethod("invalid"); + Method method = getClass().getDeclaredMethod("invalid"); MethodParameter returnType = new MethodParameter(method, -1); assertThatIllegalArgumentException().isThrownBy(() -> diff --git a/spring-messaging/spring-messaging.gradle b/spring-messaging/spring-messaging.gradle index 6440084bc2..a611cf6698 100644 --- a/spring-messaging/spring-messaging.gradle +++ b/spring-messaging/spring-messaging.gradle @@ -20,6 +20,7 @@ dependencies { optional("jakarta.xml.bind:jakarta.xml.bind-api") optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") optional("org.jetbrains.kotlinx:kotlinx-serialization-json") + optional("tools.jackson.core:jackson-databind") testImplementation(project(":spring-core-test")) testImplementation(testFixtures(project(":spring-core"))) testImplementation("com.thoughtworks.xstream:xstream") diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java index 2c3249b024..3c4cffcfaf 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.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. @@ -275,7 +275,7 @@ public abstract class AbstractMessageConverter implements SmartMessageConverter * @param message the input message * @param targetClass the target class for the conversion * @param conversionHint an extra object passed to the {@link MessageConverter}, - * for example, the associated {@code MethodParameter} (may be {@code null}} + * for example, the associated {@code MethodParameter} (may be {@code null}) * @return the result of the conversion, or {@code null} if the converter cannot * perform the conversion * @since 4.2 @@ -291,7 +291,7 @@ public abstract class AbstractMessageConverter implements SmartMessageConverter * @param payload the Object to convert * @param headers optional headers for the message (may be {@code null}) * @param conversionHint an extra object passed to the {@link MessageConverter}, - * for example, the associated {@code MethodParameter} (may be {@code null}} + * for example, the associated {@code MethodParameter} (may be {@code null}) * @return the resulting payload for the message, or {@code null} if the converter * cannot perform the conversion * @since 4.2 diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/JacksonJsonMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/JacksonJsonMessageConverter.java new file mode 100644 index 0000000000..8a48d0f351 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/JacksonJsonMessageConverter.java @@ -0,0 +1,243 @@ +/* + * 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.messaging.converter; + +import java.io.ByteArrayOutputStream; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.charset.Charset; + +import com.fasterxml.jackson.annotation.JsonView; +import org.jspecify.annotations.Nullable; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonEncoding; +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.cfg.MapperBuilder; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.MimeType; + +/** + * A Jackson 3.x based {@link MessageConverter} implementation. + * + *

The default constructor loads {@link tools.jackson.databind.JacksonModule}s + * found by {@link MapperBuilder#findModules(ClassLoader)}. + * + * @author Sebastien Deleuze + * @since 7.0 + */ +public class JacksonJsonMessageConverter extends AbstractMessageConverter { + + private static final MimeType[] DEFAULT_MIME_TYPES = new MimeType[] { + new MimeType("application", "json"), new MimeType("application", "*+json")}; + + private final ObjectMapper objectMapper; + + + /** + * Construct a new instance with a {@link JsonMapper} customized with the + * {@link tools.jackson.databind.JacksonModule}s found by + * {@link MapperBuilder#findModules(ClassLoader)}. + */ + public JacksonJsonMessageConverter() { + this(DEFAULT_MIME_TYPES); + } + + /** + * Construct a new instance with a {@link JsonMapper} customized + * with the {@link tools.jackson.databind.JacksonModule}s found + * by {@link MapperBuilder#findModules(ClassLoader)} and the + * provided {@link MimeType}s. + * @param supportedMimeTypes the supported MIME types + */ + public JacksonJsonMessageConverter(MimeType... supportedMimeTypes) { + super(supportedMimeTypes); + this.objectMapper = JsonMapper.builder().findAndAddModules(JacksonJsonMessageConverter.class.getClassLoader()).build(); + } + + /** + * Construct a new instance with the provided {@link ObjectMapper}. + * @see JsonMapper#builder() + * @see MapperBuilder#findModules(ClassLoader) + */ + public JacksonJsonMessageConverter(ObjectMapper objectMapper) { + this(objectMapper, DEFAULT_MIME_TYPES); + } + + /** + * Construct a new instance with the provided {@link ObjectMapper} and the + * provided {@link MimeType}s. + * @see JsonMapper#builder() + * @see MapperBuilder#findModules(ClassLoader) + */ + public JacksonJsonMessageConverter(ObjectMapper objectMapper, MimeType... supportedMimeTypes) { + super(supportedMimeTypes); + Assert.notNull(objectMapper, "ObjectMapper must not be null"); + this.objectMapper = objectMapper; + } + + /** + * Return the underlying {@code ObjectMapper} for this converter. + */ + protected ObjectMapper getObjectMapper() { + return this.objectMapper; + } + + @Override + protected boolean canConvertFrom(Message message, @Nullable Class targetClass) { + return targetClass != null && supportsMimeType(message.getHeaders()); + } + + @Override + protected boolean canConvertTo(Object payload, @Nullable MessageHeaders headers) { + return supportsMimeType(headers); + } + + @Override + protected boolean supports(Class clazz) { + // should not be called, since we override canConvertFrom/canConvertTo instead + throw new UnsupportedOperationException(); + } + + @Override + protected @Nullable Object convertFromInternal(Message message, Class targetClass, @Nullable Object conversionHint) { + JavaType javaType = this.objectMapper.constructType(getResolvedType(targetClass, conversionHint)); + Object payload = message.getPayload(); + Class view = getSerializationView(conversionHint); + try { + if (ClassUtils.isAssignableValue(targetClass, payload)) { + return payload; + } + else if (payload instanceof byte[] bytes) { + if (view != null) { + return this.objectMapper.readerWithView(view).forType(javaType).readValue(bytes); + } + else { + return this.objectMapper.readValue(bytes, javaType); + } + } + else { + // Assuming a text-based source payload + if (view != null) { + return this.objectMapper.readerWithView(view).forType(javaType).readValue(payload.toString()); + } + else { + return this.objectMapper.readValue(payload.toString(), javaType); + } + } + } + catch (JacksonException ex) { + throw new MessageConversionException(message, "Could not read JSON: " + ex.getMessage(), ex); + } + } + + @Override + protected @Nullable Object convertToInternal(Object payload, @Nullable MessageHeaders headers, + @Nullable Object conversionHint) { + + try { + Class view = getSerializationView(conversionHint); + if (byte[].class == getSerializedPayloadClass()) { + ByteArrayOutputStream out = new ByteArrayOutputStream(1024); + JsonEncoding encoding = getJsonEncoding(getMimeType(headers)); + try (JsonGenerator generator = this.objectMapper.createGenerator(out, encoding)) { + if (view != null) { + this.objectMapper.writerWithView(view).writeValue(generator, payload); + } + else { + this.objectMapper.writeValue(generator, payload); + } + payload = out.toByteArray(); + } + } + else { + // Assuming a text-based target payload + Writer writer = new StringWriter(1024); + if (view != null) { + this.objectMapper.writerWithView(view).writeValue(writer, payload); + } + else { + this.objectMapper.writeValue(writer, payload); + } + payload = writer.toString(); + } + } + catch (JacksonException ex) { + throw new MessageConversionException("Could not write JSON: " + ex.getMessage(), ex); + } + return payload; + } + + /** + * Determine a Jackson serialization view based on the given conversion hint. + * @param conversionHint the conversion hint Object as passed into the + * converter for the current conversion attempt + * @return the serialization view class, or {@code null} if none + */ + protected @Nullable Class getSerializationView(@Nullable Object conversionHint) { + if (conversionHint instanceof MethodParameter param) { + JsonView annotation = (param.getParameterIndex() >= 0 ? + param.getParameterAnnotation(JsonView.class) : param.getMethodAnnotation(JsonView.class)); + if (annotation != null) { + return extractViewClass(annotation, conversionHint); + } + } + else if (conversionHint instanceof JsonView jsonView) { + return extractViewClass(jsonView, conversionHint); + } + else if (conversionHint instanceof Class clazz) { + return clazz; + } + + // No JSON view specified... + return null; + } + + private Class extractViewClass(JsonView annotation, Object conversionHint) { + Class[] classes = annotation.value(); + if (classes.length != 1) { + throw new IllegalArgumentException( + "@JsonView only supported for handler methods with exactly 1 class argument: " + conversionHint); + } + return classes[0]; + } + + /** + * Determine the JSON encoding to use for the given content type. + * @param contentType the MIME type from the MessageHeaders, if any + * @return the JSON encoding to use (never {@code null}) + */ + protected JsonEncoding getJsonEncoding(@Nullable MimeType contentType) { + if (contentType != null && contentType.getCharset() != null) { + Charset charset = contentType.getCharset(); + for (JsonEncoding encoding : JsonEncoding.values()) { + if (charset.name().equals(encoding.getJavaName())) { + return encoding; + } + } + } + return JsonEncoding.UTF8; + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java index 88a89fa95b..819bd2343a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java @@ -56,7 +56,9 @@ import org.springframework.util.MimeType; * @author Juergen Hoeller * @author Sebastien Deleuze * @since 4.0 + * @deprecated since 7.0 in favor of {@link JacksonJsonMessageConverter} */ +@Deprecated(since = "7.0", forRemoval = true) public class MappingJackson2MessageConverter extends AbstractMessageConverter { private static final MimeType[] DEFAULT_MIME_TYPES = new MimeType[] { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/SmartMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/SmartMessageConverter.java index 5120b1a6a2..6809251562 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/SmartMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/SmartMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -40,7 +40,7 @@ public interface SmartMessageConverter extends MessageConverter { * @param message the input message * @param targetClass the target class for the conversion * @param conversionHint an extra object passed to the {@link MessageConverter}, - * for example, the associated {@code MethodParameter} (may be {@code null}} + * for example, the associated {@code MethodParameter} (may be {@code null}) * @return the result of the conversion, or {@code null} if the converter cannot * perform the conversion * @see #fromMessage(Message, Class) @@ -54,7 +54,7 @@ public interface SmartMessageConverter extends MessageConverter { * @param payload the Object to convert * @param headers optional headers for the message (may be {@code null}) * @param conversionHint an extra object passed to the {@link MessageConverter}, - * for example, the associated {@code MethodParameter} (may be {@code null}} + * for example, the associated {@code MethodParameter} (may be {@code null}) * @return the new message, or {@code null} if the converter does not support the * Object type or the target media type * @see #toMessage(Object, MessageHeaders) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MetadataEncoder.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MetadataEncoder.java index 5f153b3c39..52972be09a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MetadataEncoder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MetadataEncoder.java @@ -106,8 +106,7 @@ final class MetadataEncoder { Matcher matcher = VARS_PATTERN.matcher(route); while (matcher.find()) { Assert.isTrue(index < routeVars.length, () -> "No value for variable '" + matcher.group(1) + "'"); - String value = routeVars[index].toString(); - value = value.contains(".") ? value.replaceAll("\\.", "%2E") : value; + String value = routeVars[index].toString().replace(".", "%2E"); matcher.appendReplacement(sb, value); index++; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java index baac049c56..108651f3ea 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.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. @@ -310,9 +310,9 @@ public interface RSocketRequester extends Disposable { * @param port the server port * @return an {@code RSocketRequester} for the connection * @see TcpClientTransport - * @deprecated as of 5.3 in favor of {@link #tcp(String, int)} + * @deprecated in favor of {@link #tcp(String, int)} */ - @Deprecated + @Deprecated(since = "5.3") Mono connectTcp(String host, int port); /** @@ -320,18 +320,18 @@ public interface RSocketRequester extends Disposable { * @param uri the RSocket server endpoint URI * @return an {@code RSocketRequester} for the connection * @see WebsocketClientTransport - * @deprecated as of 5.3 in favor of {@link #websocket(URI)} + * @deprecated in favor of {@link #websocket(URI)} */ - @Deprecated + @Deprecated(since = "5.3") Mono connectWebSocket(URI uri); /** * Connect to the server with the given {@code ClientTransport}. * @param transport the client transport to use * @return an {@code RSocketRequester} for the connection - * @deprecated as of 5.3 in favor of {@link #transport(ClientTransport)} + * @deprecated in favor of {@link #transport(ClientTransport)} */ - @Deprecated + @Deprecated(since = "5.3") Mono connect(ClientTransport transport); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java index b55bda3dc0..7ccf4664ef 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java @@ -37,6 +37,7 @@ import org.springframework.messaging.converter.ByteArrayMessageConverter; import org.springframework.messaging.converter.CompositeMessageConverter; import org.springframework.messaging.converter.DefaultContentTypeResolver; import org.springframework.messaging.converter.GsonMessageConverter; +import org.springframework.messaging.converter.JacksonJsonMessageConverter; import org.springframework.messaging.converter.JsonbMessageConverter; import org.springframework.messaging.converter.KotlinSerializationJsonMessageConverter; import org.springframework.messaging.converter.MappingJackson2MessageConverter; @@ -103,6 +104,8 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC private static final String MVC_VALIDATOR_NAME = "mvcValidator"; + private static final boolean jacksonPresent; + private static final boolean jackson2Present; private static final boolean gsonPresent; @@ -114,6 +117,7 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC static { ClassLoader classLoader = AbstractMessageBrokerConfiguration.class.getClassLoader(); + jacksonPresent = ClassUtils.isPresent("tools.jackson.databind.ObjectMapper", classLoader); jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader); gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader); @@ -501,7 +505,10 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC if (kotlinSerializationJsonPresent) { converters.add(new KotlinSerializationJsonMessageConverter()); } - if (jackson2Present) { + if (jacksonPresent) { + converters.add(createJacksonJsonConverter()); + } + else if (jackson2Present) { converters.add(createJacksonConverter()); } else if (gsonPresent) { @@ -514,6 +521,21 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC return new CompositeMessageConverter(converters); } + /** + * Allow to customize Jackson 3.x JSON converter. + */ + protected JacksonJsonMessageConverter createJacksonJsonConverter() { + DefaultContentTypeResolver resolver = new DefaultContentTypeResolver(); + resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON); + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(); + converter.setContentTypeResolver(resolver); + return converter; + } + + /** + * Allow to customize Jackson 2.x JSON converter. + */ + @SuppressWarnings("removal") protected MappingJackson2MessageConverter createJacksonConverter() { DefaultContentTypeResolver resolver = new DefaultContentTypeResolver(); resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompSession.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompSession.java index 84b549aaa4..5fecbc11e7 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompSession.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompSession.java @@ -526,8 +526,7 @@ public class DefaultStompSession implements ConnectionHandlingStompSession { try { conn.close(); } - catch (Throwable ex) { - // ignore + catch (Throwable ignored) { } } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java index dff4c9ced0..093f9f9667 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.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. @@ -331,7 +331,7 @@ public class MessageHeaderAccessor { protected void verifyType(@Nullable String headerName, @Nullable Object headerValue) { if (headerName != null && headerValue != null) { if (MessageHeaders.ERROR_CHANNEL.equals(headerName) || - MessageHeaders.REPLY_CHANNEL.endsWith(headerName)) { + MessageHeaders.REPLY_CHANNEL.equals(headerName)) { if (!(headerValue instanceof MessageChannel || headerValue instanceof String)) { throw new IllegalArgumentException( "'" + headerName + "' header value must be a MessageChannel or String"); diff --git a/spring-messaging/src/test/java/org/springframework/messaging/converter/MappingJackson2MessageConverterTests.java b/spring-messaging/src/test/java/org/springframework/messaging/converter/MappingJackson2MessageConverterTests.java index 0f3cc097dd..a3a5624417 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/converter/MappingJackson2MessageConverterTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/converter/MappingJackson2MessageConverterTests.java @@ -43,6 +43,7 @@ import static org.assertj.core.api.Assertions.within; * @author Rossen Stoyanchev * @author Sebastien Deleuze */ +@SuppressWarnings("removal") class MappingJackson2MessageConverterTests { @Test diff --git a/spring-messaging/src/test/java/org/springframework/messaging/core/DestinationResolvingMessagingTemplateTests.java b/spring-messaging/src/test/java/org/springframework/messaging/core/DestinationResolvingMessagingTemplateTests.java index 4fb1006d1d..cc26993e81 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/core/DestinationResolvingMessagingTemplateTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/core/DestinationResolvingMessagingTemplateTests.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,7 +16,6 @@ package org.springframework.messaging.core; -import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -38,29 +37,20 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; */ class DestinationResolvingMessagingTemplateTests { - private TestDestinationResolvingMessagingTemplate template; + private final TestDestinationResolvingMessagingTemplate template = new TestDestinationResolvingMessagingTemplate(); - private ExecutorSubscribableChannel myChannel; + private final ExecutorSubscribableChannel myChannel = new ExecutorSubscribableChannel(); - private Map headers; + private final Map headers = Map.of("key", "value"); - private TestMessagePostProcessor postProcessor; + private final TestMessagePostProcessor postProcessor = new TestMessagePostProcessor(); @BeforeEach void setup() { - TestMessageChannelDestinationResolver resolver = new TestMessageChannelDestinationResolver(); - - this.myChannel = new ExecutorSubscribableChannel(); resolver.registerMessageChannel("myChannel", this.myChannel); - - this.template = new TestDestinationResolvingMessagingTemplate(); this.template.setDestinationResolver(resolver); - - this.headers = Collections.singletonMap("key", "value"); - - this.postProcessor = new TestMessagePostProcessor(); } @@ -76,8 +66,8 @@ class DestinationResolvingMessagingTemplateTests { @Test void sendNoDestinationResolver() { TestDestinationResolvingMessagingTemplate template = new TestDestinationResolvingMessagingTemplate(); - assertThatIllegalStateException().isThrownBy(() -> - template.send("myChannel", new GenericMessage<>("payload"))); + assertThatIllegalStateException() + .isThrownBy(() -> template.send("myChannel", new GenericMessage<>("payload"))); } @Test @@ -240,19 +230,21 @@ class DestinationResolvingMessagingTemplateTests { } } -} -class TestMessageChannelDestinationResolver implements DestinationResolver { + private static class TestMessageChannelDestinationResolver implements DestinationResolver { - private final Map channels = new HashMap<>(); + private final Map channels = new HashMap<>(); - public void registerMessageChannel(String name, MessageChannel channel) { - this.channels.put(name, channel); + public void registerMessageChannel(String name, MessageChannel channel) { + this.channels.put(name, channel); + } + + @Override + public MessageChannel resolveDestination(String name) throws DestinationResolutionException { + return this.channels.get(name); + } + } - @Override - public MessageChannel resolveDestination(String name) throws DestinationResolutionException { - return this.channels.get(name); - } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/core/MessageSendingTemplateTests.java b/spring-messaging/src/test/java/org/springframework/messaging/core/MessageSendingTemplateTests.java index 98f707b3f0..3d07312803 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/core/MessageSendingTemplateTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/core/MessageSendingTemplateTests.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,13 +21,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.converter.CompositeMessageConverter; -import org.springframework.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.messaging.converter.JacksonJsonMessageConverter; import org.springframework.messaging.converter.MessageConversionException; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.converter.StringMessageConverter; @@ -47,21 +46,15 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; */ class MessageSendingTemplateTests { - private TestMessageSendingTemplate template; + private final TestMessageSendingTemplate template = new TestMessageSendingTemplate(); - private TestMessagePostProcessor postProcessor; + private final TestMessagePostProcessor postProcessor = new TestMessagePostProcessor(); - private Map headers; + private final Map headers = new HashMap<>() {{ + put("key", "value"); + }}; - @BeforeEach - void setup() { - this.template = new TestMessageSendingTemplate(); - this.postProcessor = new TestMessagePostProcessor(); - this.headers = new HashMap<>(); - this.headers.put("key", "value"); - } - @Test void send() { Message message = new GenericMessage("payload"); @@ -84,8 +77,7 @@ class MessageSendingTemplateTests { @Test void sendMissingDestination() { Message message = new GenericMessage("payload"); - assertThatIllegalStateException().isThrownBy(() -> - this.template.send(message)); + assertThatIllegalStateException().isThrownBy(() -> this.template.send(message)); } @Test @@ -177,14 +169,12 @@ class MessageSendingTemplateTests { @Test void convertAndSendNoMatchingConverter() { - - MessageConverter converter = new CompositeMessageConverter( - List.of(new MappingJackson2MessageConverter())); + MessageConverter converter = new CompositeMessageConverter(List.of(new JacksonJsonMessageConverter())); this.template.setMessageConverter(converter); this.headers.put(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_XML); - assertThatExceptionOfType(MessageConversionException.class).isThrownBy(() -> - this.template.convertAndSend("home", "payload", new MessageHeaders(this.headers))); + assertThatExceptionOfType(MessageConversionException.class) + .isThrownBy(() -> this.template.convertAndSend("home", "payload", new MessageHeaders(this.headers))); } @@ -202,19 +192,3 @@ class MessageSendingTemplateTests { } } - -class TestMessagePostProcessor implements MessagePostProcessor { - - private Message message; - - - Message getMessage() { - return this.message; - } - - @Override - public Message postProcessMessage(Message message) { - this.message = message; - return message; - } -} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/core/TestMessagePostProcessor.java b/spring-messaging/src/test/java/org/springframework/messaging/core/TestMessagePostProcessor.java new file mode 100644 index 0000000000..81275d0cf9 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/core/TestMessagePostProcessor.java @@ -0,0 +1,36 @@ +/* + * 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.messaging.core; + +import org.springframework.messaging.Message; + +public class TestMessagePostProcessor implements MessagePostProcessor { + + private Message message; + + + Message getMessage() { + return this.message; + } + + @Override + public Message postProcessMessage(Message message) { + this.message = message; + return message; + } + +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolverTests.java index 9f5e884f57..a214e519eb 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolverTests.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. @@ -218,6 +218,7 @@ class MessageMethodArgumentResolverTests { } @Test // SPR-16486 + @SuppressWarnings("removal") public void resolveWithJacksonConverter() throws Exception { Message inMessage = MessageBuilder.withPayload("{\"foo\":\"bar\"}").build(); MethodParameter parameter = new MethodParameter(this.method, 5); diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java index ae55bc99d4..b12e472241 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.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. @@ -41,7 +41,7 @@ import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.messaging.converter.JacksonJsonMessageConverter; import org.springframework.messaging.converter.StringMessageConverter; import org.springframework.messaging.handler.DestinationPatternsMessageCondition; import org.springframework.messaging.handler.annotation.SendTo; @@ -129,7 +129,7 @@ public class SendToMethodReturnValueHandlerTests { this.handlerAnnotationNotRequired = new SendToMethodReturnValueHandler(messagingTemplate, false); SimpMessagingTemplate jsonMessagingTemplate = new SimpMessagingTemplate(this.messageChannel); - jsonMessagingTemplate.setMessageConverter(new MappingJackson2MessageConverter()); + jsonMessagingTemplate.setMessageConverter(new JacksonJsonMessageConverter()); this.jsonHandler = new SendToMethodReturnValueHandler(jsonMessagingTemplate, true); } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SubscriptionMethodReturnValueHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SubscriptionMethodReturnValueHandlerTests.java index 0c412d4c61..05ec0a5181 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SubscriptionMethodReturnValueHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SubscriptionMethodReturnValueHandlerTests.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. @@ -33,7 +33,7 @@ import org.springframework.core.MethodParameter; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.messaging.converter.JacksonJsonMessageConverter; import org.springframework.messaging.converter.StringMessageConverter; import org.springframework.messaging.core.MessageSendingOperations; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -59,9 +59,9 @@ import static org.mockito.Mockito.verify; * @author Sebastien Deleuze */ @ExtendWith(MockitoExtension.class) -public class SubscriptionMethodReturnValueHandlerTests { +class SubscriptionMethodReturnValueHandlerTests { - public static final MimeType MIME_TYPE = new MimeType("text", "plain", StandardCharsets.UTF_8); + private static final MimeType MIME_TYPE = new MimeType("text", "plain", StandardCharsets.UTF_8); private static final String PAYLOAD = "payload"; @@ -92,19 +92,19 @@ public class SubscriptionMethodReturnValueHandlerTests { this.handler = new SubscriptionMethodReturnValueHandler(messagingTemplate); SimpMessagingTemplate jsonMessagingTemplate = new SimpMessagingTemplate(this.messageChannel); - jsonMessagingTemplate.setMessageConverter(new MappingJackson2MessageConverter()); + jsonMessagingTemplate.setMessageConverter(new JacksonJsonMessageConverter()); this.jsonHandler = new SubscriptionMethodReturnValueHandler(jsonMessagingTemplate); - Method method = this.getClass().getDeclaredMethod("getData"); + Method method = getClass().getDeclaredMethod("getData"); this.subscribeEventReturnType = new MethodParameter(method, -1); - method = this.getClass().getDeclaredMethod("getDataAndSendTo"); + method = getClass().getDeclaredMethod("getDataAndSendTo"); this.subscribeEventSendToReturnType = new MethodParameter(method, -1); - method = this.getClass().getDeclaredMethod("handle"); + method = getClass().getDeclaredMethod("handle"); this.messageMappingReturnType = new MethodParameter(method, -1); - method = this.getClass().getDeclaredMethod("getJsonView"); + method = getClass().getDeclaredMethod("getJsonView"); this.subscribeEventJsonViewReturnType = new MethodParameter(method, -1); } @@ -144,7 +144,7 @@ public class SubscriptionMethodReturnValueHandlerTests { @Test @SuppressWarnings({ "unchecked", "rawtypes" }) - public void testHeadersPassedToMessagingTemplate() throws Exception { + void testHeadersPassedToMessagingTemplate() throws Exception { String sessionId = "sess1"; String subscriptionId = "subs1"; String destination = "/dest"; diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java index e8ad5acb25..6ba2e99aff 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java @@ -40,8 +40,8 @@ import org.springframework.messaging.converter.ByteArrayMessageConverter; import org.springframework.messaging.converter.CompositeMessageConverter; import org.springframework.messaging.converter.ContentTypeResolver; import org.springframework.messaging.converter.DefaultContentTypeResolver; +import org.springframework.messaging.converter.JacksonJsonMessageConverter; import org.springframework.messaging.converter.KotlinSerializationJsonMessageConverter; -import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.converter.StringMessageConverter; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -288,9 +288,9 @@ class MessageBrokerConfigurationTests { List converters = compositeConverter.getConverters(); assertThat(converters).hasExactlyElementsOfTypes(StringMessageConverter.class, ByteArrayMessageConverter.class, - KotlinSerializationJsonMessageConverter.class, MappingJackson2MessageConverter.class); + KotlinSerializationJsonMessageConverter.class, JacksonJsonMessageConverter.class); - ContentTypeResolver resolver = ((MappingJackson2MessageConverter) converters.get(3)).getContentTypeResolver(); + ContentTypeResolver resolver = ((JacksonJsonMessageConverter) converters.get(3)).getContentTypeResolver(); assertThat(((DefaultContentTypeResolver) resolver).getDefaultMimeType()).isEqualTo(MimeTypeUtils.APPLICATION_JSON); } @@ -349,7 +349,7 @@ class MessageBrokerConfigurationTests { assertThat(iterator.next()).isInstanceOf(StringMessageConverter.class); assertThat(iterator.next()).isInstanceOf(ByteArrayMessageConverter.class); assertThat(iterator.next()).isInstanceOf(KotlinSerializationJsonMessageConverter.class); - assertThat(iterator.next()).isInstanceOf(MappingJackson2MessageConverter.class); + assertThat(iterator.next()).isInstanceOf(JacksonJsonMessageConverter.class); } @Test diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/user/MultiServerUserRegistryTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/user/MultiServerUserRegistryTests.java index af866b37e8..01f7e0ae4e 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/user/MultiServerUserRegistryTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/user/MultiServerUserRegistryTests.java @@ -25,7 +25,7 @@ import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.messaging.Message; -import org.springframework.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.messaging.converter.JacksonJsonMessageConverter; import org.springframework.messaging.converter.MessageConverter; import static org.assertj.core.api.Assertions.assertThat; @@ -43,7 +43,7 @@ class MultiServerUserRegistryTests { private final MultiServerUserRegistry registry = new MultiServerUserRegistry(this.localRegistry); - private final MessageConverter converter = new MappingJackson2MessageConverter(); + private final MessageConverter converter = new JacksonJsonMessageConverter(); @Test diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/user/UserRegistryMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/user/UserRegistryMessageHandlerTests.java index 4c9c4f6717..a21562df3f 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/user/UserRegistryMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/user/UserRegistryMessageHandlerTests.java @@ -29,7 +29,7 @@ import org.mockito.ArgumentCaptor; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.messaging.converter.JacksonJsonMessageConverter; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessagingTemplate; @@ -59,7 +59,7 @@ class UserRegistryMessageHandlerTests { private MultiServerUserRegistry multiServerRegistry = new MultiServerUserRegistry(this.localRegistry); - private MessageConverter converter = new MappingJackson2MessageConverter(); + private MessageConverter converter = new JacksonJsonMessageConverter(); private UserRegistryMessageHandler handler; diff --git a/spring-messaging/src/test/java/org/springframework/messaging/support/MessageBuilderTests.java b/spring-messaging/src/test/java/org/springframework/messaging/support/MessageBuilderTests.java index 42aae2a371..cc4078c35b 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/support/MessageBuilderTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/support/MessageBuilderTests.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. @@ -34,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * @author Mark Fisher * @author Rossen Stoyanchev + * @author Mengqi Xu */ class MessageBuilderTests { @@ -238,4 +239,22 @@ class MessageBuilderTests { assertThat(message3.getHeaders().get("foo")).isEqualTo("bar3"); } + @Test // gh-34949 + void buildMessageWithReplyChannelHeader() { + MessageHeaderAccessor headerAccessor = new MessageHeaderAccessor(); + MessageBuilder messageBuilder = MessageBuilder.withPayload("payload").setHeaders(headerAccessor); + + headerAccessor.setHeader(MessageHeaders.REPLY_CHANNEL, "foo"); + Message message1 = messageBuilder.build(); + assertThat(message1.getHeaders().get(MessageHeaders.REPLY_CHANNEL)).isEqualTo("foo"); + + headerAccessor.setHeader("hannel", 0); + Message message2 = messageBuilder.build(); + assertThat(message2.getHeaders().get("hannel")).isEqualTo(0); + + assertThatIllegalArgumentException() + .isThrownBy(() -> headerAccessor.setHeader(MessageHeaders.REPLY_CHANNEL, 0)) + .withMessage("'%s' header value must be a MessageChannel or String", MessageHeaders.REPLY_CHANNEL); + } + } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/JpaTransactionManagerTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/JpaTransactionManagerTests.java index 9fcb07b8b6..80ad93b08f 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/JpaTransactionManagerTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/JpaTransactionManagerTests.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,7 +16,6 @@ package org.springframework.orm.jpa; -import java.util.ArrayList; import java.util.List; import jakarta.persistence.EntityManager; @@ -46,21 +45,25 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; /** + * Unit tests for {@link JpaTransactionManager}. * @author Costin Leau * @author Juergen Hoeller * @author Phillip Webb */ class JpaTransactionManagerTests { - private EntityManagerFactory factory = mock(); + private static final List testList = List.of("test"); - private EntityManager manager = mock(); - private EntityTransaction tx = mock(); + private final EntityManagerFactory factory = mock(); - private JpaTransactionManager tm = new JpaTransactionManager(factory); + private final EntityManager manager = mock(); - private TransactionTemplate tt = new TransactionTemplate(tm); + private final EntityTransaction tx = mock(); + + private final JpaTransactionManager tm = new JpaTransactionManager(factory); + + private final TransactionTemplate tt = new TransactionTemplate(tm); @BeforeEach @@ -81,20 +84,15 @@ class JpaTransactionManagerTests { @Test void testTransactionCommit() { - given(manager.getTransaction()).willReturn(tx); - - final List l = new ArrayList<>(); - l.add("test"); - assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); Object result = tt.execute(status -> { assertThat(TransactionSynchronizationManager.hasResource(factory)).isTrue(); EntityManagerFactoryUtils.getTransactionalEntityManager(factory).flush(); - return l; + return testList; }); - assertThat(result).isSameAs(l); + assertThat(result).isSameAs(testList); assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -106,13 +104,9 @@ class JpaTransactionManagerTests { @Test void testTransactionCommitWithRollbackException() { - given(manager.getTransaction()).willReturn(tx); given(tx.getRollbackOnly()).willReturn(true); willThrow(new RollbackException()).given(tx).commit(); - final List l = new ArrayList<>(); - l.add("test"); - assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -120,9 +114,9 @@ class JpaTransactionManagerTests { Object result = tt.execute(status -> { assertThat(TransactionSynchronizationManager.hasResource(factory)).isTrue(); EntityManagerFactoryUtils.getTransactionalEntityManager(factory).flush(); - return l; + return testList; }); - assertThat(result).isSameAs(l); + assertThat(result).isSameAs(testList); } catch (TransactionSystemException tse) { // expected @@ -138,12 +132,8 @@ class JpaTransactionManagerTests { @Test void testTransactionRollback() { - given(manager.getTransaction()).willReturn(tx); given(tx.isActive()).willReturn(true); - final List l = new ArrayList<>(); - l.add("test"); - assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -163,11 +153,6 @@ class JpaTransactionManagerTests { @Test void testTransactionRollbackWithAlreadyRolledBack() { - given(manager.getTransaction()).willReturn(tx); - - final List l = new ArrayList<>(); - l.add("test"); - assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -186,12 +171,8 @@ class JpaTransactionManagerTests { @Test void testTransactionRollbackOnly() { - given(manager.getTransaction()).willReturn(tx); given(tx.isActive()).willReturn(true); - final List l = new ArrayList<>(); - l.add("test"); - assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -201,7 +182,7 @@ class JpaTransactionManagerTests { EntityManagerFactoryUtils.getTransactionalEntityManager(factory).flush(); status.setRollbackOnly(); - return l; + return testList; }); assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); @@ -214,11 +195,6 @@ class JpaTransactionManagerTests { @Test void testParticipatingTransactionWithCommit() { - given(manager.getTransaction()).willReturn(tx); - - final List l = new ArrayList<>(); - l.add("test"); - assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -227,7 +203,7 @@ class JpaTransactionManagerTests { return tt.execute(status1 -> { EntityManagerFactoryUtils.getTransactionalEntityManager(factory).flush(); - return l; + return testList; }); }); @@ -241,12 +217,8 @@ class JpaTransactionManagerTests { @Test void testParticipatingTransactionWithRollback() { - given(manager.getTransaction()).willReturn(tx); given(tx.isActive()).willReturn(true); - final List l = new ArrayList<>(); - l.add("test"); - assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -269,14 +241,10 @@ class JpaTransactionManagerTests { @Test void testParticipatingTransactionWithRollbackOnly() { - given(manager.getTransaction()).willReturn(tx); given(tx.isActive()).willReturn(true); given(tx.getRollbackOnly()).willReturn(true); willThrow(new RollbackException()).given(tx).commit(); - final List l = new ArrayList<>(); - l.add("test"); - assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -304,13 +272,6 @@ class JpaTransactionManagerTests { void testParticipatingTransactionWithRequiresNew() { tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - given(factory.createEntityManager()).willReturn(manager); - given(manager.getTransaction()).willReturn(tx); - given(manager.isOpen()).willReturn(true); - - final List l = new ArrayList<>(); - l.add("test"); - assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -318,10 +279,10 @@ class JpaTransactionManagerTests { assertThat(TransactionSynchronizationManager.hasResource(factory)).isTrue(); return tt.execute(status1 -> { EntityManagerFactoryUtils.getTransactionalEntityManager(factory).flush(); - return l; + return testList; }); }); - assertThat(result).isSameAs(l); + assertThat(result).isSameAs(testList); assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -335,11 +296,6 @@ class JpaTransactionManagerTests { void testParticipatingTransactionWithRequiresNewAndPrebound() { tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - given(manager.getTransaction()).willReturn(tx); - - final List l = new ArrayList<>(); - l.add("test"); - assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -352,10 +308,10 @@ class JpaTransactionManagerTests { assertThat(TransactionSynchronizationManager.hasResource(factory)).isTrue(); return tt.execute(status1 -> { EntityManagerFactoryUtils.getTransactionalEntityManager(factory).flush(); - return l; + return testList; }); }); - assertThat(result).isSameAs(l); + assertThat(result).isSameAs(testList); } finally { TransactionSynchronizationManager.unbindResource(factory); @@ -374,11 +330,6 @@ class JpaTransactionManagerTests { void testPropagationSupportsAndRequiresNew() { tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); - given(manager.getTransaction()).willReturn(tx); - - final List l = new ArrayList<>(); - l.add("test"); - assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -388,10 +339,10 @@ class JpaTransactionManagerTests { tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); return tt2.execute(status1 -> { EntityManagerFactoryUtils.getTransactionalEntityManager(factory).flush(); - return l; + return testList; }); }); - assertThat(result).isSameAs(l); + assertThat(result).isSameAs(testList); assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -405,13 +356,6 @@ class JpaTransactionManagerTests { void testPropagationSupportsAndRequiresNewAndEarlyAccess() { tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); - given(factory.createEntityManager()).willReturn(manager); - given(manager.getTransaction()).willReturn(tx); - given(manager.isOpen()).willReturn(true); - - final List l = new ArrayList<>(); - l.add("test"); - assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -423,10 +367,10 @@ class JpaTransactionManagerTests { tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); return tt2.execute(status1 -> { EntityManagerFactoryUtils.getTransactionalEntityManager(factory).flush(); - return l; + return testList; }); }); - assertThat(result).isSameAs(l); + assertThat(result).isSameAs(testList); assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -443,7 +387,6 @@ class JpaTransactionManagerTests { EntityManager manager2 = mock(); EntityTransaction tx2 = mock(); - given(manager.getTransaction()).willReturn(tx); given(factory.createEntityManager()).willReturn(manager, manager2); given(manager2.getTransaction()).willReturn(tx2); given(manager2.isOpen()).willReturn(true); @@ -479,11 +422,6 @@ class JpaTransactionManagerTests { @Test void testTransactionCommitWithPropagationSupports() { - given(manager.isOpen()).willReturn(true); - - final List l = new ArrayList<>(); - l.add("test"); - tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); @@ -494,9 +432,9 @@ class JpaTransactionManagerTests { assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); assertThat(status.isNewTransaction()).isFalse(); EntityManagerFactoryUtils.getTransactionalEntityManager(factory).flush(); - return l; + return testList; }); - assertThat(result).isSameAs(l); + assertThat(result).isSameAs(testList); assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -507,8 +445,6 @@ class JpaTransactionManagerTests { @Test void testTransactionRollbackWithPropagationSupports() { - given(manager.isOpen()).willReturn(true); - tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); @@ -532,11 +468,6 @@ class JpaTransactionManagerTests { @Test void testTransactionCommitWithPrebound() { - given(manager.getTransaction()).willReturn(tx); - - final List l = new ArrayList<>(); - l.add("test"); - assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); TransactionSynchronizationManager.bindResource(factory, new EntityManagerHolder(manager)); @@ -546,9 +477,9 @@ class JpaTransactionManagerTests { assertThat(TransactionSynchronizationManager.hasResource(factory)).isTrue(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); EntityManagerFactoryUtils.getTransactionalEntityManager(factory); - return l; + return testList; }); - assertThat(result).isSameAs(l); + assertThat(result).isSameAs(testList); assertThat(TransactionSynchronizationManager.hasResource(factory)).isTrue(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -563,7 +494,6 @@ class JpaTransactionManagerTests { @Test void testTransactionRollbackWithPrebound() { - given(manager.getTransaction()).willReturn(tx); given(tx.isActive()).willReturn(true); assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); @@ -593,9 +523,6 @@ class JpaTransactionManagerTests { @Test void testTransactionCommitWithPreboundAndPropagationSupports() { - final List l = new ArrayList<>(); - l.add("test"); - tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); @@ -608,9 +535,9 @@ class JpaTransactionManagerTests { assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); assertThat(status.isNewTransaction()).isFalse(); EntityManagerFactoryUtils.getTransactionalEntityManager(factory).flush(); - return l; + return testList; }); - assertThat(result).isSameAs(l); + assertThat(result).isSameAs(testList); assertThat(TransactionSynchronizationManager.hasResource(factory)).isTrue(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -655,8 +582,6 @@ class JpaTransactionManagerTests { void testInvalidIsolation() { tt.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); - given(manager.isOpen()).willReturn(true); - assertThatExceptionOfType(InvalidIsolationLevelException.class).isThrownBy(() -> tt.execute(new TransactionCallbackWithoutResult() { @Override @@ -669,8 +594,6 @@ class JpaTransactionManagerTests { @Test void testTransactionFlush() { - given(manager.getTransaction()).willReturn(tx); - assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); diff --git a/spring-oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java b/spring-oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java index 6f04669aa1..f48c0cfc16 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java @@ -1000,8 +1000,7 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi URI uri = ResourceUtils.toURI(elementNamespace); return uri.getHost(); } - catch (URISyntaxException ex) { - // ignore + catch (URISyntaxException ignored) { } return dataHandler.getName(); } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java index 32c9b9299b..da2ed7128c 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.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. @@ -219,7 +219,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager if (txObject.isNewConnectionHolder()) { synchronizationManager.bindResource(obtainConnectionFactory(), txObject.getConnectionHolder()); } - }).thenReturn(con).onErrorResume(ex -> { + }).onErrorResume(ex -> { if (txObject.isNewConnectionHolder()) { return ConnectionFactoryUtils.releaseConnection(con, obtainConnectionFactory()) .doOnTerminate(() -> txObject.setConnectionHolder(null, false)) @@ -294,7 +294,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager } @Override - protected Mono doCommit(TransactionSynchronizationManager TransactionSynchronizationManager, + protected Mono doCommit(TransactionSynchronizationManager synchronizationManager, GenericReactiveTransaction status) { ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction(); @@ -306,7 +306,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager } @Override - protected Mono doRollback(TransactionSynchronizationManager TransactionSynchronizationManager, + protected Mono doRollback(TransactionSynchronizationManager synchronizationManager, GenericReactiveTransaction status) { ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction(); diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java index 680213cb09..5c48b99728 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.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. @@ -49,6 +49,7 @@ import org.springframework.util.Assert; * @author Juergen Hoeller * @author Mark Paluch * @author Anton Naydenov + * @author Sam Brannen * @since 5.3 */ abstract class NamedParameterUtils { @@ -512,69 +513,17 @@ abstract class NamedParameterUtils { private final BindParameterSource parameterSource; + ExpandedQuery(String expandedSql, NamedParameters parameters, BindParameterSource parameterSource) { this.expandedSql = expandedSql; this.parameters = parameters; this.parameterSource = parameterSource; } - @SuppressWarnings({"rawtypes", "unchecked"}) - public void bind(BindTarget target, String identifier, Parameter parameter) { - List bindMarkers = getBindMarkers(identifier); - if (bindMarkers == null) { - target.bind(identifier, parameter); - return; - } - if (parameter.getValue() instanceof Collection collection) { - Iterator iterator = collection.iterator(); - Iterator markers = bindMarkers.iterator(); - while (iterator.hasNext()) { - Object valueToBind = iterator.next(); - if (valueToBind instanceof Object[] objects) { - for (Object object : objects) { - bind(target, markers, object); - } - } - else { - bind(target, markers, valueToBind); - } - } - } - else { - for (BindMarker bindMarker : bindMarkers) { - bindMarker.bind(target, parameter); - } - } - } - private void bind(BindTarget target, Iterator markers, Object valueToBind) { - Assert.isTrue(markers.hasNext(), () -> String.format( - "No bind marker for value [%s] in SQL [%s]. Check that the query was expanded using the same arguments.", - valueToBind, toQuery())); - markers.next().bind(target, valueToBind); - } - - public void bindNull(BindTarget target, String identifier, Parameter parameter) { - List bindMarkers = getBindMarkers(identifier); - if (bindMarkers == null) { - target.bind(identifier, parameter); - return; - } - for (BindMarker bindMarker : bindMarkers) { - bindMarker.bind(target, parameter); - } - } - - @Nullable List getBindMarkers(String identifier) { - List parameters = this.parameters.getMarker(identifier); - if (parameters == null) { - return null; - } - List markers = new ArrayList<>(); - for (NamedParameters.NamedParameter parameter : parameters) { - markers.addAll(parameter.placeholders); - } - return markers; + @Override + public String toQuery() { + return this.expandedSql; } @Override @@ -595,10 +544,67 @@ abstract class NamedParameterUtils { } } - @Override - public String toQuery() { - return this.expandedSql; + private void bindNull(BindTarget target, String identifier, Parameter parameter) { + List> bindMarkers = getBindMarkers(identifier); + if (bindMarkers == null) { + target.bind(identifier, parameter); + return; + } + for (List outer : bindMarkers) { + for (BindMarker bindMarker : outer) { + bindMarker.bind(target, parameter); + } + } } + + private void bind(BindTarget target, String identifier, Parameter parameter) { + List> bindMarkers = getBindMarkers(identifier); + if (bindMarkers == null) { + target.bind(identifier, parameter); + return; + } + + for (List outer : bindMarkers) { + if (parameter.getValue() instanceof Collection collection) { + Iterator markers = outer.iterator(); + for (Object valueToBind : collection) { + if (valueToBind instanceof Object[] objects) { + for (Object object : objects) { + bind(target, markers, object); + } + } + else { + bind(target, markers, valueToBind); + } + } + } + else { + for (BindMarker bindMarker : outer) { + bindMarker.bind(target, parameter); + } + } + } + } + + private void bind(BindTarget target, Iterator markers, Object valueToBind) { + Assert.isTrue(markers.hasNext(), () -> String.format( + "No bind marker for value [%s] in SQL [%s]. Check that the query was expanded using the same arguments.", + valueToBind, toQuery())); + markers.next().bind(target, valueToBind); + } + + private @Nullable List> getBindMarkers(String identifier) { + List parameters = this.parameters.getMarker(identifier); + if (parameters == null) { + return null; + } + List> markers = new ArrayList<>(); + for (NamedParameters.NamedParameter parameter : parameters) { + markers.add(new ArrayList<>(parameter.placeholders)); + } + return markers; + } + } } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/AnonymousBindMarkers.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/AnonymousBindMarkers.java index 71569702cc..c66ada2ca1 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/AnonymousBindMarkers.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/AnonymousBindMarkers.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. @@ -19,8 +19,9 @@ package org.springframework.r2dbc.core.binding; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; /** - * Anonymous, index-based bind marker using a static placeholder. - * Instances are bound by the ordinal position ordered by the appearance of + * Anonymous, index-based bind markers that use a static placeholder. + * + *

Instances are bound by the ordinal position ordered by the appearance of * the placeholder. This implementation creates indexed bind markers using * an anonymous placeholder that correlates with an index. * @@ -46,7 +47,7 @@ class AnonymousBindMarkers implements BindMarkers { /** - * Create a new {@link AnonymousBindMarkers} instance given {@code placeholder}. + * Create a new {@link AnonymousBindMarkers} instance for the given {@code placeholder}. * @param placeholder parameter bind marker */ AnonymousBindMarkers(String placeholder) { diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarker.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarker.java index 5fb53c47c1..40f8a71313 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarker.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarker.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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,8 @@ import io.r2dbc.spi.Statement; /** * A bind marker represents a single bindable parameter within a query. - * Bind markers are dialect-specific and provide a + * + *

Bind markers are dialect-specific and provide a * {@link #getPlaceholder() placeholder} that is used in the actual query. * * @author Mark Paluch @@ -37,7 +38,8 @@ public interface BindMarker { String getPlaceholder(); /** - * Bind the given {@code value} to the {@link Statement} using the underlying binding strategy. + * Bind the given {@code value} to the {@link Statement} using the underlying + * binding strategy. * @param bindTarget the target to bind the value to * @param value the actual value (must not be {@code null}; * use {@link #bindNull(BindTarget, Class)} for {@code null} values) @@ -46,7 +48,8 @@ public interface BindMarker { void bind(BindTarget bindTarget, Object value); /** - * Bind a {@code null} value to the {@link Statement} using the underlying binding strategy. + * Bind a {@code null} value to the {@link Statement} using the underlying + * binding strategy. * @param bindTarget the target to bind the value to * @param valueType the value type (must not be {@code null}) * @see Statement#bindNull diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkers.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkers.java index af5a9fd48c..5504cbc01e 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkers.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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,10 +20,10 @@ package org.springframework.r2dbc.core.binding; * Bind markers represent placeholders in SQL queries for substitution * for an actual parameter. Using bind markers allows creating safe queries * so query strings are not required to contain escaped values but rather - * the driver encodes parameter in the appropriate representation. + * the driver encodes the parameter in the appropriate representation. * *

{@link BindMarkers} is stateful and can be only used for a single binding - * pass of one or more parameters. It maintains bind indexes/bind parameter names. + * pass of one or more parameters. It maintains bind indexes or bind parameter names. * * @author Mark Paluch * @since 5.3 @@ -41,7 +41,7 @@ public interface BindMarkers { /** * Create a new {@link BindMarker} that accepts a {@code hint}. - * Implementations are allowed to consider/ignore/filter + *

Implementations are allowed to consider/ignore/filter * the name hint to create more expressive bind markers. * @param hint an optional name hint that can be used as part of the bind marker * @return a new {@link BindMarker} diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkersFactoryResolver.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkersFactoryResolver.java index 8a4ff087c8..bac723f4c3 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkersFactoryResolver.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkersFactoryResolver.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. @@ -30,8 +30,8 @@ import org.springframework.util.LinkedCaseInsensitiveMap; /** * Resolves a {@link BindMarkersFactory} from a {@link ConnectionFactory} using - * {@link BindMarkerFactoryProvider}. Dialect resolution uses Spring's - * {@link SpringFactoriesLoader spring.factories} to determine available extensions. + * a {@link BindMarkerFactoryProvider}. Dialect resolution uses Spring's + * {@link SpringFactoriesLoader spring.factories} file to determine available extensions. * * @author Mark Paluch * @since 5.3 @@ -45,8 +45,8 @@ public final class BindMarkersFactoryResolver { /** - * Retrieve a {@link BindMarkersFactory} by inspecting {@link ConnectionFactory} - * and its metadata. + * Retrieve a {@link BindMarkersFactory} by inspecting the supplied + * {@link ConnectionFactory} and its metadata. * @param connectionFactory the connection factory to inspect * @return the resolved {@link BindMarkersFactory} * @throws NoBindMarkersFactoryException if no {@link BindMarkersFactory} can be resolved @@ -69,18 +69,21 @@ public final class BindMarkersFactoryResolver { /** - * SPI to extend Spring's default R2DBC BindMarkersFactory discovery mechanism. - * Implementations of this interface are discovered through Spring's + * SPI to extend Spring's default R2DBC {@link BindMarkersFactory} discovery + * mechanism. + * + *

Implementations of this interface are discovered through Spring's * {@link SpringFactoriesLoader} mechanism. + * * @see SpringFactoriesLoader */ @FunctionalInterface public interface BindMarkerFactoryProvider { /** - * Return a {@link BindMarkersFactory} for a {@link ConnectionFactory}. - * @param connectionFactory the connection factory to be used with the {@link BindMarkersFactory} - * @return the {@link BindMarkersFactory} if the {@link BindMarkerFactoryProvider} + * Return a {@link BindMarkersFactory} for the given {@link ConnectionFactory}. + * @param connectionFactory the connection factory to be used with the {@code BindMarkersFactory} + * @return the {@code BindMarkersFactory} if this {@code BindMarkerFactoryProvider} * can provide a bind marker factory object, otherwise {@code null} */ @Nullable BindMarkersFactory getBindMarkers(ConnectionFactory connectionFactory); @@ -88,7 +91,7 @@ public final class BindMarkersFactoryResolver { /** - * Exception thrown when {@link BindMarkersFactoryResolver} cannot resolve a + * Exception thrown when a {@link BindMarkersFactoryResolver} cannot resolve a * {@link BindMarkersFactory}. */ @SuppressWarnings("serial") @@ -105,8 +108,11 @@ public final class BindMarkersFactoryResolver { /** - * Built-in bind maker factories. Used typically as last {@link BindMarkerFactoryProvider} - * when other providers register with a higher precedence. + * Built-in bind marker factories. + * + *

Typically used as the last {@link BindMarkerFactoryProvider} when other + * providers are registered with a higher precedence. + * * @see org.springframework.core.Ordered * @see org.springframework.core.annotation.AnnotationAwareOrderComparator */ diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/IndexedBindMarkers.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/IndexedBindMarkers.java index 5f14d937b4..23081c9778 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/IndexedBindMarkers.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/IndexedBindMarkers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ package org.springframework.r2dbc.core.binding; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; /** - * Index-based bind marker. This implementation creates indexed bind + * Index-based bind markers. This implementation creates indexed bind * markers using a numeric index and an optional prefix for bind markers * to be represented within the query string. * @@ -43,14 +43,15 @@ class IndexedBindMarkers implements BindMarkers { /** - * Create a new {@link IndexedBindMarker} instance given {@code prefix} and {@code beginWith}. - * @param prefix bind parameter prefix - * @param beginWith the first index to use + * Create a new {@link IndexedBindMarker} instance for the given {@code prefix} + * and {@code beginWith} value. + * @param prefix the bind parameter prefix + * @param beginIndex the first index to use */ - IndexedBindMarkers(String prefix, int beginWith) { + IndexedBindMarkers(String prefix, int beginIndex) { this.counter = 0; this.prefix = prefix; - this.offset = beginWith; + this.offset = beginIndex; } 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 b383b633b0..ab20840c7f 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 @@ -710,8 +710,7 @@ class R2dbcTransactionManagerTests { try { return Mono.fromRunnable(() -> doAfterCompletion(status)); } - catch (Throwable ex) { - // ignore + catch (Throwable ignored) { } return Mono.empty(); diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java index 19332ecd40..c992aa242f 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.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. @@ -23,6 +23,7 @@ import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.Parameters; import io.r2dbc.spi.Result; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -38,6 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Mark Paluch * @author Mingyuan Wu * @author Juergen Hoeller + * @author Sam Brannen */ abstract class AbstractDatabaseClientIntegrationTests { @@ -121,7 +123,8 @@ abstract class AbstractDatabaseClientIntegrationTests { DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); databaseClient.sql("INSERT INTO legoset (id, name, manual) VALUES(:id, :name, :manual)") - .bindValues(Map.of("id", 42055, + .bindValues(Map.of( + "id", 42055, "name", Parameters.in("SCHAUFELRADBAGGER"), "manual", Parameters.in(Integer.class))) .fetch().rowsUpdated() @@ -199,8 +202,7 @@ abstract class AbstractDatabaseClientIntegrationTests { void shouldEmitGeneratedKey() { DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); - databaseClient.sql( - "INSERT INTO legoset ( name, manual) VALUES(:name, :manual)") + databaseClient.sql("INSERT INTO legoset ( name, manual) VALUES(:name, :manual)") .bind("name","SCHAUFELRADBAGGER") .bindNull("manual", Integer.class) .filter(statement -> statement.returnGeneratedValues("id")) @@ -212,6 +214,129 @@ abstract class AbstractDatabaseClientIntegrationTests { } + @Nested + class ReusedNamedParameterTests { + + @Test // gh-34768 + void executeInsertWithReusedNamedParameter() { + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + Lego lego = new Lego(1, 42, "Star Wars", 42); + + // ":number" is reused. + databaseClient.sql("INSERT INTO legoset (id, version, name, manual) VALUES(:id, :number, :name, :number)") + .bind("id", lego.id) + .bind("name", lego.name) + .bind("number", lego.version) + .fetch().rowsUpdated() + .as(StepVerifier::create) + .expectNext(1L) + .verifyComplete(); + + databaseClient.sql("SELECT * FROM legoset") + .mapProperties(Lego.class) + .first() + .as(StepVerifier::create) + .assertNext(actual -> assertThat(actual).isEqualTo(lego)) + .verifyComplete(); + } + + @Test // gh-34768 + void executeSelectWithReusedNamedParameterList() { + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + String insertSql = "INSERT INTO legoset (id, version, name, manual) VALUES(:id, :version, :name, :manual)"; + // ":numbers" is reused. + String selectSql = "SELECT * FROM legoset WHERE version IN (:numbers) OR manual IN (:numbers)"; + Lego lego = new Lego(1, 42, "Star Wars", 99); + + databaseClient.sql(insertSql) + .bind("id", lego.id) + .bind("version", lego.version) + .bind("name", lego.name) + .bind("manual", lego.manual) + .fetch().rowsUpdated() + .as(StepVerifier::create) + .expectNext(1L) + .verifyComplete(); + + databaseClient.sql(selectSql) + // match version + .bind("numbers", List.of(2, 3, lego.version, 4)) + .mapProperties(Lego.class) + .first() + .as(StepVerifier::create) + .assertNext(actual -> assertThat(actual).isEqualTo(lego)) + .verifyComplete(); + + databaseClient.sql(selectSql) + // match manual + .bind("numbers", List.of(2, 3, lego.manual, 4)) + .mapProperties(Lego.class) + .first() + .as(StepVerifier::create) + .assertNext(actual -> assertThat(actual).isEqualTo(lego)) + .verifyComplete(); + } + + @Test // gh-34768 + void executeSelectWithReusedNamedParameterListFromBeanProperties() { + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + String insertSql = "INSERT INTO legoset (id, version, name, manual) VALUES(:id, :version, :name, :manual)"; + // ":numbers" is reused. + String selectSql = "SELECT * FROM legoset WHERE version IN (:numbers) OR manual IN (:numbers)"; + Lego lego = new Lego(1, 42, "Star Wars", 99); + + databaseClient.sql(insertSql) + .bind("id", lego.id) + .bind("version", lego.version) + .bind("name", lego.name) + .bind("manual", lego.manual) + .fetch().rowsUpdated() + .as(StepVerifier::create) + .expectNext(1L) + .verifyComplete(); + + databaseClient.sql(selectSql) + // match version + .bindProperties(new LegoRequest(List.of(lego.version))) + .mapProperties(Lego.class) + .first() + .as(StepVerifier::create) + .assertNext(actual -> assertThat(actual).isEqualTo(lego)) + .verifyComplete(); + + databaseClient.sql(selectSql) + // match manual + .bindProperties(new LegoRequest(List.of(lego.manual))) + .mapProperties(Lego.class) + .first() + .as(StepVerifier::create) + .assertNext(actual -> assertThat(actual).isEqualTo(lego)) + .verifyComplete(); + } + + + record Lego(int id, Integer version, String name, Integer manual) { + } + + static class LegoRequest { + + private final List numbers; + + LegoRequest(List numbers) { + this.numbers = numbers; + } + + public List getNumbers() { + return numbers; + } + } + + } + + record ParameterRecord(int id, String name, Integer manual) { } diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java index 6d23d48103..725bcf72a9 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.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,10 +16,8 @@ package org.springframework.r2dbc.core; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import io.r2dbc.spi.Parameters; @@ -29,8 +27,6 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.r2dbc.core.binding.BindMarkersFactory; import org.springframework.r2dbc.core.binding.BindTarget; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -42,10 +38,13 @@ import static org.mockito.Mockito.verify; * @author Mark Paluch * @author Jens Schauder * @author Anton Naydenov + * @author Sam Brannen */ class NamedParameterUtilsTests { - private final BindMarkersFactory BIND_MARKERS = BindMarkersFactory.indexed("$", 1); + private static final BindMarkersFactory INDEXED_MARKERS = BindMarkersFactory.indexed("$", 1); + + private static final BindMarkersFactory ANONYMOUS_MARKERS = BindMarkersFactory.anonymous("?"); @Test @@ -73,7 +72,7 @@ class NamedParameterUtilsTests { namedParams.addValue("a", "a").addValue("b", "b").addValue("c", "c"); PreparedOperation operation = NamedParameterUtils.substituteNamedParameters( - "xxx :a :b :c", BIND_MARKERS, namedParams); + "xxx :a :b :c", INDEXED_MARKERS, namedParams); assertThat(operation.toQuery()).isEqualTo("xxx $1 $2 $3"); @@ -87,11 +86,11 @@ class NamedParameterUtilsTests { void substituteObjectArray() { MapBindParameterSource namedParams = new MapBindParameterSource(new HashMap<>()); namedParams.addValue("a", - Arrays.asList(new Object[] {"Walter", "Heisenberg"}, - new Object[] {"Walt Jr.", "Flynn"})); + List.of(new Object[] {"Walter", "Heisenberg"}, + new Object[] {"Walt Jr.", "Flynn"})); PreparedOperation operation = NamedParameterUtils.substituteNamedParameters( - "xxx :a", BIND_MARKERS, namedParams); + "xxx :a", INDEXED_MARKERS, namedParams); assertThat(operation.toQuery()).isEqualTo("xxx ($1, $2), ($3, $4)"); } @@ -100,13 +99,13 @@ class NamedParameterUtilsTests { void shouldBindObjectArray() { MapBindParameterSource namedParams = new MapBindParameterSource(new HashMap<>()); namedParams.addValue("a", - Arrays.asList(new Object[] {"Walter", "Heisenberg"}, - new Object[] {"Walt Jr.", "Flynn"})); + List.of(new Object[] {"Walter", "Heisenberg"}, + new Object[] {"Walt Jr.", "Flynn"})); BindTarget bindTarget = mock(); PreparedOperation operation = NamedParameterUtils.substituteNamedParameters( - "xxx :a", BIND_MARKERS, namedParams); + "xxx :a", INDEXED_MARKERS, namedParams); operation.bindTo(bindTarget); verify(bindTarget).bind(0, "Walter"); @@ -141,7 +140,7 @@ class NamedParameterUtilsTests { ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); PreparedOperation operation = NamedParameterUtils.substituteNamedParameters( - parsedSql, BIND_MARKERS, new MapBindParameterSource()); + parsedSql, INDEXED_MARKERS, new MapBindParameterSource()); assertThat(operation.toQuery()).isEqualTo(expectedSql); } @@ -312,157 +311,139 @@ class NamedParameterUtilsTests { void multipleEqualParameterReferencesBindsValueOnce() { String sql = "SELECT * FROM person where name = :id or lastname = :id"; - BindMarkersFactory factory = BindMarkersFactory.indexed("$", 0); + MapBindParameterSource source = new MapBindParameterSource(Map.of("id", Parameters.in("foo"))); + PreparedOperation operation = NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS, source); - PreparedOperation operation = NamedParameterUtils.substituteNamedParameters( - sql, factory, new MapBindParameterSource( - Collections.singletonMap("id", Parameters.in("foo")))); + assertThat(operation.toQuery()) + .isEqualTo("SELECT * FROM person where name = $1 or lastname = $1"); - assertThat(operation.toQuery()).isEqualTo( - "SELECT * FROM person where name = $0 or lastname = $0"); + TrackingBindTarget trackingBindTarget = new TrackingBindTarget(); - operation.bindTo(new BindTarget() { - @Override - public void bind(String identifier, Object value) { - throw new UnsupportedOperationException(); - } - @Override - public void bind(int index, Object value) { - assertThat(index).isEqualTo(0); - assertThat(value).isEqualTo(Parameters.in("foo")); - } - @Override - public void bindNull(String identifier, Class type) { - throw new UnsupportedOperationException(); - } - @Override - public void bindNull(int index, Class type) { - throw new UnsupportedOperationException(); - } - }); + operation.bindTo(trackingBindTarget); + + assertThat(trackingBindTarget.bindings) + .hasSize(1) + .containsEntry(0, Parameters.in("foo")); } @Test - void multipleEqualCollectionParameterReferencesBindsValueOnce() { + void multipleEqualCollectionParameterReferencesForIndexedMarkersBindsValuesOnce() { String sql = "SELECT * FROM person where name IN (:ids) or lastname IN (:ids)"; - BindMarkersFactory factory = BindMarkersFactory.indexed("$", 0); + MapBindParameterSource source = new MapBindParameterSource(Map.of("ids", + Parameters.in(List.of("foo", "bar", "baz")))); + PreparedOperation operation = NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS, source); - MultiValueMap bindings = new LinkedMultiValueMap<>(); + assertThat(operation.toQuery()) + .isEqualTo("SELECT * FROM person where name IN ($1, $2, $3) or lastname IN ($1, $2, $3)"); - PreparedOperation operation = NamedParameterUtils.substituteNamedParameters( - sql, factory, new MapBindParameterSource(Collections.singletonMap("ids", - Parameters.in(Arrays.asList("foo", "bar", "baz"))))); + TrackingBindTarget trackingBindTarget = new TrackingBindTarget(); - assertThat(operation.toQuery()).isEqualTo( - "SELECT * FROM person where name IN ($0, $1, $2) or lastname IN ($0, $1, $2)"); + operation.bindTo(trackingBindTarget); - operation.bindTo(new BindTarget() { - @Override - public void bind(String identifier, Object value) { - throw new UnsupportedOperationException(); - } - @Override - public void bind(int index, Object value) { - assertThat(index).isIn(0, 1, 2); - assertThat(value).isIn("foo", "bar", "baz"); - bindings.add(index, value); - } - @Override - public void bindNull(String identifier, Class type) { - throw new UnsupportedOperationException(); - } - @Override - public void bindNull(int index, Class type) { - throw new UnsupportedOperationException(); - } - }); + assertThat(trackingBindTarget.bindings) + .hasSize(3) + .containsEntry(0, "foo") + .containsEntry(1, "bar") + .containsEntry(2, "baz"); + } - assertThat(bindings).containsEntry(0, Collections.singletonList("foo")) // - .containsEntry(1, Collections.singletonList("bar")) // - .containsEntry(2, Collections.singletonList("baz")); + @Test // gh-34768 + void multipleEqualCollectionParameterReferencesForAnonymousMarkersBindsValuesTwice() { + String sql = "SELECT * FROM fund_info WHERE fund_code IN (:fundCodes) OR fund_code IN (:fundCodes)"; + + MapBindParameterSource source = new MapBindParameterSource(Map.of("fundCodes", Parameters.in(List.of("foo", "bar", "baz")))); + PreparedOperation operation = NamedParameterUtils.substituteNamedParameters(sql, ANONYMOUS_MARKERS, source); + + assertThat(operation.toQuery()) + .isEqualTo("SELECT * FROM fund_info WHERE fund_code IN (?, ?, ?) OR fund_code IN (?, ?, ?)"); + + TrackingBindTarget trackingBindTarget = new TrackingBindTarget(); + + operation.bindTo(trackingBindTarget); + + assertThat(trackingBindTarget.bindings) + .hasSize(6) + .containsEntry(0, "foo") + .containsEntry(1, "bar") + .containsEntry(2, "baz") + .containsEntry(3, "foo") + .containsEntry(4, "bar") + .containsEntry(5, "baz"); } @Test - void multipleEqualParameterReferencesForAnonymousMarkersBindsValueMultipleTimes() { + void multipleEqualParameterReferencesForAnonymousMarkersBindsValueTwice() { String sql = "SELECT * FROM person where name = :id or lastname = :id"; - BindMarkersFactory factory = BindMarkersFactory.anonymous("?"); + MapBindParameterSource source = new MapBindParameterSource(Map.of("id", Parameters.in("foo"))); + PreparedOperation operation = NamedParameterUtils.substituteNamedParameters(sql, ANONYMOUS_MARKERS, source); - PreparedOperation operation = NamedParameterUtils.substituteNamedParameters( - sql, factory, new MapBindParameterSource( - Collections.singletonMap("id", Parameters.in("foo")))); + assertThat(operation.toQuery()) + .isEqualTo("SELECT * FROM person where name = ? or lastname = ?"); - assertThat(operation.toQuery()).isEqualTo( - "SELECT * FROM person where name = ? or lastname = ?"); + TrackingBindTarget trackingBindTarget = new TrackingBindTarget(); - Map bindValues = new LinkedHashMap<>(); + operation.bindTo(trackingBindTarget); - operation.bindTo(new BindTarget() { - @Override - public void bind(String identifier, Object value) { - throw new UnsupportedOperationException(); - } - @Override - public void bind(int index, Object value) { - bindValues.put(index, value); - } - @Override - public void bindNull(String identifier, Class type) { - throw new UnsupportedOperationException(); - } - @Override - public void bindNull(int index, Class type) { - throw new UnsupportedOperationException(); - } - }); - - assertThat(bindValues).hasSize(2).containsEntry(0, Parameters.in("foo")).containsEntry(1, Parameters.in("foo")); + assertThat(trackingBindTarget.bindings) + .hasSize(2) + .containsEntry(0, Parameters.in("foo")) + .containsEntry(1, Parameters.in("foo")); } @Test void multipleEqualParameterReferencesBindsNullOnce() { String sql = "SELECT * FROM person where name = :id or lastname = :id"; - BindMarkersFactory factory = BindMarkersFactory.indexed("$", 0); + MapBindParameterSource source = new MapBindParameterSource(Map.of("id", Parameters.in(String.class))); + PreparedOperation operation = NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS, source); - PreparedOperation operation = NamedParameterUtils.substituteNamedParameters( - sql, factory, new MapBindParameterSource( - Collections.singletonMap("id", Parameters.in(String.class)))); + assertThat(operation.toQuery()) + .isEqualTo("SELECT * FROM person where name = $1 or lastname = $1"); - assertThat(operation.toQuery()).isEqualTo( - "SELECT * FROM person where name = $0 or lastname = $0"); + TrackingBindTarget trackingBindTarget = new TrackingBindTarget(); - operation.bindTo(new BindTarget() { - @Override - public void bind(String identifier, Object value) { - throw new UnsupportedOperationException(); - } - @Override - public void bind(int index, Object value) { - assertThat(index).isEqualTo(0); - assertThat(value).isEqualTo(Parameters.in(String.class)); - } - @Override - public void bindNull(String identifier, Class type) { - throw new UnsupportedOperationException(); - } - @Override - public void bindNull(int index, Class type) { - throw new UnsupportedOperationException(); - } - }); + operation.bindTo(trackingBindTarget); + + assertThat(trackingBindTarget.bindings) + .hasSize(1) + .containsEntry(0, Parameters.in(String.class)); } - private String expand(ParsedSql sql) { - return NamedParameterUtils.substituteNamedParameters(sql, BIND_MARKERS, + private static String expand(ParsedSql sql) { + return NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS, new MapBindParameterSource()).toQuery(); } - private String expand(String sql) { - return NamedParameterUtils.substituteNamedParameters(sql, BIND_MARKERS, + private static String expand(String sql) { + return NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS, new MapBindParameterSource()).toQuery(); } + + private static class TrackingBindTarget implements BindTarget { + + final Map bindings = new HashMap<>(); + + @Override + public void bind(String identifier, Object value) {} + + @Override + public void bind(int index, Object value) { + this.bindings.put(index, value); + } + + @Override + public void bindNull(String identifier, Class type) { + throw new UnsupportedOperationException(); + } + + @Override + public void bindNull(int index, Class type) { + throw new UnsupportedOperationException(); + } + } + } diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index f703232a89..4742c72356 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -81,6 +81,7 @@ dependencies { testImplementation("org.hibernate.validator:hibernate-validator") testImplementation("org.hsqldb:hsqldb") testImplementation("org.junit.platform:junit-platform-testkit") + testImplementation("tools.jackson.core:jackson-databind") testRuntimeOnly("com.sun.xml.bind:jaxb-core") testRuntimeOnly("com.sun.xml.bind:jaxb-impl") testRuntimeOnly("org.glassfish:jakarta.el") @@ -104,14 +105,10 @@ test { description = "Runs JUnit 4, JUnit Jupiter, and TestNG tests." useJUnitPlatform { includeEngines "junit-vintage", "junit-jupiter", "testng" - excludeTags "failing-test-case" } - // We use `include` instead of `filter.includeTestsMatching`, since - // the latter results in some tests being executed/reported - // multiple times. - include(["**/*Tests.class", "**/*Test.class"]) + // `include` test filters and system properties are configured in + // org.springframework.build.TestConventions in buildSrc. filter.excludeTestsMatching("*TestCase") - systemProperty("testGroups", project.properties.get("testGroups")) - // Java Util Logging for the JUnit Platform. + // Optionally configure Java Util Logging for the JUnit Platform. // systemProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager") } diff --git a/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java b/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java index 88072db943..6af12761e2 100644 --- a/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java +++ b/spring-test/src/main/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. @@ -26,7 +26,7 @@ import org.springframework.core.env.ConfigurableEnvironment; * @author Chris Beams * @author Sam Brannen * @since 3.2 - * @see org.springframework.mock.env.MockPropertySource + * @see MockPropertySource */ public class MockEnvironment extends AbstractEnvironment { @@ -43,19 +43,23 @@ public class MockEnvironment extends AbstractEnvironment { /** * Set a property on the underlying {@link MockPropertySource} for this environment. + * @since 6.2.8 + * @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 + * @since 6.2.8 + * @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-test/src/main/java/org/springframework/mock/env/MockPropertySource.java b/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java index 3ef180fcf2..8b2c6e1d77 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 @@ import org.springframework.core.env.PropertySource; * 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 @@ import org.springframework.core.env.PropertySource;
  *
  * @author Chris Beams
  * @since 3.1
- * @see org.springframework.mock.env.MockEnvironment
+ * @see MockEnvironment
  */
 public class MockPropertySource extends PropertiesPropertySource {
 
@@ -95,7 +95,7 @@ public class MockPropertySource extends PropertiesPropertySource {
 
 	/**
 	 * 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/http/client/MockClientHttpRequest.java b/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpRequest.java index 42222f4493..b56b4ea9cb 100644 --- a/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpRequest.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. @@ -104,7 +104,7 @@ public class MockClientHttpRequest extends MockHttpOutputMessage implements Clie /** * Set the {@link ClientHttpResponse} to be used as the result of executing - * the this request. + * this request. * @see #execute() */ public void setResponse(ClientHttpResponse clientHttpResponse) { diff --git a/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpResponse.java b/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpResponse.java index 328068f111..a5f9e1aeac 100644 --- a/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpResponse.java +++ b/spring-test/src/main/java/org/springframework/mock/http/client/MockClientHttpResponse.java @@ -100,8 +100,7 @@ public class MockClientHttpResponse extends MockHttpInputMessage implements Clie try { getBody().close(); } - catch (IOException ex) { - // ignore + catch (IOException ignored) { } } diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java index 8f2500c89e..9203a90a1c 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.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. @@ -1036,8 +1036,7 @@ public class MockHttpServletRequest implements HttpServletRequest { * @see #getDateHeader */ public void addHeader(String name, Object value) { - if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name) && - !this.headers.containsKey(HttpHeaders.CONTENT_TYPE)) { + if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { setContentType(value.toString()); } else if (HttpHeaders.ACCEPT_LANGUAGE.equalsIgnoreCase(name) && @@ -1130,8 +1129,7 @@ public class MockHttpServletRequest implements HttpServletRequest { try { return simpleDateFormat.parse(value).getTime(); } - catch (ParseException ex) { - // ignore + catch (ParseException ignored) { } } throw new IllegalArgumentException("Cannot parse date value '" + value + "' for '" + name + "' header"); diff --git a/spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java b/spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java index 8be9bfb93f..ca8a064888 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java +++ b/spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java @@ -16,6 +16,8 @@ package org.springframework.mock.web.server; +import java.security.Principal; + import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; @@ -40,15 +42,19 @@ import org.springframework.web.server.session.WebSessionManager; */ public final class MockServerWebExchange extends DefaultServerWebExchange { + private final Mono principalMono; + private MockServerWebExchange( MockServerHttpRequest request, @Nullable WebSessionManager sessionManager, - @Nullable ApplicationContext applicationContext) { + @Nullable ApplicationContext applicationContext, @Nullable Principal principal) { super(request, new MockServerHttpResponse(), sessionManager != null ? sessionManager : new DefaultWebSessionManager(), ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver(), applicationContext); + + this.principalMono = (principal != null) ? Mono.just(principal) : Mono.empty(); } @@ -57,6 +63,16 @@ public final class MockServerWebExchange extends DefaultServerWebExchange { return (MockServerHttpResponse) super.getResponse(); } + /** + * Return the user set via {@link Builder#principal(Principal)}. + * @since 6.2.7 + */ + @SuppressWarnings("unchecked") + @Override + public Mono getPrincipal() { + return (Mono) this.principalMono; + } + /** * Create a {@link MockServerWebExchange} from the given mock request. @@ -107,8 +123,9 @@ public final class MockServerWebExchange extends DefaultServerWebExchange { private @Nullable WebSessionManager sessionManager; - @Nullable - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; + + private @Nullable Principal principal; public Builder(MockServerHttpRequest request) { this.request = request; @@ -146,11 +163,22 @@ public final class MockServerWebExchange extends DefaultServerWebExchange { return this; } + /** + * Provide a user to associate with the exchange. + * @param principal the principal to use + * @since 6.2.7 + */ + public Builder principal(@Nullable Principal principal) { + this.principal = principal; + return this; + } + /** * Build the {@code MockServerWebExchange} instance. */ public MockServerWebExchange build() { - return new MockServerWebExchange(this.request, this.sessionManager, this.applicationContext); + return new MockServerWebExchange( + this.request, this.sessionManager, this.applicationContext, this.principal); } } diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Commit.java b/spring-test/src/main/java/org/springframework/test/annotation/Commit.java index c4cb54cbfa..0f4dedf3ed 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/Commit.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/Commit.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. @@ -44,8 +44,7 @@ import java.lang.annotation.Target; * {@code @Commit} and {@code @Rollback} on the same test method or on the * same test class is unsupported and may lead to unpredictable results. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See + *

This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java b/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java index 6e602f5338..f1e509c729 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.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. @@ -76,8 +76,7 @@ import java.lang.annotation.Target; *

This annotation may be used as a meta-annotation to create custom * composed annotations. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See + *

This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/annotation/IfProfileValue.java b/spring-test/src/main/java/org/springframework/test/annotation/IfProfileValue.java index 9af1ec7492..96689bb0f2 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/IfProfileValue.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/IfProfileValue.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. @@ -94,11 +94,15 @@ import java.lang.annotation.Target; * @see org.springframework.test.context.junit4.statements.ProfileValueChecker * @see org.springframework.context.annotation.Profile * @see org.springframework.test.context.ActiveProfiles + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited +@Deprecated(since = "7.0") public @interface IfProfileValue { /** diff --git a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSource.java b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSource.java index 4121bcfeae..33551d5f44 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSource.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSource.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. @@ -40,7 +40,11 @@ import org.jspecify.annotations.Nullable; * @see ProfileValueSourceConfiguration * @see IfProfileValue * @see ProfileValueUtils + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ +@Deprecated(since = "7.0") public interface ProfileValueSource { /** diff --git a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java index 37af23cb3d..667f5f8dd1 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.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,11 +37,15 @@ import java.lang.annotation.Target; * @see ProfileValueSource * @see IfProfileValue * @see ProfileValueUtils + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited +@Deprecated(since = "7.0") public @interface ProfileValueSourceConfiguration { /** diff --git a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java index ed2e5e08ad..472f65259b 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.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. @@ -38,7 +38,9 @@ import org.springframework.util.StringUtils; * @see ProfileValueSource * @see ProfileValueSourceConfiguration * @see IfProfileValue + * @deprecated since Spring Framework 7.0 with no replacement */ +@Deprecated(since = "7.0") public abstract class ProfileValueUtils { private static final Log logger = LogFactory.getLog(ProfileValueUtils.class); diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java b/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java index dfa062ceb2..7ff46c35a7 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/Repeat.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. @@ -44,10 +44,14 @@ import java.lang.annotation.Target; * @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner * @see org.springframework.test.context.junit4.rules.SpringMethodRule * @see org.springframework.test.context.junit4.statements.SpringRepeat + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented +@Deprecated(since = "7.0") public @interface Repeat { /** diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java b/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java index 9d2cbcd8df..a0b28e10e3 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/Rollback.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,8 +48,7 @@ import java.lang.annotation.Target; * custom composed annotations. Consult the source code for * {@link Commit @Commit} for a concrete example. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See + *

This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java b/spring-test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java index 4a0f627664..b021904b6c 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.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. @@ -25,7 +25,11 @@ import org.springframework.util.Assert; * @author Rod Johnson * @author Sam Brannen * @since 2.0 + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ +@Deprecated(since = "7.0") public final class SystemProfileValueSource implements ProfileValueSource { private static final SystemProfileValueSource INSTANCE = new SystemProfileValueSource(); diff --git a/spring-test/src/main/java/org/springframework/test/annotation/TestAnnotationUtils.java b/spring-test/src/main/java/org/springframework/test/annotation/TestAnnotationUtils.java index e0b4ac927a..c61fb8e8cf 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/TestAnnotationUtils.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/TestAnnotationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,7 +25,10 @@ import org.springframework.core.annotation.AnnotatedElementUtils; * * @author Sam Brannen * @since 4.2 + * @see org.springframework.test.context.TestContextAnnotationUtils + * @deprecated since Spring Framework 7.0 with no replacement */ +@Deprecated(since = "7.0") public abstract class TestAnnotationUtils { /** diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Timed.java b/spring-test/src/main/java/org/springframework/test/annotation/Timed.java index 3e5ca273eb..b40597c076 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/Timed.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/Timed.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. @@ -43,10 +43,14 @@ import java.lang.annotation.Target; * @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner * @see org.springframework.test.context.junit4.rules.SpringMethodRule * @see org.springframework.test.context.junit4.statements.SpringFailOnTimeout + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented +@Deprecated(since = "7.0") public @interface Timed { /** diff --git a/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java b/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java index 97d5959319..5f9dc72cd2 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java +++ b/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.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. @@ -34,9 +34,8 @@ import org.springframework.core.annotation.AliasFor; *

This annotation may be used as a meta-annotation to create custom * composed annotations. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See - * {@link NestedTestConfiguration @NestedTestConfiguration} for details. + *

This annotation will be inherited from an enclosing test class by default. + * See {@link NestedTestConfiguration @NestedTestConfiguration} for details. * * @author Sam Brannen * @since 3.1 diff --git a/spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java b/spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java index 17b5c61ace..da351f7943 100644 --- a/spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java +++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java @@ -34,9 +34,8 @@ import java.lang.annotation.Target; * present on the current test class) will override any meta-present * declarations of {@code @BootstrapWith}. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See - * {@link NestedTestConfiguration @NestedTestConfiguration} for details. + *

This annotation will be inherited from an enclosing test class by default. + * See {@link NestedTestConfiguration @NestedTestConfiguration} for details. * * @author Sam Brannen * @since 4.1 diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java index 90bb738b77..e02360b914 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.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. @@ -75,9 +75,8 @@ import org.springframework.core.annotation.AliasFor; *

This annotation may be used as a meta-annotation to create custom * composed annotations. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See - * {@link NestedTestConfiguration @NestedTestConfiguration} for details. + *

This annotation will be inherited from an enclosing test class by default. + * See {@link NestedTestConfiguration @NestedTestConfiguration} for details. * * @author Sam Brannen * @since 2.5 @@ -292,13 +291,18 @@ public @interface ContextConfiguration { *

If not specified the name will be inferred based on the numerical level * within all declared contexts within the hierarchy. *

This attribute is only applicable when used within a test class hierarchy - * or enclosing class hierarchy that is configured using - * {@code @ContextHierarchy}, in which case the name can be used for - * merging or overriding this configuration with configuration - * of the same name in hierarchy levels defined in superclasses or enclosing - * classes. See the Javadoc for {@link ContextHierarchy @ContextHierarchy} for - * details. + * or enclosing class hierarchy that is configured using {@code @ContextHierarchy}, + * in which case the name can be used for merging or overriding + * this configuration with configuration of the same name in hierarchy levels + * defined in superclasses or enclosing classes. As of Spring Framework 6.2.6, + * the name can also be used to identify the configuration in which a + * Bean Override should be applied — for example, + * {@code @MockitoBean(contextName = "child")}. See the Javadoc for + * {@link ContextHierarchy @ContextHierarchy} for details. * @since 3.2.2 + * @see org.springframework.test.context.bean.override.mockito.MockitoBean#contextName @MockitoBean(contextName = ...) + * @see org.springframework.test.context.bean.override.mockito.MockitoSpyBean#contextName @MockitoSpyBean(contextName = ...) + * @see org.springframework.test.context.bean.override.convention.TestBean#contextName @TestBean(contextName = ...) */ String name() default ""; diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java index 0785c965f8..8bc139884b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.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,10 +29,12 @@ import java.lang.annotation.Target; * ApplicationContexts} for integration tests. * *

Examples

+ * *

The following JUnit-based examples demonstrate common configuration * scenarios for integration tests that require the use of context hierarchies. * *

Single Test Class with Context Hierarchy

+ * *

{@code ControllerIntegrationTests} represents a typical integration testing * scenario for a Spring MVC web application by declaring a context hierarchy * consisting of two levels, one for the root {@code WebApplicationContext} @@ -57,6 +59,7 @@ import java.lang.annotation.Target; * }

* *

Class Hierarchy with Implicit Parent Context

+ * *

The following test classes define a context hierarchy within a test class * hierarchy. {@code AbstractWebTests} declares the configuration for a root * {@code WebApplicationContext} in a Spring-powered web application. Note, @@ -83,12 +86,13 @@ import java.lang.annotation.Target; * public class RestWebServiceTests extends AbstractWebTests {} * *

Class Hierarchy with Merged Context Hierarchy Configuration

+ * *

The following classes demonstrate the use of named hierarchy levels * in order to merge the configuration for specific levels in a context - * hierarchy. {@code BaseTests} defines two levels in the hierarchy, {@code parent} - * and {@code child}. {@code ExtendedTests} extends {@code BaseTests} and instructs + * hierarchy. {@code BaseTests} defines two levels in the hierarchy, {@code "parent"} + * and {@code "child"}. {@code ExtendedTests} extends {@code BaseTests} and instructs * the Spring TestContext Framework to merge the context configuration for the - * {@code child} hierarchy level, simply by ensuring that the names declared via + * {@code "child"} hierarchy level, simply by ensuring that the names declared via * {@link ContextConfiguration#name} are both {@code "child"}. The result is that * three application contexts will be loaded: one for {@code "/app-config.xml"}, * one for {@code "/user-config.xml"}, and one for {"/user-config.xml", @@ -111,6 +115,7 @@ import java.lang.annotation.Target; * public class ExtendedTests extends BaseTests {} * *

Class Hierarchy with Overridden Context Hierarchy Configuration

+ * *

In contrast to the previous example, this example demonstrates how to * override the configuration for a given named level in a context hierarchy * by setting the {@link ContextConfiguration#inheritLocations} flag to {@code false}. @@ -131,12 +136,77 @@ import java.lang.annotation.Target; * ) * public class ExtendedTests extends BaseTests {} * + *

Context Hierarchies with Bean Overrides

+ * + *

When {@code @ContextHierarchy} is used in conjunction with bean overrides such as + * {@link org.springframework.test.context.bean.override.convention.TestBean @TestBean}, + * {@link org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean}, or + * {@link org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean}, + * it may be desirable or necessary to have the override applied to a single level + * in the context hierarchy. To achieve that, the bean override must specify a + * context name that matches a name configured via {@link ContextConfiguration#name}. + * + *

The following test class configures the name of the second hierarchy level to be + * {@code "user-config"} and simultaneously specifies that the {@code UserService} should + * be wrapped in a Mockito spy in the context named {@code "user-config"}. Consequently, + * Spring will only attempt to create the spy in the {@code "user-config"} context and will + * not attempt to create the spy in the parent context. + * + *

+ * @ExtendWith(SpringExtension.class)
+ * @ContextHierarchy({
+ *     @ContextConfiguration(classes = AppConfig.class),
+ *     @ContextConfiguration(classes = UserConfig.class, name = "user-config")
+ * })
+ * class IntegrationTests {
+ *
+ *     @MockitoSpyBean(contextName = "user-config")
+ *     UserService userService;
+ *
+ *     // ...
+ * }
+ * + *

When applying bean overrides in different levels of the context hierarchy, you may + * need to have all of the bean override instances injected into the test class in order + * to interact with them — for example, to configure stubbing for mocks. However, + * {@link org.springframework.beans.factory.annotation.Autowired @Autowired} will always + * inject a matching bean found in the lowest level of the context hierarchy. Thus, to + * inject bean override instances from specific levels in the context hierarchy, you need + * to annotate fields with appropriate bean override annotations and configure the name + * of the context level. + * + *

The following test class configures the names of the hierarchy levels to be + * {@code "parent"} and {@code "child"}. It also declares two {@code PropertyService} + * fields that are configured to create or replace {@code PropertyService} beans with + * Mockito mocks in the respective contexts, named {@code "parent"} and {@code "child"}. + * Consequently, the mock from the {@code "parent"} context will be injected into the + * {@code propertyServiceInParent} field, and the mock from the {@code "child"} context + * will be injected into the {@code propertyServiceInChild} field. + * + *

+ * @ExtendWith(SpringExtension.class)
+ * @ContextHierarchy({
+ *     @ContextConfiguration(classes = ParentConfig.class, name = "parent"),
+ *     @ContextConfiguration(classes = ChildConfig.class, name = "child")
+ * })
+ * class IntegrationTests {
+ *
+ *     @MockitoBean(contextName = "parent")
+ *     PropertyService propertyServiceInParent;
+ *
+ *     @MockitoBean(contextName = "child")
+ *     PropertyService propertyServiceInChild;
+ *
+ *     // ...
+ * }
+ * + *

Miscellaneous

+ * *

This annotation may be used as a meta-annotation to create custom * composed annotations. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See - * {@link NestedTestConfiguration @NestedTestConfiguration} for details. + *

This annotation will be inherited from an enclosing test class by default. + * See {@link NestedTestConfiguration @NestedTestConfiguration} for details. * * @author Sam Brannen * @since 3.2.2 diff --git a/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java index c5f88a7f1d..700826598a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java @@ -75,6 +75,7 @@ import org.jspecify.annotations.Nullable; *

    *
  • {@link BootstrapWith @BootstrapWith}
  • *
  • {@link TestExecutionListeners @TestExecutionListeners}
  • + *
  • {@link ContextCustomizerFactories @ContextCustomizerFactories}
  • *
  • {@link ContextConfiguration @ContextConfiguration}
  • *
  • {@link ContextHierarchy @ContextHierarchy}
  • *
  • {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration}
  • diff --git a/spring-test/src/main/java/org/springframework/test/context/TestConstructor.java b/spring-test/src/main/java/org/springframework/test/context/TestConstructor.java index 142c62e2ca..19c28a9fd8 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestConstructor.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestConstructor.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,9 +53,8 @@ import org.jspecify.annotations.Nullable; * {@link org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig @SpringJUnitWebConfig} * or various test-related annotations from Spring Boot Test. * - *

    As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See - * {@link NestedTestConfiguration @NestedTestConfiguration} for details. + *

    This annotation will be inherited from an enclosing test class by default. + * See {@link NestedTestConfiguration @NestedTestConfiguration} for details. * * @author Sam Brannen * @since 5.2 @@ -88,7 +87,7 @@ public @interface TestConstructor { *

    May alternatively be configured via the * {@link org.springframework.core.SpringProperties SpringProperties} * mechanism. - *

    As of Spring Framework 5.3, this property may also be configured as a + *

    This property may also be configured as a * JUnit * Platform configuration parameter. * @see #autowireMode diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java index a06ea72d1b..bc12aeadf6 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,8 @@ import org.springframework.core.annotation.AliasFor; * mechanism described in {@link TestExecutionListener}. * *

    This annotation may be used as a meta-annotation to create custom - * composed annotations. As of Spring Framework 5.3, this annotation will - * be inherited from an enclosing test class by default. See + * composed annotations. In addition, this annotation will be inherited + * from an enclosing test class by default. See * {@link NestedTestConfiguration @NestedTestConfiguration} for details. * *

    Switching to default {@code TestExecutionListener} implementations

    diff --git a/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java b/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java index f53079a8b7..5cfbf2f358 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.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. @@ -80,9 +80,8 @@ import org.springframework.core.io.support.PropertySourceFactory; * of both annotations can lead to ambiguity during the attribute resolution * process. Note, however, that ambiguity can be avoided via explicit annotation * attribute overrides using {@link AliasFor @AliasFor}. - *
  • As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See - * {@link NestedTestConfiguration @NestedTestConfiguration} for details.
  • + *
  • This annotation will be inherited from an enclosing test class by default. + * See {@link NestedTestConfiguration @NestedTestConfiguration} for details.
  • *
* * @author Sam Brannen diff --git a/spring-test/src/main/java/org/springframework/test/context/TestPropertySources.java b/spring-test/src/main/java/org/springframework/test/context/TestPropertySources.java index 8aee402866..4ff318d51d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestPropertySources.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestPropertySources.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -31,9 +31,8 @@ import java.lang.annotation.Target; * completely optional since {@code @TestPropertySource} is a * {@linkplain java.lang.annotation.Repeatable repeatable} annotation. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See - * {@link NestedTestConfiguration @NestedTestConfiguration} for details. + *

This annotation will be inherited from an enclosing test class by default. + * See {@link NestedTestConfiguration @NestedTestConfiguration} for details. * * @author Anatoliy Korovin * @author Sam Brannen diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java index 953aa1094f..490416d4b9 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java @@ -314,8 +314,7 @@ public class TestContextAotGenerator { ClassName processAheadOfTime(MergedContextConfiguration mergedConfig, GenerationContext generationContext) throws TestContextAotException { - GenericApplicationContext gac = loadContextForAotProcessing(mergedConfig); - try { + try (GenericApplicationContext gac = loadContextForAotProcessing(mergedConfig)) { return this.aotGenerator.processAheadOfTime(gac, generationContext); } catch (Throwable ex) { @@ -333,7 +332,7 @@ public class TestContextAotGenerator { * context or if one of the prerequisites is not met * @see AotContextLoader#loadContextForAotProcessing(MergedContextConfiguration, RuntimeHints) */ - private GenericApplicationContext loadContextForAotProcessing( + GenericApplicationContext loadContextForAotProcessing( MergedContextConfiguration mergedConfig) throws TestContextAotException { Class testClass = mergedConfig.getTestClass(); diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java index 014c2f7082..6446e6c226 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java @@ -153,6 +153,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, // an existing bean definition. if (beanFactory.containsBeanDefinition(beanName)) { existingBeanDefinition = beanFactory.getBeanDefinition(beanName); + setQualifiedElement(existingBeanDefinition, handler); } } else { @@ -167,6 +168,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, if (candidates.contains(beanName)) { // 3) We are overriding an existing bean by-name. existingBeanDefinition = beanFactory.getBeanDefinition(beanName); + setQualifiedElement(existingBeanDefinition, handler); } else if (requireExistingBean) { Field field = handler.getField(); @@ -448,10 +450,25 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, private static RootBeanDefinition createPseudoBeanDefinition(BeanOverrideHandler handler) { RootBeanDefinition definition = new RootBeanDefinition(handler.getBeanType().resolve()); definition.setTargetType(handler.getBeanType()); - definition.setQualifiedElement(handler.getField()); + setQualifiedElement(definition, handler); return definition; } + /** + * Set the {@linkplain RootBeanDefinition#setQualifiedElement(java.lang.reflect.AnnotatedElement) + * qualified element} in the supplied {@link BeanDefinition} to the + * {@linkplain BeanOverrideHandler#getField() field} of the supplied + * {@code BeanOverrideHandler}. + *

This is necessary for proper autowiring candidate resolution. + * @since 6.2.6 + */ + private static void setQualifiedElement(BeanDefinition beanDefinition, BeanOverrideHandler handler) { + Field field = handler.getField(); + if (field != null && beanDefinition instanceof RootBeanDefinition rbd) { + rbd.setQualifiedElement(field); + } + } + /** * Validate that the {@link BeanDefinition} for the supplied bean name is suitable * for being replaced by a bean override. diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java index 0820042209..3e2d24163b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java @@ -34,9 +34,6 @@ import org.springframework.test.context.MergedContextConfiguration; */ class BeanOverrideContextCustomizer implements ContextCustomizer { - static final String REGISTRY_BEAN_NAME = - "org.springframework.test.context.bean.override.internalBeanOverrideRegistry"; - private static final String INFRASTRUCTURE_BEAN_NAME = "org.springframework.test.context.bean.override.internalBeanOverridePostProcessor"; @@ -60,7 +57,7 @@ class BeanOverrideContextCustomizer implements ContextCustomizer { // AOT processing, since a bean definition cannot be generated for the // Set argument that it accepts in its constructor. BeanOverrideRegistry beanOverrideRegistry = new BeanOverrideRegistry(beanFactory); - beanFactory.registerSingleton(REGISTRY_BEAN_NAME, beanOverrideRegistry); + beanFactory.registerSingleton(BeanOverrideRegistry.BEAN_NAME, beanOverrideRegistry); beanFactory.registerSingleton(INFRASTRUCTURE_BEAN_NAME, new BeanOverrideBeanFactoryPostProcessor(this.handlers, beanOverrideRegistry)); beanFactory.registerSingleton(EARLY_INFRASTRUCTURE_BEAN_NAME, diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java index e239b5d816..bdb6dcca6c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java @@ -42,19 +42,25 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory { public @Nullable BeanOverrideContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { + // Base the context name on the "closest" @ContextConfiguration declaration + // within the type and enclosing class hierarchies of the test class. + String contextName = configAttributes.get(0).getName(); Set handlers = new LinkedHashSet<>(); - findBeanOverrideHandlers(testClass, handlers); + findBeanOverrideHandlers(testClass, contextName, handlers); if (handlers.isEmpty()) { return null; } return new BeanOverrideContextCustomizer(handlers); } - private void findBeanOverrideHandlers(Class testClass, Set handlers) { - BeanOverrideHandler.findAllHandlers(testClass).forEach(handler -> - Assert.state(handlers.add(handler), () -> - "Duplicate BeanOverrideHandler discovered in test class %s: %s" - .formatted(testClass.getName(), handler))); + private void findBeanOverrideHandlers(Class testClass, @Nullable String contextName, Set handlers) { + BeanOverrideHandler.findAllHandlers(testClass).stream() + // If a handler does not specify a context name, it always gets applied. + // Otherwise, the handler's context name must match the current context name. + .filter(handler -> handler.getContextName().isEmpty() || handler.getContextName().equals(contextName)) + .forEach(handler -> Assert.state(handlers.add(handler), + () -> "Duplicate BeanOverrideHandler discovered in test class %s: %s" + .formatted(testClass.getName(), handler))); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java index 9f4de84353..0d9c6a7622 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java @@ -86,17 +86,55 @@ public abstract class BeanOverrideHandler { private final @Nullable String beanName; + private final String contextName; + private final BeanOverrideStrategy strategy; + /** + * Construct a new {@code BeanOverrideHandler} from the supplied values. + *

To provide proper support for + * {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}, + * invoke {@link #BeanOverrideHandler(Field, ResolvableType, String, String, BeanOverrideStrategy)} + * instead. + * @param field the {@link Field} annotated with {@link BeanOverride @BeanOverride}, + * or {@code null} if {@code @BeanOverride} was declared at the type level + * @param beanType the {@linkplain ResolvableType type} of bean to override + * @param beanName the name of the bean to override, or {@code null} to look + * for a single matching bean by type + * @param strategy the {@link BeanOverrideStrategy} to use + * @deprecated As of Spring Framework 6.2.6, in favor of + * {@link #BeanOverrideHandler(Field, ResolvableType, String, String, BeanOverrideStrategy)} + */ + @Deprecated(since = "6.2.6", forRemoval = true) protected BeanOverrideHandler(@Nullable Field field, ResolvableType beanType, @Nullable String beanName, BeanOverrideStrategy strategy) { + this(field, beanType, beanName, "", strategy); + } + + /** + * Construct a new {@code BeanOverrideHandler} from the supplied values. + * @param field the {@link Field} annotated with {@link BeanOverride @BeanOverride}, + * or {@code null} if {@code @BeanOverride} was declared at the type level + * @param beanType the {@linkplain ResolvableType type} of bean to override + * @param beanName the name of the bean to override, or {@code null} to look + * for a single matching bean by type + * @param contextName the name of the context hierarchy level in which the + * handler should be applied, or an empty string to indicate that the handler + * should be applied to all application contexts within a context hierarchy + * @param strategy the {@link BeanOverrideStrategy} to use + * @since 6.2.6 + */ + protected BeanOverrideHandler(@Nullable Field field, ResolvableType beanType, @Nullable String beanName, + String contextName, BeanOverrideStrategy strategy) { + this.field = field; this.qualifierAnnotations = getQualifierAnnotations(field); this.beanType = beanType; this.beanName = beanName; this.strategy = strategy; + this.contextName = contextName; } /** @@ -145,30 +183,32 @@ public abstract class BeanOverrideHandler { * @param testClass the original test class * @param handlers the list of handlers found * @param localFieldsOnly whether to search only on local fields within the type hierarchy - * @param visitedEnclosingClasses the set of enclosing classes already visited + * @param visitedTypes the set of types already visited * @since 6.2.2 */ private static void findHandlers(Class clazz, Class testClass, List handlers, - boolean localFieldsOnly, Set> visitedEnclosingClasses) { + boolean localFieldsOnly, Set> visitedTypes) { + + // 0) Ensure that we do not process the same class or interface multiple times. + if (!visitedTypes.add(clazz)) { + return; + } // 1) Search enclosing class hierarchy. if (!localFieldsOnly && TestContextAnnotationUtils.searchEnclosingClass(clazz)) { - Class enclosingClass = clazz.getEnclosingClass(); - if (visitedEnclosingClasses.add(enclosingClass)) { - findHandlers(enclosingClass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses); - } + findHandlers(clazz.getEnclosingClass(), testClass, handlers, localFieldsOnly, visitedTypes); } // 2) Search class hierarchy. Class superclass = clazz.getSuperclass(); if (superclass != null && superclass != Object.class) { - findHandlers(superclass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses); + findHandlers(superclass, testClass, handlers, localFieldsOnly, visitedTypes); } if (!localFieldsOnly) { // 3) Search interfaces. for (Class ifc : clazz.getInterfaces()) { - findHandlers(ifc, testClass, handlers, localFieldsOnly, visitedEnclosingClasses); + findHandlers(ifc, testClass, handlers, localFieldsOnly, visitedTypes); } // 4) Process current class. @@ -214,7 +254,7 @@ public abstract class BeanOverrideHandler { /** - * Get the annotated {@link Field}. + * Get the {@link Field} annotated with {@link BeanOverride @BeanOverride}. */ public final @Nullable Field getField() { return this.field; @@ -235,6 +275,21 @@ public abstract class BeanOverrideHandler { return this.beanName; } + /** + * Get the name of the context hierarchy level in which this handler should + * be applied. + *

An empty string indicates that this handler should be applied to all + * application contexts. + *

If a context name is configured for this handler, it must match a name + * configured via {@code @ContextConfiguration(name=...)}. + * @since 6.2.6 + * @see org.springframework.test.context.ContextHierarchy @ContextHierarchy + * @see org.springframework.test.context.ContextConfiguration#name() + */ + public final String getContextName() { + return this.contextName; + } + /** * Get the {@link BeanOverrideStrategy} for this {@code BeanOverrideHandler}, * which influences how and when the bean override instance should be created. @@ -308,6 +363,7 @@ public abstract class BeanOverrideHandler { BeanOverrideHandler that = (BeanOverrideHandler) other; if (!Objects.equals(this.beanType.getType(), that.beanType.getType()) || !Objects.equals(this.beanName, that.beanName) || + !Objects.equals(this.contextName, that.contextName) || !Objects.equals(this.strategy, that.strategy)) { return false; } @@ -327,7 +383,7 @@ public abstract class BeanOverrideHandler { @Override public int hashCode() { - int hash = Objects.hash(getClass(), this.beanType.getType(), this.beanName, this.strategy); + int hash = Objects.hash(getClass(), this.beanType.getType(), this.beanName, this.contextName, this.strategy); return (this.beanName != null ? hash : hash + Objects.hash((this.field != null ? this.field.getName() : null), this.qualifierAnnotations)); } @@ -338,6 +394,7 @@ public abstract class BeanOverrideHandler { .append("field", this.field) .append("beanType", this.beanType) .append("beanName", this.beanName) + .append("contextName", this.contextName) .append("strategy", this.strategy) .toString(); } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java index 3afc7c885a..fe7d4c8480 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java @@ -16,7 +16,6 @@ package org.springframework.test.context.bean.override; -import java.lang.reflect.Field; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -24,17 +23,20 @@ import java.util.Map.Entry; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; /** * An internal class used to track {@link BeanOverrideHandler}-related state after - * the bean factory has been processed and to provide field injection utilities - * for test execution listeners. + * the bean factory has been processed and to provide lookup facilities to test + * execution listeners. + * + *

As of Spring Framework 6.2.6, {@code BeanOverrideRegistry} is hierarchical + * and has access to a potential parent in order to provide first-class support + * for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}. * * @author Simon Baslé * @author Sam Brannen @@ -42,6 +44,8 @@ import org.springframework.util.StringUtils; */ class BeanOverrideRegistry { + static final String BEAN_NAME = "org.springframework.test.context.bean.override.internalBeanOverrideRegistry"; + private static final Log logger = LogFactory.getLog(BeanOverrideRegistry.class); @@ -51,10 +55,16 @@ class BeanOverrideRegistry { private final ConfigurableBeanFactory beanFactory; + @Nullable + private final BeanOverrideRegistry parent; + BeanOverrideRegistry(ConfigurableBeanFactory beanFactory) { Assert.notNull(beanFactory, "ConfigurableBeanFactory must not be null"); this.beanFactory = beanFactory; + BeanFactory parentBeanFactory = beanFactory.getParentBeanFactory(); + this.parent = (parentBeanFactory != null && parentBeanFactory.containsBean(BEAN_NAME) ? + parentBeanFactory.getBean(BEAN_NAME, BeanOverrideRegistry.class) : null); } /** @@ -63,6 +73,7 @@ class BeanOverrideRegistry { *

Also associates a {@linkplain BeanOverrideStrategy#WRAP "wrapping"} handler * with the given {@code beanName}, allowing for subsequent wrapping of the * bean via {@link #wrapBeanIfNecessary(Object, String)}. + * @see #getBeanForHandler(BeanOverrideHandler, Class) */ void registerBeanOverrideHandler(BeanOverrideHandler handler, String beanName) { Assert.state(!this.handlerToBeanNameMap.containsKey(handler), () -> @@ -107,23 +118,24 @@ class BeanOverrideRegistry { return handler.createOverrideInstance(beanName, null, bean, this.beanFactory); } - void inject(Object target, BeanOverrideHandler handler) { - Field field = handler.getField(); - Assert.notNull(field, () -> "BeanOverrideHandler must have a non-null field: " + handler); + /** + * Get the bean instance that was created by the provided {@link BeanOverrideHandler}. + * @param handler the {@code BeanOverrideHandler} that created the bean + * @param requiredType the required bean type + * @return the bean instance, or {@code null} if the provided handler is not + * registered in this registry or a parent registry + * @since 6.2.6 + * @see #registerBeanOverrideHandler(BeanOverrideHandler, String) + */ + @Nullable Object getBeanForHandler(BeanOverrideHandler handler, Class requiredType) { String beanName = this.handlerToBeanNameMap.get(handler); - Assert.state(StringUtils.hasLength(beanName), () -> "No bean found for BeanOverrideHandler: " + handler); - inject(field, target, beanName); - } - - private void inject(Field field, Object target, String beanName) { - try { - Object bean = this.beanFactory.getBean(beanName, field.getType()); - ReflectionUtils.makeAccessible(field); - ReflectionUtils.setField(field, target, bean); + if (beanName != null) { + return this.beanFactory.getBean(beanName, requiredType); } - catch (Throwable ex) { - throw new BeanCreationException("Could not inject field '" + field + "'", ex); + if (this.parent != null) { + return this.parent.getBeanForHandler(handler, requiredType); } + return null; } } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java index 736223358c..4d934980df 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java @@ -16,11 +16,17 @@ package org.springframework.test.context.bean.override; +import java.lang.reflect.Field; import java.util.List; +import java.util.Objects; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.ApplicationContext; import org.springframework.test.context.TestContext; import org.springframework.test.context.support.AbstractTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; /** * {@code TestExecutionListener} that enables {@link BeanOverride @BeanOverride} @@ -90,13 +96,38 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList List handlers = BeanOverrideHandler.forTestClass(testContext.getTestClass()); if (!handlers.isEmpty()) { Object testInstance = testContext.getTestInstance(); - BeanOverrideRegistry beanOverrideRegistry = testContext.getApplicationContext() - .getBean(BeanOverrideContextCustomizer.REGISTRY_BEAN_NAME, BeanOverrideRegistry.class); + ApplicationContext applicationContext = testContext.getApplicationContext(); + + Assert.state(applicationContext.containsBean(BeanOverrideRegistry.BEAN_NAME), () -> """ + Test class %s declares @BeanOverride fields %s, but no BeanOverrideHandler has been registered. \ + If you are using @ContextHierarchy, ensure that context names for bean overrides match \ + configured @ContextConfiguration names.""".formatted(testContext.getTestClass().getSimpleName(), + handlers.stream().map(BeanOverrideHandler::getField).filter(Objects::nonNull) + .map(Field::getName).toList())); + BeanOverrideRegistry beanOverrideRegistry = applicationContext.getBean(BeanOverrideRegistry.BEAN_NAME, + BeanOverrideRegistry.class); for (BeanOverrideHandler handler : handlers) { - beanOverrideRegistry.inject(testInstance, handler); + Field field = handler.getField(); + Assert.state(field != null, () -> "BeanOverrideHandler must have a non-null field: " + handler); + Object bean = beanOverrideRegistry.getBeanForHandler(handler, field.getType()); + Assert.state(bean != null, () -> """ + No bean override instance found for BeanOverrideHandler %s. If you are using \ + @ContextHierarchy, ensure that context names for bean overrides match configured \ + @ContextConfiguration names.""".formatted(handler)); + injectField(field, testInstance, bean); } } } + private static void injectField(Field field, Object target, Object bean) { + try { + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, target, bean); + } + catch (Throwable ex) { + throw new BeanCreationException("Could not inject field '" + field + "'", ex); + } + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java index 9393a17ed0..837b975b33 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java @@ -99,6 +99,16 @@ import org.springframework.test.context.bean.override.BeanOverride; * } * } * + *

WARNING: Using {@code @TestBean} in conjunction with + * {@code @ContextHierarchy} can lead to undesirable results since each + * {@code @TestBean} will be applied to all context hierarchy levels by default. + * To ensure that a particular {@code @TestBean} is applied to a single context + * hierarchy level, set the {@link #contextName() contextName} to match a + * configured {@code @ContextConfiguration} + * {@link org.springframework.test.context.ContextConfiguration#name() name}. + * See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy} + * for further details and examples. + * *

NOTE: Only singleton beans can be overridden. * Any attempt to override a non-singleton bean will result in an exception. When * overriding a bean created by a {@link org.springframework.beans.factory.FactoryBean @@ -164,6 +174,19 @@ public @interface TestBean { */ String methodName() default ""; + /** + * The name of the context hierarchy level in which this {@code @TestBean} + * should be applied. + *

Defaults to an empty string which indicates that this {@code @TestBean} + * should be applied to all application contexts. + *

If a context name is configured, it must match a name configured via + * {@code @ContextConfiguration(name=...)}. + * @since 6.2.6 + * @see org.springframework.test.context.ContextHierarchy @ContextHierarchy + * @see org.springframework.test.context.ContextConfiguration#name() @ContextConfiguration(name=...) + */ + String contextName() default ""; + /** * Whether to require the existence of the bean being overridden. *

Defaults to {@code false} which means that a bean will be created if a diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandler.java index ead7bf9f6e..7156e7f636 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandler.java @@ -44,9 +44,9 @@ final class TestBeanOverrideHandler extends BeanOverrideHandler { TestBeanOverrideHandler(Field field, ResolvableType beanType, @Nullable String beanName, - BeanOverrideStrategy strategy, Method factoryMethod) { + String contextName, BeanOverrideStrategy strategy, Method factoryMethod) { - super(field, beanType, beanName, strategy); + super(field, beanType, beanName, contextName, strategy); this.factoryMethod = factoryMethod; } @@ -91,6 +91,7 @@ final class TestBeanOverrideHandler extends BeanOverrideHandler { .append("field", getField()) .append("beanType", getBeanType()) .append("beanName", getBeanName()) + .append("contextName", getContextName()) .append("strategy", getStrategy()) .append("factoryMethod", this.factoryMethod) .toString(); diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java index a47d491b84..601afcec09 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java @@ -82,7 +82,7 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor { } return new TestBeanOverrideHandler( - field, ResolvableType.forField(field, testClass), beanName, strategy, factoryMethod); + field, ResolvableType.forField(field, testClass), beanName, testBean.contextName(), strategy, factoryMethod); } /** diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java index ca01136829..e445c435c1 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java @@ -40,9 +40,10 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler { protected AbstractMockitoBeanOverrideHandler(@Nullable Field field, ResolvableType beanType, - @Nullable String beanName, BeanOverrideStrategy strategy, MockReset reset) { + @Nullable String beanName, String contextName, BeanOverrideStrategy strategy, + MockReset reset) { - super(field, beanType, beanName, strategy); + super(field, beanType, beanName, contextName, strategy); this.reset = (reset != null ? reset : MockReset.AFTER); } @@ -93,6 +94,7 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler { .append("field", getField()) .append("beanType", getBeanType()) .append("beanName", getBeanName()) + .append("contextName", getContextName()) .append("strategy", getStrategy()) .append("reset", getReset()) .toString(); diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java index 46d5c0917f..4c95a21518 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java @@ -74,6 +74,16 @@ import org.springframework.test.context.bean.override.BeanOverride; * registered directly}) will not be found, and a mocked bean will be added to * the context alongside the existing dependency. * + *

WARNING: Using {@code @MockitoBean} in conjunction with + * {@code @ContextHierarchy} can lead to undesirable results since each + * {@code @MockitoBean} will be applied to all context hierarchy levels by default. + * To ensure that a particular {@code @MockitoBean} is applied to a single context + * hierarchy level, set the {@link #contextName() contextName} to match a + * configured {@code @ContextConfiguration} + * {@link org.springframework.test.context.ContextConfiguration#name() name}. + * See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy} + * for further details and examples. + * *

NOTE: Only singleton beans can be mocked. * Any attempt to mock a non-singleton bean will result in an exception. When * mocking a bean created by a {@link org.springframework.beans.factory.FactoryBean @@ -144,6 +154,19 @@ public @interface MockitoBean { */ Class[] types() default {}; + /** + * The name of the context hierarchy level in which this {@code @MockitoBean} + * should be applied. + *

Defaults to an empty string which indicates that this {@code @MockitoBean} + * should be applied to all application contexts. + *

If a context name is configured, it must match a name configured via + * {@code @ContextConfiguration(name=...)}. + * @since 6.2.6 + * @see org.springframework.test.context.ContextHierarchy @ContextHierarchy + * @see org.springframework.test.context.ContextConfiguration#name() @ContextConfiguration(name=...) + */ + String contextName() default ""; + /** * Extra interfaces that should also be declared by the mock. *

Defaults to none. diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java index cd097a6a09..1a5cd191dd 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java @@ -63,15 +63,15 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler { MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, MockitoBean mockitoBean) { this(field, typeToMock, (!mockitoBean.name().isBlank() ? mockitoBean.name() : null), - (mockitoBean.enforceOverride() ? REPLACE : REPLACE_OR_CREATE), - mockitoBean.reset(), mockitoBean.extraInterfaces(), mockitoBean.answers(), mockitoBean.serializable()); + mockitoBean.contextName(), (mockitoBean.enforceOverride() ? REPLACE : REPLACE_OR_CREATE), + mockitoBean.reset(), mockitoBean.extraInterfaces(), mockitoBean.answers(), mockitoBean.serializable()); } private MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, @Nullable String beanName, - BeanOverrideStrategy strategy, MockReset reset, Class[] extraInterfaces, Answers answers, - boolean serializable) { + String contextName, BeanOverrideStrategy strategy, MockReset reset, Class[] extraInterfaces, + Answers answers, boolean serializable) { - super(field, typeToMock, beanName, strategy, reset); + super(field, typeToMock, beanName, contextName, strategy, reset); Assert.notNull(typeToMock, "'typeToMock' must not be null"); this.extraInterfaces = asClassSet(extraInterfaces); this.answers = answers; @@ -160,6 +160,7 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler { .append("field", getField()) .append("beanType", getBeanType()) .append("beanName", getBeanName()) + .append("contextName", getContextName()) .append("strategy", getStrategy()) .append("reset", getReset()) .append("extraInterfaces", getExtraInterfaces()) diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java index e42c0b4563..aa2d8cbb59 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java @@ -67,6 +67,16 @@ import org.springframework.test.context.bean.override.BeanOverride; * {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object) * registered directly} as resolvable dependencies. * + *

WARNING: Using {@code @MockitoSpyBean} in conjunction with + * {@code @ContextHierarchy} can lead to undesirable results since each + * {@code @MockitoSpyBean} will be applied to all context hierarchy levels by default. + * To ensure that a particular {@code @MockitoSpyBean} is applied to a single context + * hierarchy level, set the {@link #contextName() contextName} to match a + * configured {@code @ContextConfiguration} + * {@link org.springframework.test.context.ContextConfiguration#name() name}. + * See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy} + * for further details and examples. + * *

NOTE: Only singleton beans can be spied. Any attempt * to create a spy for a non-singleton bean will result in an exception. When * creating a spy for a {@link org.springframework.beans.factory.FactoryBean FactoryBean}, @@ -136,6 +146,19 @@ public @interface MockitoSpyBean { */ Class[] types() default {}; + /** + * The name of the context hierarchy level in which this {@code @MockitoSpyBean} + * should be applied. + *

Defaults to an empty string which indicates that this {@code @MockitoSpyBean} + * should be applied to all application contexts. + *

If a context name is configured, it must match a name configured via + * {@code @ContextConfiguration(name=...)}. + * @since 6.2.6 + * @see org.springframework.test.context.ContextHierarchy @ContextHierarchy + * @see org.springframework.test.context.ContextConfiguration#name() @ContextConfiguration(name=...) + */ + String contextName() default ""; + /** * The reset mode to apply to the spied bean. *

The default is {@link MockReset#AFTER} meaning that spies are automatically diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java index e2ab4f46ce..d04712e0cb 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java @@ -54,7 +54,7 @@ class MockitoSpyBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler { MockitoSpyBeanOverrideHandler(@Nullable Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) { super(field, typeToSpy, (StringUtils.hasText(spyBean.name()) ? spyBean.name() : null), - BeanOverrideStrategy.WRAP, spyBean.reset()); + spyBean.contextName(), BeanOverrideStrategy.WRAP, spyBean.reset()); Assert.notNull(typeToSpy, "typeToSpy must not be null"); } diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.java index c42cb28bfc..5446fef59d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.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. @@ -54,8 +54,7 @@ import java.lang.annotation.Target; * {@code ""}, {}, or {@code DEFAULT}. Explicit local configuration * therefore overrides global configuration. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See + *

This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlGroup.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlGroup.java index 75f8aa36c5..1b3f8f19fb 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlGroup.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlGroup.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. @@ -34,8 +34,7 @@ import java.lang.annotation.Target; *

This annotation may be used as a meta-annotation to create custom * composed annotations. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See + *

This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlMergeMode.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlMergeMode.java index 6479a85524..ad997a4cc9 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlMergeMode.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlMergeMode.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. @@ -37,8 +37,7 @@ import java.lang.annotation.Target; *

This annotation may be used as a meta-annotation to create custom * composed annotations with attribute overrides. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See + *

This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIf.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIf.java index 02223b6566..85adb729c0 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIf.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIf.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. @@ -55,7 +55,7 @@ import org.springframework.core.annotation.AliasFor; * {@link org.junit.jupiter.api.condition.DisabledOnOs @DisabledOnOs(MAC)} support * in JUnit Jupiter. * - *

Since JUnit 5.7, JUnit Jupiter also has a condition annotation named + *

JUnit Jupiter also has a condition annotation named * {@link org.junit.jupiter.api.condition.DisabledIf @DisabledIf}. Thus, if you * wish to use Spring's {@code @DisabledIf} support make sure you import the * annotation type from the correct package. diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIfCondition.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIfCondition.java index 6c4908eb8b..3549c3e63d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIfCondition.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIfCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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,7 +22,7 @@ import org.junit.jupiter.api.extension.ExtensionContext; /** * {@code DisabledIfCondition} is an {@link org.junit.jupiter.api.extension.ExecutionCondition} * that supports the {@link DisabledIf @DisabledIf} annotation when using the Spring - * TestContext Framework in conjunction with JUnit 5's Jupiter programming model. + * TestContext Framework in conjunction with the JUnit Jupiter testing framework. * *

Any attempt to use the {@code DisabledIfCondition} without the presence of * {@link DisabledIf @DisabledIf} will result in an enabled diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/EnabledIf.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/EnabledIf.java index a80f2cef80..47c8364ee5 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/EnabledIf.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/EnabledIf.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. @@ -55,7 +55,7 @@ import org.springframework.core.annotation.AliasFor; * {@link org.junit.jupiter.api.condition.EnabledOnOs @EnabledOnOs(MAC)} support * in JUnit Jupiter. * - *

Since JUnit 5.7, JUnit Jupiter also has a condition annotation named + *

JUnit Jupiter also has a condition annotation named * {@link org.junit.jupiter.api.condition.EnabledIf @EnabledIf}. Thus, if you * wish to use Spring's {@code @EnabledIf} support make sure you import the * annotation type from the correct package. diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/EnabledIfCondition.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/EnabledIfCondition.java index f637c05c52..5b7fe8257c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/EnabledIfCondition.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/EnabledIfCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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,7 +22,7 @@ import org.junit.jupiter.api.extension.ExtensionContext; /** * {@code EnabledIfCondition} is an {@link org.junit.jupiter.api.extension.ExecutionCondition} * that supports the {@link EnabledIf @EnabledIf} annotation when using the Spring - * TestContext Framework in conjunction with JUnit 5's Jupiter programming model. + * TestContext Framework in conjunction with the JUnit Jupiter testing framework. * *

Any attempt to use the {@code EnabledIfCondition} without the presence of * {@link EnabledIf @EnabledIf} will result in an enabled diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java index 7810d5616d..820725d7e4 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.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. @@ -65,7 +65,7 @@ import org.springframework.util.ReflectionUtils.MethodFilter; /** * {@code SpringExtension} integrates the Spring TestContext Framework - * into JUnit 5's Jupiter programming model. + * into the JUnit Jupiter testing framework. * *

To use this extension, simply annotate a JUnit Jupiter based test class with * {@code @ExtendWith(SpringExtension.class)}, {@code @SpringJUnitConfig}, or @@ -147,9 +147,8 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes /** * Delegates to {@link TestContextManager#prepareTestInstance}. - *

As of Spring Framework 5.3.2, this method also validates that test - * methods and test lifecycle methods are not annotated with - * {@link Autowired @Autowired}. + *

This method also validates that test methods and test lifecycle methods + * are not annotated with {@link Autowired @Autowired}. */ @Override public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringJUnitConfig.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringJUnitConfig.java index f493ad5b2c..8d311e438f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringJUnitConfig.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringJUnitConfig.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. @@ -36,8 +36,7 @@ import org.springframework.test.context.ContextLoader; * {@link ContextConfiguration @ContextConfiguration} from the Spring TestContext * Framework. * - *

As of Spring Framework 5.3, this annotation will effectively be inherited - * from an enclosing test class by default. See + *

This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/package-info.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/package-info.java index 44c237362a..4345689613 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/package-info.java @@ -1,6 +1,6 @@ /** * Core support for integrating the Spring TestContext Framework - * with the JUnit Jupiter extension model in JUnit 5. + * with the JUnit Jupiter testing framework. */ @NullMarked package org.springframework.test.context.junit.jupiter; diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/web/SpringJUnitWebConfig.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/web/SpringJUnitWebConfig.java index 595695ee89..b42dbd30ed 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/web/SpringJUnitWebConfig.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/web/SpringJUnitWebConfig.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. @@ -39,8 +39,7 @@ import org.springframework.test.context.web.WebAppConfiguration; * {@link WebAppConfiguration @WebAppConfiguration} from the Spring TestContext * Framework. * - *

As of Spring Framework 5.3, this annotation will effectively be inherited - * from an enclosing test class by default. See + *

This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/web/package-info.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/web/package-info.java index 5fd141c9a5..159f3b5e39 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/web/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/web/package-info.java @@ -1,6 +1,6 @@ /** * Web support for integrating the Spring TestContext Framework - * with the JUnit Jupiter extension model in JUnit 5. + * with the JUnit Jupiter extension model. */ @NullMarked package org.springframework.test.context.junit.jupiter.web; diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java index 05e1861d01..a873d57e33 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.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. @@ -62,8 +62,12 @@ import org.springframework.test.context.TestExecutionListeners; * @see TestExecutionListeners * @see AbstractTransactionalJUnit4SpringContextTests * @see org.springframework.test.context.testng.AbstractTestNGSpringContextTests + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ @RunWith(SpringRunner.class) +@Deprecated(since = "7.0") public abstract class AbstractJUnit4SpringContextTests implements ApplicationContextAware { /** diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java index c8970a9151..6d1e5a688d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.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. @@ -78,8 +78,12 @@ import org.springframework.util.Assert; * @see org.springframework.test.context.transaction.AfterTransaction * @see org.springframework.test.jdbc.JdbcTestUtils * @see org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ @Transactional +@Deprecated(since = "7.0") public abstract class AbstractTransactionalJUnit4SpringContextTests extends AbstractJUnit4SpringContextTests { /** diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java index e98b57e665..c8bb57dd6d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.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. @@ -93,7 +93,11 @@ import org.springframework.util.ReflectionUtils; * @see AbstractTransactionalJUnit4SpringContextTests * @see org.springframework.test.context.junit4.rules.SpringClassRule * @see org.springframework.test.context.junit4.rules.SpringMethodRule + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ +@Deprecated(since = "7.0") public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner { private static final Log logger = LogFactory.getLog(SpringJUnit4ClassRunner.class); diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/SpringRunner.java b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringRunner.java index dd6ca659bc..97bfd42b4a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/SpringRunner.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringRunner.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. @@ -35,7 +35,11 @@ import org.junit.runners.model.InitializationError; * @see SpringJUnit4ClassRunner * @see org.springframework.test.context.junit4.rules.SpringClassRule * @see org.springframework.test.context.junit4.rules.SpringMethodRule + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ +@Deprecated(since = "7.0") public final class SpringRunner extends SpringJUnit4ClassRunner { /** diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringClassRule.java b/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringClassRule.java index f2c5f31dce..d0da5972df 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringClassRule.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringClassRule.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. @@ -81,7 +81,11 @@ import org.springframework.util.Assert; * @see SpringMethodRule * @see org.springframework.test.context.TestContextManager * @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ +@Deprecated(since = "7.0") public class SpringClassRule implements TestRule { private static final Log logger = LogFactory.getLog(SpringClassRule.class); diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.java b/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.java index bb205c7cd8..2ebacc0bc4 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.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. @@ -95,7 +95,11 @@ import org.springframework.test.context.junit4.statements.SpringRepeat; * @see SpringClassRule * @see org.springframework.test.context.TestContextManager * @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ +@Deprecated(since = "7.0") public class SpringMethodRule implements MethodRule { private static final Log logger = LogFactory.getLog(SpringMethodRule.class); diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/ProfileValueChecker.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/ProfileValueChecker.java index 293dcd0eef..7da5c29e8d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/ProfileValueChecker.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/ProfileValueChecker.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. @@ -39,7 +39,11 @@ import org.springframework.util.Assert; * @see #evaluate() * @see IfProfileValue * @see ProfileValueUtils + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ +@Deprecated(since = "7.0") public class ProfileValueChecker extends Statement { private final Statement next; diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestClassCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestClassCallbacks.java index 467b2f7081..c396152190 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestClassCallbacks.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestClassCallbacks.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,11 @@ import org.springframework.test.context.TestContextManager; * @since 3.0 * @see #evaluate() * @see RunBeforeTestClassCallbacks + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ +@Deprecated(since = "7.0") public class RunAfterTestClassCallbacks extends Statement { private final Statement next; diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestExecutionCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestExecutionCallbacks.java index f166cd26a0..8e4b67961d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestExecutionCallbacks.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestExecutionCallbacks.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. @@ -37,7 +37,11 @@ import org.springframework.test.context.TestContextManager; * @since 5.0 * @see #evaluate() * @see RunBeforeTestExecutionCallbacks + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ +@Deprecated(since = "7.0") public class RunAfterTestExecutionCallbacks extends Statement { private final Statement next; diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestMethodCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestMethodCallbacks.java index db8b747b97..687c027d97 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestMethodCallbacks.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestMethodCallbacks.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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,7 +37,11 @@ import org.springframework.test.context.TestContextManager; * @since 3.0 * @see #evaluate() * @see RunBeforeTestMethodCallbacks + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ +@Deprecated(since = "7.0") public class RunAfterTestMethodCallbacks extends Statement { private final Statement next; diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestClassCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestClassCallbacks.java index afadf43a88..1979d2d9c9 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestClassCallbacks.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestClassCallbacks.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 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. @@ -30,7 +30,11 @@ import org.springframework.test.context.TestContextManager; * @since 3.0 * @see #evaluate() * @see RunAfterTestMethodCallbacks + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ +@Deprecated(since = "7.0") public class RunBeforeTestClassCallbacks extends Statement { private final Statement next; diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestExecutionCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestExecutionCallbacks.java index f95eb5a382..3f260a92e7 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestExecutionCallbacks.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestExecutionCallbacks.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. @@ -34,7 +34,11 @@ import org.springframework.test.context.TestContextManager; * @since 5.0 * @see #evaluate() * @see RunAfterTestExecutionCallbacks + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ +@Deprecated(since = "7.0") public class RunBeforeTestExecutionCallbacks extends Statement { private final Statement next; diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestMethodCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestMethodCallbacks.java index 7e5c882a25..08bd8f512e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestMethodCallbacks.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestMethodCallbacks.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 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. @@ -32,7 +32,11 @@ import org.springframework.test.context.TestContextManager; * @since 3.0 * @see #evaluate() * @see RunAfterTestMethodCallbacks + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ +@Deprecated(since = "7.0") public class RunBeforeTestMethodCallbacks extends Statement { private final Statement next; diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunPrepareTestInstanceCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunPrepareTestInstanceCallbacks.java index 12baf0ea77..d041ccfde9 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunPrepareTestInstanceCallbacks.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunPrepareTestInstanceCallbacks.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. @@ -29,7 +29,11 @@ import org.springframework.test.context.TestContextManager; * @author Sam Brannen * @since 4.2 * @see #evaluate() + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ +@Deprecated(since = "7.0") public class RunPrepareTestInstanceCallbacks extends Statement { private final Statement next; diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringFailOnTimeout.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringFailOnTimeout.java index cb4e1dc6d9..88d70034c2 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringFailOnTimeout.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringFailOnTimeout.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. @@ -38,7 +38,11 @@ import org.springframework.util.Assert; * @author Sam Brannen * @since 3.0 * @see #evaluate() + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ +@Deprecated(since = "7.0") public class SpringFailOnTimeout extends Statement { private final Statement next; diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringRepeat.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringRepeat.java index b7277f364d..9adce86462 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringRepeat.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/SpringRepeat.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. @@ -32,7 +32,11 @@ import org.springframework.test.annotation.TestAnnotationUtils; * @author Sam Brannen * @since 3.0 * @see #evaluate() + * @deprecated since Spring Framework 7.0 in favor of the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * and JUnit Jupiter */ +@Deprecated(since = "7.0") public class SpringRepeat extends Statement { protected static final Log logger = LogFactory.getLog(SpringRepeat.class); diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java index 52cc39e6b7..5478fbc7b2 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.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. @@ -41,8 +41,7 @@ import java.lang.annotation.Target; *

This annotation may be used as a meta-annotation to create custom * composed annotations. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See + *

This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java b/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java index 3f1b8ae3ac..201c9b70d7 100644 --- a/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java +++ b/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.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. @@ -28,9 +28,8 @@ import org.springframework.util.Assert; * Simple utility for finding available TCP ports on {@code localhost} for use in * integration testing scenarios. * - *

This is a limited form of {@code org.springframework.util.SocketUtils}, which - * has been deprecated since Spring Framework 5.3.16 and removed in Spring - * Framework 6.0. + *

This is a limited form of the original {@code org.springframework.util.SocketUtils} + * class which was removed in Spring Framework 6.0. * *

{@code TestSocketUtils} can be used in integration tests which start an * external server on an available random port. However, these utilities make no diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java index 8a72d2b99d..f28300c382 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java @@ -19,6 +19,7 @@ package org.springframework.test.web.client.match; import java.net.URI; import java.util.List; import java.util.Map; +import java.util.Set; import javax.xml.xpath.XPathExpressionException; @@ -130,6 +131,7 @@ public abstract class MockRestRequestMatchers { * @since 5.3.27 * @see #queryParam(String, Matcher...) * @see #queryParam(String, String...) + * @see #queryParamCount(int) */ public static RequestMatcher queryParamList(String name, Matcher> matcher) { return request -> { @@ -158,6 +160,7 @@ public abstract class MockRestRequestMatchers { * parameter value * @see #queryParamList(String, Matcher) * @see #queryParam(String, String...) + * @see #queryParamCount(int) */ @SafeVarargs @SuppressWarnings("NullAway") // Dataflow analysis limitation @@ -187,6 +190,7 @@ public abstract class MockRestRequestMatchers { * parameter value * @see #queryParamList(String, Matcher) * @see #queryParam(String, Matcher...) + * @see #queryParamCount(int) */ @SuppressWarnings("NullAway") // Dataflow analysis limitation public static RequestMatcher queryParam(String name, String... expectedValues) { @@ -199,6 +203,25 @@ public abstract class MockRestRequestMatchers { }; } + /** + * Assert the number of query parameters present in the request. + * @param expectedCount the number of expected query parameters + * @since 7.0 + * @see #queryParamList(String, Matcher) + * @see #queryParam(String, Matcher...) + * @see #queryParam(String, String...) + */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public static RequestMatcher queryParamCount(int expectedCount) { + return request -> { + Set parameterNames = getQueryParams(request).keySet(); + int actualCount = parameterNames.size(); + if (expectedCount != actualCount) { + fail("Expected %d query parameter(s) but found %d: %s".formatted(expectedCount, actualCount, parameterNames)); + } + }; + } + private static MultiValueMap getQueryParams(ClientHttpRequest request) { return UriComponentsBuilder.fromUri(request.getURI()).build().getQueryParams(); } @@ -359,7 +382,6 @@ public abstract class MockRestRequestMatchers { private static void assertValueCount(String name, MultiValueMap map, int count) { - List values = map.get(name); String message = "Expected query param <" + name + ">"; if (values == null) { @@ -371,7 +393,6 @@ public abstract class MockRestRequestMatchers { } private static void assertValueCount(String name, HttpHeaders headers, int count) { - List values = headers.get(name); String message = "Expected header <" + name + ">"; if (values == null) { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultControllerSpec.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultControllerSpec.java index f01f772742..32b938cb5d 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultControllerSpec.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultControllerSpec.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. @@ -31,6 +31,7 @@ import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.validation.Validator; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; +import org.springframework.web.reactive.config.ApiVersionConfigurer; import org.springframework.web.reactive.config.BlockingExecutionConfigurer; import org.springframework.web.reactive.config.CorsRegistry; import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration; @@ -118,6 +119,12 @@ class DefaultControllerSpec extends AbstractMockServerSpec configurer) { + this.configurer.versionConsumer = configurer; + return this; + } + @Override public DefaultControllerSpec viewResolvers(Consumer consumer) { this.configurer.viewResolversConsumer = consumer; @@ -168,6 +175,8 @@ class DefaultControllerSpec extends AbstractMockServerSpec versionConsumer; + private @Nullable Consumer viewResolversConsumer; private @Nullable Consumer executionConsumer; @@ -219,6 +228,13 @@ class DefaultControllerSpec extends AbstractMockServerSpec defaultCookies; + private final @Nullable Object defaultApiVersion; + + private final @Nullable ApiVersionInserter apiVersionInserter; + private final Consumer> entityResultConsumer; private final Duration responseTimeout; @@ -97,9 +102,11 @@ class DefaultWebTestClient implements WebTestClient { private final AtomicLong requestIndex = new AtomicLong(); - DefaultWebTestClient(ClientHttpConnector connector, ExchangeStrategies exchangeStrategies, + DefaultWebTestClient( + ClientHttpConnector connector, ExchangeStrategies exchangeStrategies, Function exchangeFactory, UriBuilderFactory uriBuilderFactory, @Nullable HttpHeaders headers, @Nullable MultiValueMap cookies, + @Nullable Object defaultApiVersion, @Nullable ApiVersionInserter apiVersionInserter, Consumer> entityResultConsumer, @Nullable Duration responseTimeout, DefaultWebTestClientBuilder clientBuilder) { @@ -110,6 +117,8 @@ class DefaultWebTestClient implements WebTestClient { this.uriBuilderFactory = uriBuilderFactory; this.defaultHeaders = headers; this.defaultCookies = cookies; + this.defaultApiVersion = defaultApiVersion; + this.apiVersionInserter = apiVersionInserter; this.entityResultConsumer = entityResultConsumer; this.responseTimeout = (responseTimeout != null ? responseTimeout : Duration.ofSeconds(5)); this.builder = clientBuilder; @@ -186,6 +195,8 @@ class DefaultWebTestClient implements WebTestClient { private @Nullable MultiValueMap cookies; + private @Nullable Object apiVersion; + private @Nullable BodyInserter inserter; private final Map attributes = new LinkedHashMap<>(4); @@ -310,6 +321,12 @@ class DefaultWebTestClient implements WebTestClient { return this; } + @Override + public RequestBodySpec apiVersion(Object version) { + this.apiVersion = version; + return this; + } + @Override public RequestHeadersSpec bodyValue(Object body) { this.inserter = BodyInserters.fromValue(body); @@ -373,6 +390,11 @@ class DefaultWebTestClient implements WebTestClient { if (!this.headers.isEmpty()) { headersToUse.putAll(this.headers); } + Object version = getApiVersionOrDefault(); + if (version != null) { + Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured"); + apiVersionInserter.insertVersion(version, headersToUse); + } }) .cookies(cookiesToUse -> { if (!CollectionUtils.isEmpty(DefaultWebTestClient.this.defaultCookies)) { @@ -386,7 +408,17 @@ class DefaultWebTestClient implements WebTestClient { } private URI initUri() { - return (this.uri != null ? this.uri : DefaultWebTestClient.this.uriBuilderFactory.expand("")); + URI uriToUse = this.uri != null ? this.uri : DefaultWebTestClient.this.uriBuilderFactory.expand(""); + Object version = getApiVersionOrDefault(); + if (version != null) { + Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured"); + uriToUse = apiVersionInserter.insertVersion(version, uriToUse); + } + return uriToUse; + } + + private @Nullable Object getApiVersionOrDefault() { + return (this.apiVersion != null ? this.apiVersion : DefaultWebTestClient.this.defaultApiVersion); } } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java index daab08e753..70dd95a062 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java @@ -37,6 +37,7 @@ import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeFunction; import org.springframework.web.reactive.function.client.ExchangeFunctions; @@ -85,6 +86,10 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { private @Nullable MultiValueMap defaultCookies; + private @Nullable Object defaultApiVersion; + + private @Nullable ApiVersionInserter apiVersionInserter; + private @Nullable List filters; private Consumer> entityResultConsumer = result -> {}; @@ -142,6 +147,8 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { } this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null); + this.defaultApiVersion = other.defaultApiVersion; + this.apiVersionInserter = other.apiVersionInserter; this.filters = (other.filters != null ? new ArrayList<>(other.filters) : null); this.entityResultConsumer = other.entityResultConsumer; this.strategies = other.strategies; @@ -200,6 +207,18 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { return this.defaultCookies; } + @Override + public WebTestClient.Builder defaultApiVersion(Object version) { + this.defaultApiVersion = version; + return this; + } + + @Override + public WebTestClient.Builder apiVersionInserter(ApiVersionInserter apiVersionInserter) { + this.apiVersionInserter = apiVersionInserter; + return this; + } + @Override public WebTestClient.Builder filter(ExchangeFilterFunction filter) { Assert.notNull(filter, "ExchangeFilterFunction is required"); @@ -283,10 +302,12 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { .orElse(exchange); }; - return new DefaultWebTestClient(connectorToUse, exchangeStrategies, exchangeFactory, initUriBuilderFactory(), - this.defaultHeaders != null ? HttpHeaders.readOnlyHttpHeaders(this.defaultHeaders) : null, - this.defaultCookies != null ? CollectionUtils.unmodifiableMultiValueMap(this.defaultCookies) : null, - this.entityResultConsumer, this.responseTimeout, new DefaultWebTestClientBuilder(this)); + return new DefaultWebTestClient( + connectorToUse, exchangeStrategies, exchangeFactory, initUriBuilderFactory(), + (this.defaultHeaders != null ? HttpHeaders.readOnlyHttpHeaders(this.defaultHeaders) : null), + (this.defaultCookies != null ? CollectionUtils.unmodifiableMultiValueMap(this.defaultCookies) : null), + this.defaultApiVersion, this.apiVersionInserter, this.entityResultConsumer, + this.responseTimeout, new DefaultWebTestClientBuilder(this)); } private static ClientHttpConnector initConnector() { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index d9849d8c27..b7018bcdda 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -45,7 +45,10 @@ import org.springframework.test.json.JsonCompareMode; import org.springframework.test.json.JsonComparison; import org.springframework.util.MultiValueMap; import org.springframework.validation.Validator; +import org.springframework.web.client.ApiVersionFormatter; +import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; +import org.springframework.web.reactive.config.ApiVersionConfigurer; import org.springframework.web.reactive.config.BlockingExecutionConfigurer; import org.springframework.web.reactive.config.CorsRegistry; import org.springframework.web.reactive.config.PathMatchConfigurer; @@ -344,6 +347,12 @@ public interface WebTestClient { */ ControllerSpec validator(Validator validator); + /** + * Configure API versioning for mapping requests to controller methods. + * @since 7.0 + */ + ControllerSpec apiVersioning(Consumer configurer); + /** * Configure view resolution. * @see WebFluxConfigurer#configureViewResolvers @@ -428,6 +437,24 @@ public interface WebTestClient { */ Builder defaultCookies(Consumer> cookiesConsumer); + /** + * Global option to specify an API version to add to every request, + * if not already set. + * @param version the version to use + * @return this builder + * @since 7.0 + */ + Builder defaultApiVersion(Object version); + + /** + * Configure an {@link ApiVersionInserter} to abstract how an API version + * specified via {@link RequestHeadersSpec#apiVersion(Object)} + * is inserted into the request. + * @param apiVersionInserter the inserter to use + * @since 7.0 + */ + Builder apiVersionInserter(ApiVersionInserter apiVersionInserter); + /** * Add the given filter to the filter chain. * @param filter the filter to be added to the chain @@ -643,6 +670,17 @@ public interface WebTestClient { */ S headers(Consumer headersConsumer); + /** + * Set an API version for the request. The version is inserted into the + * request by the {@linkplain Builder#apiVersionInserter(ApiVersionInserter) + * configured} {@code ApiVersionInserter}. + * @param version the API version of the request; this can be a String or + * some Object that can be formatted by the inserter — for example, + * through an {@link ApiVersionFormatter} + * @since 7.0 + */ + S apiVersion(Object version); + /** * Set the attribute with the given name to the given value. * @param name the name of the attribute to add diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcWebTestClient.java index bab99a0a41..7967512b3f 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcWebTestClient.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,6 +38,7 @@ import org.springframework.test.web.servlet.setup.MockMvcConfigurer; import org.springframework.test.web.servlet.setup.RouterFunctionMockMvcBuilder; import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder; import org.springframework.validation.Validator; +import org.springframework.web.accept.ApiVersionStrategy; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -284,6 +285,14 @@ public interface MockMvcWebTestClient { */ ControllerSpec conversionService(FormattingConversionService conversionService); + /** + * Set the {@link ApiVersionStrategy} to use when mapping requests. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setApiVersionStrategy(ApiVersionStrategy)}. + * @since 7.0 + */ + ControllerSpec apiVersionStrategy(ApiVersionStrategy versionStrategy); + /** * Add global interceptors. *

This is delegated to diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/StandaloneMockMvcSpec.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/StandaloneMockMvcSpec.java index b4a2e9c0e1..d0f6712f11 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/StandaloneMockMvcSpec.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/StandaloneMockMvcSpec.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. @@ -26,6 +26,7 @@ import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder; import org.springframework.validation.Validator; +import org.springframework.web.accept.ApiVersionStrategy; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; @@ -79,6 +80,12 @@ class StandaloneMockMvcSpec extends AbstractMockMvcServerSpec locales = new ArrayList<>(); + private @Nullable Object version; + + private @Nullable ApiVersionInserter versionInserter; + private final Map requestAttributes = new LinkedHashMap<>(); private final Map sessionAttributes = new LinkedHashMap<>(); @@ -469,6 +476,34 @@ public abstract class AbstractMockHttpServletRequestBuilder entry : parentBuilder.requestAttributes.entrySet()) { String attributeName = entry.getKey(); if (!this.requestAttributes.containsKey(attributeName)) { @@ -700,7 +743,15 @@ public abstract class AbstractMockHttpServletRequestBuilder values.forEach(value -> this.headers.add(name, value))); + } + this.headers.forEach((name, values) -> { for (Object value : values) { request.addHeader(name, value); @@ -753,7 +811,7 @@ public abstract class AbstractMockHttpServletRequestBuilder { for (String value : values) { diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java index bf95cfee92..2189ad5dd6 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.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. @@ -36,10 +36,12 @@ import org.springframework.test.web.servlet.MockMvcBuilderSupport; import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.test.web.servlet.ResultHandler; import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.request.AbstractMockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.ConfigurableSmartRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.util.Assert; +import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.context.WebApplicationContext; /** @@ -62,6 +64,8 @@ public abstract class AbstractMockMvcBuilder private final List filters = new ArrayList<>(); + private @Nullable ApiVersionInserter apiVersionInserter; + private @Nullable RequestBuilder defaultRequestBuilder; private @Nullable Charset defaultResponseCharacterEncoding; @@ -106,6 +110,12 @@ public abstract class AbstractMockMvcBuilder return self(); } + @Override + public T apiVersionInserter(ApiVersionInserter versionInserter) { + this.apiVersionInserter = versionInserter; + return self(); + } + @Override public final T defaultRequest(RequestBuilder requestBuilder) { this.defaultRequestBuilder = requestBuilder; @@ -189,11 +199,20 @@ public abstract class AbstractMockMvcBuilder filterDecorator.initIfRequired(servletContext); } catch (ServletException ex) { - throw new RuntimeException("Failed to initialize Filter " + filter, ex); + throw new IllegalStateException("Failed to initialize Filter " + filter, ex); } } } + if (this.apiVersionInserter != null) { + if (this.defaultRequestBuilder == null) { + this.defaultRequestBuilder = MockMvcRequestBuilders.get("/"); + } + if (this.defaultRequestBuilder instanceof AbstractMockHttpServletRequestBuilder srb) { + srb.apiVersionInserter(this.apiVersionInserter); + } + } + return super.createMockMvc(filterArray, mockServletConfig, wac, this.defaultRequestBuilder, this.defaultResponseCharacterEncoding, this.globalResultMatchers, this.globalResultHandlers, this.dispatcherServletCustomizers); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/ConfigurableMockMvcBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/ConfigurableMockMvcBuilder.java index 109280cd53..7afd093c88 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/ConfigurableMockMvcBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/ConfigurableMockMvcBuilder.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. @@ -30,6 +30,7 @@ import org.springframework.test.web.servlet.MockMvcBuilder; import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.test.web.servlet.ResultHandler; import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.web.client.ApiVersionInserter; /** * Defines common methods for building a {@code MockMvc}. @@ -76,6 +77,14 @@ public interface ConfigurableMockMvcBuilder initParams, EnumSet dispatcherTypes, String... urlPatterns); + /** + * Set the {@link ApiVersionInserter} to use to apply to versions specified via + * {@link org.springframework.test.web.servlet.request.AbstractMockHttpServletRequestBuilder#apiVersion(Object)}. + * @param versionInserter the inserter to use + * @since 7.0 + */ + T apiVersionInserter(ApiVersionInserter versionInserter); + /** * Define default request properties that should be merged into all * performed requests. In effect this provides a mechanism for defining diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java index 4f0ec932a9..2759335aed 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.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. @@ -42,6 +42,7 @@ import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; import org.springframework.util.StringValueResolver; import org.springframework.validation.Validator; +import org.springframework.web.accept.ApiVersionStrategy; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationObjectSupport; @@ -108,6 +109,8 @@ public class StandaloneMockMvcBuilder extends AbstractMockMvcBuilder handlerExceptionResolvers; private @Nullable Long asyncRequestTimeout; @@ -189,6 +192,15 @@ public class StandaloneMockMvcBuilder extends AbstractMockMvcBuilder beanClass, int autowireMode, boolean dependencyCheck) { return BeanUtils.instantiateClass(beanClass); diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java index 006c68745f..6ab8ba41c8 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.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. @@ -226,6 +226,15 @@ class MockHttpServletRequestTests { assertThat(request.getCharacterEncoding()).isEqualTo("UTF-8"); } + @Test + void contentTypeMultipleCalls() { + String contentType = "text/html"; + request.addHeader(HttpHeaders.CONTENT_TYPE, "text/plain"); + request.addHeader(HttpHeaders.CONTENT_TYPE, contentType); + assertThat(request.getContentType()).isEqualTo(contentType); + assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo(contentType); + } + @Test // SPR-12677 void setContentTypeHeaderWithMoreComplexCharsetSyntax() { String contentType = "test/plain;charset=\"utf-8\";foo=\"charset=bar\";foocharset=bar;foo=bar"; diff --git a/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueUtilsTests.java b/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueUtilsTests.java index c5c82c3972..d3d8a564c5 100644 --- a/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueUtilsTests.java @@ -31,6 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 3.0 */ +@SuppressWarnings("deprecation") class ProfileValueUtilsTests { private static final String NON_ANNOTATED_METHOD = "nonAnnotatedMethod"; @@ -140,7 +141,6 @@ class ProfileValueUtilsTests { @Test void isTestEnabledInThisEnvironmentForProvidedProfileValueSourceMethodAndClass() throws Exception { - ProfileValueSource profileValueSource = SystemProfileValueSource.getInstance(); assertMethodIsEnabled(profileValueSource, NON_ANNOTATED_METHOD, NonAnnotated.class); diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextHierarchyDirtiesContextTests.java b/spring-test/src/test/java/org/springframework/test/context/ContextHierarchyDirtiesContextTests.java index 41388fc7da..57d6ad93f3 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextHierarchyDirtiesContextTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/ContextHierarchyDirtiesContextTests.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. @@ -20,9 +20,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import org.junit.runner.JUnitCore; -import org.junit.runner.Result; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.testkit.engine.EngineTestKit; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; @@ -32,9 +31,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; /** * Integration tests that verify proper behavior of {@link DirtiesContext @DirtiesContext} @@ -87,9 +87,11 @@ class ContextHierarchyDirtiesContextTests { private void runTestAndVerifyHierarchies(Class testClass, boolean isFooContextActive, boolean isBarContextActive, boolean isBazContextActive) { - JUnitCore jUnitCore = new JUnitCore(); - Result result = jUnitCore.run(testClass); - assertThat(result.wasSuccessful()).as("all tests passed").isTrue(); + EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(testClass)) + .execute() + .testEvents() + .assertStatistics(stats -> stats.started(1).succeeded(1).failed(0)); assertThat(ContextHierarchyDirtiesContextTests.context).isNotNull(); @@ -111,7 +113,7 @@ class ContextHierarchyDirtiesContextTests { // ------------------------------------------------------------------------- - @RunWith(SpringRunner.class) + @ExtendWith(SpringExtension.class) @ContextHierarchy(@ContextConfiguration(name = "foo")) abstract static class FooTestCase implements ApplicationContextAware { @@ -170,10 +172,10 @@ class ContextHierarchyDirtiesContextTests { * context. */ @DirtiesContext - public static class ClassLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase { + static class ClassLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase { - @org.junit.Test - public void test() { + @Test + void test() { } } @@ -184,10 +186,10 @@ class ContextHierarchyDirtiesContextTests { * beginning from the current context hierarchy and down through all subhierarchies. */ @DirtiesContext(hierarchyMode = HierarchyMode.CURRENT_LEVEL) - public static class ClassLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase { + static class ClassLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase { - @org.junit.Test - public void test() { + @Test + void test() { } } @@ -199,11 +201,11 @@ class ContextHierarchyDirtiesContextTests { * parent context, and then back down through all subhierarchies of the parent * context. */ - public static class MethodLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase { + static class MethodLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase { - @org.junit.Test + @Test @DirtiesContext - public void test() { + void test() { } } @@ -213,11 +215,11 @@ class ContextHierarchyDirtiesContextTests { *

After running this test class, the context cache should be cleared * beginning from the current context hierarchy and down through all subhierarchies. */ - public static class MethodLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase { + static class MethodLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase { - @org.junit.Test + @Test @DirtiesContext(hierarchyMode = HierarchyMode.CURRENT_LEVEL) - public void test() { + void test() { } } diff --git a/spring-test/src/test/java/org/springframework/test/context/SpringTestContextFrameworkTestSuite.java b/spring-test/src/test/java/org/springframework/test/context/SpringTestContextFrameworkTestSuite.java index 4500d55668..cb4b809abf 100644 --- a/spring-test/src/test/java/org/springframework/test/context/SpringTestContextFrameworkTestSuite.java +++ b/spring-test/src/test/java/org/springframework/test/context/SpringTestContextFrameworkTestSuite.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. @@ -16,6 +16,8 @@ package org.springframework.test.context; +import org.junit.jupiter.api.ClassOrderer; +import org.junit.platform.suite.api.ConfigurationParameter; import org.junit.platform.suite.api.ExcludeTags; import org.junit.platform.suite.api.IncludeClassNamePatterns; import org.junit.platform.suite.api.SelectPackages; @@ -44,5 +46,9 @@ import org.junit.platform.suite.api.Suite; @SelectPackages("org.springframework.test.context") @IncludeClassNamePatterns(".*Tests?$") @ExcludeTags("failing-test-case") +@ConfigurationParameter( + key = ClassOrderer.DEFAULT_ORDER_PROPERTY_NAME, + value = "org.junit.jupiter.api.ClassOrderer$ClassName" + ) class SpringTestContextFrameworkTestSuite { } diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java index e5fa0317f8..aa9eae8800 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.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. @@ -147,8 +147,6 @@ class AotIntegrationTests extends AbstractAotTests { .filter(clazz -> clazz.getSimpleName().endsWith("Tests")) // TestNG EJB tests use @PersistenceContext which is not yet supported in tests in AOT mode. .filter(clazz -> !clazz.getPackageName().contains("testng.transaction.ejb")) - // Uncomment the following to disable Bean Override tests since they are not yet supported in AOT mode. - // .filter(clazz -> !clazz.getPackageName().contains("test.context.bean.override")) .toList(); // Optionally set failOnError flag to true to halt processing at the first failure. @@ -169,8 +167,6 @@ class AotIntegrationTests extends AbstractAotTests { void endToEndTestsForSelectedTestClasses() { List> testClasses = List.of( org.springframework.test.context.bean.override.easymock.EasyMockBeanIntegrationTests.class, - org.springframework.test.context.bean.override.mockito.MockitoBeanForByNameLookupIntegrationTests.class, - org.springframework.test.context.junit4.SpringJUnit4ClassRunnerAppCtxTests.class, org.springframework.test.context.junit4.ParameterizedDependencyInjectionTests.class ); diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java index 2e9dd795cf..de275583b8 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.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,13 +16,23 @@ package org.springframework.test.context.aot; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.aot.generate.GeneratedFiles; +import org.springframework.aot.generate.InMemoryGeneratedFiles; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.SpringProperties; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.context.aot.TestContextAotGenerator.FAIL_ON_ERROR_PROPERTY_NAME; @@ -60,9 +70,55 @@ class TestContextAotGeneratorTests { assertThat(createGenerator().failOnError).isFalse(); } + @Test // gh-34841 + void contextIsClosedAfterAotProcessing() { + DemoTestContextAotGenerator generator = createGenerator(); + generator.processAheadOfTime(Stream.of(TestCase1.class, TestCase2.class)); - private static TestContextAotGenerator createGenerator() { - return new TestContextAotGenerator(null); + assertThat(generator.contexts) + .allSatisfy(context -> assertThat(context.isClosed()).as("context is closed").isTrue()); + } + + + private static DemoTestContextAotGenerator createGenerator() { + return new DemoTestContextAotGenerator(new InMemoryGeneratedFiles()); + } + + + private static class DemoTestContextAotGenerator extends TestContextAotGenerator { + + List contexts = new ArrayList<>(); + + DemoTestContextAotGenerator(GeneratedFiles generatedFiles) { + super(generatedFiles); + } + + @Override + GenericApplicationContext loadContextForAotProcessing( + MergedContextConfiguration mergedConfig) throws TestContextAotException { + + GenericApplicationContext context = super.loadContextForAotProcessing(mergedConfig); + this.contexts.add(context); + return context; + } + } + + @SpringJUnitConfig + private static class TestCase1 { + + @Configuration(proxyBeanMethods = false) + static class Config { + // no beans + } + } + + @SpringJUnitConfig + private static class TestCase2 { + + @Configuration(proxyBeanMethods = false) + static class Config { + // no beans + } } } diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests.java index 4e1189674b..e9186c0961 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests.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. @@ -46,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThat; // Override the default loader configured by the CustomXmlBootstrapper @ContextConfiguration(classes = BasicTestConfiguration.class, loader = AnnotationConfigContextLoader.class) @TestPropertySource +@SuppressWarnings("deprecation") public class BasicSpringVintageTests { @Autowired diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/web/WebSpringVintageTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/web/WebSpringVintageTests.java index d18c206838..14c3e1b9df 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/samples/web/WebSpringVintageTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/web/WebSpringVintageTests.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. @@ -41,6 +41,7 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC @ContextConfiguration(classes = WebTestConfiguration.class) @WebAppConfiguration @TestPropertySource(properties = "test.engine = vintage") +@SuppressWarnings("deprecation") public class WebSpringVintageTests { MockMvc mockMvc; diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/xml/XmlSpringVintageTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/xml/XmlSpringVintageTests.java index 1e67f5dc32..0fe9cffbf6 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/samples/xml/XmlSpringVintageTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/xml/XmlSpringVintageTests.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. @@ -35,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @ContextConfiguration("test-config.xml") @TestPropertySource(properties = "test.engine = vintage") +@SuppressWarnings("deprecation") public class XmlSpringVintageTests { @Autowired diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java index 3d1ca60275..2e16914673 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.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,12 +16,13 @@ package org.springframework.test.context.bean.override; -import java.util.Collections; +import java.util.List; import java.util.function.Consumer; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; +import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.bean.override.DummyBean.DummyBeanOverrideProcessor.DummyBeanOverrideHandler; import static org.assertj.core.api.Assertions.assertThat; @@ -91,7 +92,7 @@ class BeanOverrideContextCustomizerFactoryTests { } private @Nullable BeanOverrideContextCustomizer createContextCustomizer(Class testClass) { - return this.factory.createContextCustomizer(testClass, Collections.emptyList()); + return this.factory.createContextCustomizer(testClass, List.of(new ContextConfigurationAttributes(testClass))); } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTestUtils.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTestUtils.java index 9e05c0499b..3948db4ff8 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTestUtils.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTestUtils.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,11 +16,12 @@ package org.springframework.test.context.bean.override; -import java.util.Collections; +import java.util.List; import org.jspecify.annotations.Nullable; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.MergedContextConfiguration; @@ -44,7 +45,7 @@ public abstract class BeanOverrideContextCustomizerTestUtils { * @return a context customizer for bean override support, or null */ public static @Nullable ContextCustomizer createContextCustomizer(Class testClass) { - return factory.createContextCustomizer(testClass, Collections.emptyList()); + return factory.createContextCustomizer(testClass, List.of(new ContextConfigurationAttributes(testClass))); } /** diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTests.java index 03886cf570..17a32878fc 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTests.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 @@ class BeanOverrideContextCustomizerTests { public DummyBeanOverrideHandler(String key) { super(ReflectionUtils.findField(DummyBeanOverrideHandler.class, "key"), - ResolvableType.forClass(Object.class), null, BeanOverrideStrategy.REPLACE); + ResolvableType.forClass(Object.class), null, "", BeanOverrideStrategy.REPLACE); this.key = key; } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideHandlerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideHandlerTests.java index 3d49710a72..49c8d85007 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideHandlerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideHandlerTests.java @@ -30,6 +30,7 @@ import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.context.bean.override.DummyBean.DummyBeanOverrideProcessor; import org.springframework.test.context.bean.override.DummyBean.DummyBeanOverrideProcessor.DummyBeanOverrideHandler; import org.springframework.test.context.bean.override.example.CustomQualifier; import org.springframework.test.context.bean.override.example.ExampleService; @@ -116,7 +117,7 @@ class BeanOverrideHandlerTests { } @Test - void isEqualToWithSameMetadataAndBeanNames() { + void isEqualToWithSameMetadataAndSameBeanNames() { BeanOverrideHandler handler1 = createBeanOverrideHandler(field(ConfigA.class, "noQualifier"), "testBean"); BeanOverrideHandler handler2 = createBeanOverrideHandler(field(ConfigA.class, "noQualifier"), "testBean"); assertThat(handler1).isEqualTo(handler2); @@ -124,10 +125,29 @@ class BeanOverrideHandlerTests { } @Test - void isNotEqualToWithSameMetadataAndDifferentBeaName() { + void isNotEqualToWithSameMetadataButDifferentBeanNames() { BeanOverrideHandler handler1 = createBeanOverrideHandler(field(ConfigA.class, "noQualifier"), "testBean"); BeanOverrideHandler handler2 = createBeanOverrideHandler(field(ConfigA.class, "noQualifier"), "testBean2"); assertThat(handler1).isNotEqualTo(handler2); + assertThat(handler1).doesNotHaveSameHashCodeAs(handler2); + } + + @Test + void isEqualToWithSameMetadataSameBeanNamesAndSameContextNames() { + Class testClass = MultipleAnnotationsWithSameNameInDifferentContext.class; + BeanOverrideHandler handler1 = createBeanOverrideHandler(testClass, field(testClass, "parentMessageBean")); + BeanOverrideHandler handler2 = createBeanOverrideHandler(testClass, field(testClass, "parentMessageBean2")); + assertThat(handler1).isEqualTo(handler2); + assertThat(handler1).hasSameHashCodeAs(handler2); + } + + @Test + void isEqualToWithSameMetadataAndSameBeanNamesButDifferentContextNames() { + Class testClass = MultipleAnnotationsWithSameNameInDifferentContext.class; + BeanOverrideHandler handler1 = createBeanOverrideHandler(testClass, field(testClass, "parentMessageBean")); + BeanOverrideHandler handler2 = createBeanOverrideHandler(testClass, field(testClass, "childMessageBean")); + assertThat(handler1).isNotEqualTo(handler2); + assertThat(handler1).doesNotHaveSameHashCodeAs(handler2); } @Test @@ -173,6 +193,7 @@ class BeanOverrideHandlerTests { BeanOverrideHandler handler1 = createBeanOverrideHandler(field(ConfigA.class, "directQualifier")); BeanOverrideHandler handler2 = createBeanOverrideHandler(field(ConfigA.class, "differentDirectQualifier")); assertThat(handler1).isNotEqualTo(handler2); + assertThat(handler1).doesNotHaveSameHashCodeAs(handler2); } @Test @@ -180,6 +201,7 @@ class BeanOverrideHandlerTests { BeanOverrideHandler handler1 = createBeanOverrideHandler(field(ConfigA.class, "directQualifier")); BeanOverrideHandler handler2 = createBeanOverrideHandler(field(ConfigA.class, "customQualifier")); assertThat(handler1).isNotEqualTo(handler2); + assertThat(handler1).doesNotHaveSameHashCodeAs(handler2); } @Test @@ -187,6 +209,7 @@ class BeanOverrideHandlerTests { BeanOverrideHandler handler1 = createBeanOverrideHandler(field(ConfigA.class, "noQualifier")); BeanOverrideHandler handler2 = createBeanOverrideHandler(field(ConfigB.class, "example")); assertThat(handler1).isNotEqualTo(handler2); + assertThat(handler1).doesNotHaveSameHashCodeAs(handler2); } private static BeanOverrideHandler createBeanOverrideHandler(Field field) { @@ -194,7 +217,11 @@ class BeanOverrideHandlerTests { } private static BeanOverrideHandler createBeanOverrideHandler(Field field, @Nullable String name) { - return new DummyBeanOverrideHandler(field, field.getType(), name, BeanOverrideStrategy.REPLACE); + return new DummyBeanOverrideHandler(field, field.getType(), name, "", BeanOverrideStrategy.REPLACE); + } + + private static BeanOverrideHandler createBeanOverrideHandler(Class testClass, Field field) { + return new DummyBeanOverrideProcessor().createHandler(field.getAnnotation(DummyBean.class), testClass, field); } private static Field field(Class target, String fieldName) { @@ -234,6 +261,18 @@ class BeanOverrideHandlerTests { Integer counter; } + static class MultipleAnnotationsWithSameNameInDifferentContext { + + @DummyBean(beanName = "messageBean", contextName = "parent") + String parentMessageBean; + + @DummyBean(beanName = "messageBean", contextName = "parent") + String parentMessageBean2; + + @DummyBean(beanName = "messageBean", contextName = "child") + String childMessageBean; + } + static class MultipleAnnotationsDuplicate { @DummyBean(beanName = "messageBean") diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListenerTests.java new file mode 100644 index 0000000000..cb0018d3a2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListenerTests.java @@ -0,0 +1,143 @@ +/* + * 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.test.context.bean.override; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.testkit.engine.EngineTestKit; +import org.junit.platform.testkit.engine.Events; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.testkit.engine.EventConditions.event; +import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; +import static org.junit.platform.testkit.engine.EventConditions.test; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; + +/** + * Integration tests for {@link BeanOverrideTestExecutionListener}. + * + * @author Sam Brannen + * @since 6.2.6 + */ +class BeanOverrideTestExecutionListenerTests { + + @Test + void beanOverrideWithNoMatchingContextName() { + executeTests(BeanOverrideWithNoMatchingContextNameTestCase.class) + .assertThatEvents().haveExactly(1, event(test("test"), + finishedWithFailure( + instanceOf(IllegalStateException.class), + message(""" + Test class BeanOverrideWithNoMatchingContextNameTestCase declares @BeanOverride \ + fields [message, number], but no BeanOverrideHandler has been registered. \ + If you are using @ContextHierarchy, ensure that context names for bean overrides match \ + configured @ContextConfiguration names.""")))); + } + + @Test + void beanOverrideWithInvalidContextName() { + executeTests(BeanOverrideWithInvalidContextNameTestCase.class) + .assertThatEvents().haveExactly(1, event(test("test"), + finishedWithFailure( + instanceOf(IllegalStateException.class), + message(msg -> + msg.startsWith("No bean override instance found for BeanOverrideHandler") && + msg.contains("DummyBeanOverrideHandler") && + msg.contains("BeanOverrideWithInvalidContextNameTestCase.message2") && + msg.contains("contextName = 'BOGUS'") && + msg.endsWith(""" + If you are using @ContextHierarchy, ensure that context names for bean overrides match \ + configured @ContextConfiguration names."""))))); + } + + + private static Events executeTests(Class testClass) { + return EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(testClass)) + .execute() + .testEvents() + .assertStatistics(stats -> stats.started(1).failed(1)); + } + + + @ExtendWith(SpringExtension.class) + @ContextHierarchy({ + @ContextConfiguration(classes = Config1.class), + @ContextConfiguration(classes = Config2.class, name = "child") + }) + @DisabledInAotMode("@ContextHierarchy is not supported in AOT") + static class BeanOverrideWithNoMatchingContextNameTestCase { + + @DummyBean(contextName = "BOGUS") + String message; + + @DummyBean(contextName = "BOGUS") + Integer number; + + @Test + void test() { + // no-op + } + } + + @ExtendWith(SpringExtension.class) + @ContextHierarchy({ + @ContextConfiguration(classes = Config1.class), + @ContextConfiguration(classes = Config2.class, name = "child") + }) + @DisabledInAotMode("@ContextHierarchy is not supported in AOT") + static class BeanOverrideWithInvalidContextNameTestCase { + + @DummyBean(contextName = "child") + String message1; + + @DummyBean(contextName = "BOGUS") + String message2; + + @Test + void test() { + // no-op + } + } + + @Configuration + static class Config1 { + + @Bean + String message() { + return "Message 1"; + } + } + + @Configuration + static class Config2 { + + @Bean + String message() { + return "Message 2"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/DummyBean.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/DummyBean.java index b285510219..4d7a147667 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/DummyBean.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/DummyBean.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. @@ -33,7 +33,7 @@ import org.springframework.util.StringUtils; /** * A dummy {@link BeanOverride} implementation that only handles {@link CharSequence} - * and {@link Integer} and replace them with {@code "overridden"} and {@code 42}, + * and {@link Integer} and replaces them with {@code "overridden"} and {@code 42}, * respectively. * * @author Stephane Nicoll @@ -46,6 +46,8 @@ import org.springframework.util.StringUtils; String beanName() default ""; + String contextName() default ""; + BeanOverrideStrategy strategy() default BeanOverrideStrategy.REPLACE; class DummyBeanOverrideProcessor implements BeanOverrideProcessor { @@ -55,7 +57,7 @@ import org.springframework.util.StringUtils; DummyBean dummyBean = (DummyBean) annotation; String beanName = (StringUtils.hasText(dummyBean.beanName()) ? dummyBean.beanName() : null); return new DummyBeanOverrideProcessor.DummyBeanOverrideHandler(field, field.getType(), beanName, - dummyBean.strategy()); + dummyBean.contextName(), dummyBean.strategy()); } // Bare bone, "dummy", implementation that should not override anything @@ -63,9 +65,9 @@ import org.springframework.util.StringUtils; static class DummyBeanOverrideHandler extends BeanOverrideHandler { DummyBeanOverrideHandler(Field field, Class typeToOverride, @Nullable String beanName, - BeanOverrideStrategy strategy) { + String contextName, BeanOverrideStrategy strategy) { - super(field, ResolvableType.forClass(typeToOverride), beanName, strategy); + super(field, ResolvableType.forClass(typeToOverride), beanName, contextName, strategy); } @Override diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandlerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandlerTests.java index f95fe62912..b47ea30c13 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandlerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandlerTests.java @@ -130,7 +130,7 @@ class TestBeanOverrideHandlerTests { TestBean annotation = field.getAnnotation(TestBean.class); String beanName = (StringUtils.hasText(annotation.name()) ? annotation.name() : null); return new TestBeanOverrideHandler( - field, ResolvableType.forClass(field.getType()), beanName, BeanOverrideStrategy.REPLACE, overrideMethod); + field, ResolvableType.forClass(field.getType()), beanName, "", BeanOverrideStrategy.REPLACE, overrideMethod); } static class SampleOneOverride { diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInChildContextHierarchyTests.java new file mode 100644 index 0000000000..a59c59bfa0 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInChildContextHierarchyTests.java @@ -0,0 +1,109 @@ +/* + * 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.test.context.bean.override.convention.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInChildContextHierarchyTests.Config2; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@link TestBean @TestBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only overridden "by name" in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class TestBeanByNameInChildContextHierarchyTests { + + @TestBean(name = "service", contextName = "child") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + static ExampleService service() { + return () -> "@TestBean 2"; + } + + + @Test + void test(ApplicationContext context) { + ExampleService serviceInParent = context.getParent().getBean(ExampleService.class); + + assertThat(service.greeting()).isEqualTo("@TestBean 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return () -> "Service 1"; + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return () -> "Service 2"; + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentAndChildContextHierarchyTests.java new file mode 100644 index 0000000000..8df069273a --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentAndChildContextHierarchyTests.java @@ -0,0 +1,114 @@ +/* + * 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.test.context.bean.override.convention.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInParentAndChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInParentAndChildContextHierarchyTests.Config2; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@link TestBean @TestBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * identical beans are overridden "by name" in the parent and in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class TestBeanByNameInParentAndChildContextHierarchyTests { + + @TestBean(name = "service", contextName = "parent") + ExampleService serviceInParent; + + @TestBean(name = "service", contextName = "child") + ExampleService serviceInChild; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + static ExampleService serviceInParent() { + return () -> "@TestBean 1"; + } + + static ExampleService serviceInChild() { + return () -> "@TestBean 2"; + } + + + @Test + void test() { + assertThat(serviceInParent.greeting()).isEqualTo("@TestBean 1"); + assertThat(serviceInChild.greeting()).isEqualTo("@TestBean 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(serviceInChild); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say @TestBean 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return () -> "Service 1"; + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return () -> "Service 2"; + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentContextHierarchyTests.java new file mode 100644 index 0000000000..e2f3ec516c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentContextHierarchyTests.java @@ -0,0 +1,101 @@ +/* + * 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.test.context.bean.override.convention.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInParentContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInParentContextHierarchyTests.Config2; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@link TestBean @TestBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only overridden "by name" in the parent. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class) +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class TestBeanByNameInParentContextHierarchyTests { + + @TestBean(name = "service", contextName = "parent") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + static ExampleService service() { + return () -> "@TestBean 1"; + } + + + @Test + void test() { + assertThat(service.greeting()).isEqualTo("@TestBean 1"); + assertThat(serviceCaller1.getService()).isSameAs(service); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say @TestBean 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 1"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return () -> "Service 1"; + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInChildContextHierarchyTests.java new file mode 100644 index 0000000000..b1e8461fe0 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInChildContextHierarchyTests.java @@ -0,0 +1,109 @@ +/* + * 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.test.context.bean.override.convention.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInChildContextHierarchyTests.Config2; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@link TestBean @TestBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only overridden "by type" in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class TestBeanByTypeInChildContextHierarchyTests { + + @TestBean(contextName = "child") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + static ExampleService service() { + return () -> "@TestBean 2"; + } + + + @Test + void test(ApplicationContext context) { + ExampleService serviceInParent = context.getParent().getBean(ExampleService.class); + + assertThat(service.greeting()).isEqualTo("@TestBean 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return () -> "Service 1"; + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return () -> "Service 2"; + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentAndChildContextHierarchyTests.java new file mode 100644 index 0000000000..b7e021528e --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentAndChildContextHierarchyTests.java @@ -0,0 +1,114 @@ +/* + * 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.test.context.bean.override.convention.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInParentAndChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInParentAndChildContextHierarchyTests.Config2; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@link TestBean @TestBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * identical beans are overridden "by type" in the parent and in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class TestBeanByTypeInParentAndChildContextHierarchyTests { + + @TestBean(contextName = "parent") + ExampleService serviceInParent; + + @TestBean(contextName = "child") + ExampleService serviceInChild; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + static ExampleService serviceInParent() { + return () -> "@TestBean 1"; + } + + static ExampleService serviceInChild() { + return () -> "@TestBean 2"; + } + + + @Test + void test() { + assertThat(serviceInParent.greeting()).isEqualTo("@TestBean 1"); + assertThat(serviceInChild.greeting()).isEqualTo("@TestBean 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(serviceInChild); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say @TestBean 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return () -> "Service 1"; + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return () -> "Service 2"; + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentContextHierarchyTests.java new file mode 100644 index 0000000000..5fb01297c7 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentContextHierarchyTests.java @@ -0,0 +1,101 @@ +/* + * 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.test.context.bean.override.convention.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInParentContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInParentContextHierarchyTests.Config2; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@link TestBean @TestBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only overridden "by type" in the parent. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class) +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class TestBeanByTypeInParentContextHierarchyTests { + + @TestBean(contextName = "parent") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + static ExampleService service() { + return () -> "@TestBean 1"; + } + + + @Test + void test() { + assertThat(service.greeting()).isEqualTo("@TestBean 1"); + assertThat(serviceCaller1.getService()).isSameAs(service); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say @TestBean 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 1"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return () -> "Service 1"; + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideHandler.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideHandler.java index 519c009dbe..c121fa30c6 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideHandler.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideHandler.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. @@ -43,7 +43,7 @@ class EasyMockBeanOverrideHandler extends BeanOverrideHandler { EasyMockBeanOverrideHandler(Field field, Class typeToOverride, @Nullable String beanName, MockType mockType) { - super(field, ResolvableType.forClass(typeToOverride), beanName, REPLACE_OR_CREATE); + super(field, ResolvableType.forClass(typeToOverride), beanName, "", REPLACE_OR_CREATE); this.mockType = mockType; } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests.java new file mode 100644 index 0000000000..84fd8be143 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; + +/** + * Abstract top-level class and abstract inner class for integration tests for + * {@link MockitoBean @MockitoBean} which verify that {@code @MockitoBean} fields + * are not discovered more than once when searching intertwined enclosing class + * hierarchies and type hierarchies, when a superclass is present twice + * in the intertwined hierarchies. + * + * @author Sam Brannen + * @since 6.2.7 + * @see MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests + * @see gh-34844 + */ +@ExtendWith(SpringExtension.class) +abstract class AbstractMockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests { + + @Autowired + ApplicationContext enclosingContext; + + @MockitoBean + ExampleService service; + + + @Test + void topLevelTest() { + assertIsMock(service); + assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1); + } + + + abstract class AbstractBaseClassForNestedTests { + + @Test + void nestedTest(ApplicationContext nestedContext) { + assertIsMock(service); + assertThat(enclosingContext).isSameAs(nestedContext); + assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests.java similarity index 81% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesTests.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests.java index 72babe6867..7934e07f7b 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests.java @@ -31,14 +31,17 @@ import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; /** * Integration tests for {@link MockitoBean @MockitoBean} which verify that * {@code @MockitoBean} fields are not discovered more than once when searching - * intertwined enclosing class hierarchies and type hierarchies. + * intertwined enclosing class hierarchies and type hierarchies, when an enclosing + * class is present twice in the intertwined hierarchies. * * @author Sam Brannen * @since 6.2.3 + * @see MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests + * @see MockitoBeanWithInterfacePresentTwiceTests * @see gh-34324 */ @ExtendWith(SpringExtension.class) -class MockitoBeanNestedAndTypeHierarchiesTests { +class MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests { @Autowired ApplicationContext enclosingContext; @@ -50,6 +53,7 @@ class MockitoBeanNestedAndTypeHierarchiesTests { @Test void topLevelTest() { assertIsMock(service); + assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1); // The following are prerequisites for the reported regression. assertThat(NestedTests.class.getSuperclass()) @@ -66,6 +70,7 @@ class MockitoBeanNestedAndTypeHierarchiesTests { void nestedTest(ApplicationContext nestedContext) { assertIsMock(service); assertThat(enclosingContext).isSameAs(nestedContext); + assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1); } } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests.java new file mode 100644 index 0000000000..bc43837b24 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests.java @@ -0,0 +1,59 @@ +/* + * 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.test.context.bean.override.mockito; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link MockitoBean @MockitoBean} which verify that + * {@code @MockitoBean} fields are not discovered more than once when searching + * intertwined enclosing class hierarchies and type hierarchies, when a superclass + * is present twice in the intertwined hierarchies. + * + * @author Sam Brannen + * @since 6.2.7 + * @see MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests + * @see MockitoBeanWithInterfacePresentTwiceTests + * @see gh-34844 + */ +class MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests + extends AbstractMockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests { + + @Test + @Override + void topLevelTest() { + super.topLevelTest(); + + // The following are prerequisites for the reported regression. + assertThat(NestedTests.class.getSuperclass()) + .isEqualTo(AbstractBaseClassForNestedTests.class); + assertThat(NestedTests.class.getEnclosingClass()) + .isEqualTo(getClass()); + assertThat(NestedTests.class.getEnclosingClass().getSuperclass()) + .isEqualTo(AbstractBaseClassForNestedTests.class.getEnclosingClass()) + .isEqualTo(getClass().getSuperclass()); + } + + + @Nested + class NestedTests extends AbstractBaseClassForNestedTests { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java index 466bcd93e3..1875e2e2d6 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java @@ -126,7 +126,7 @@ class MockitoBeanOverrideHandlerTests { /** * Since the "field name as fallback qualifier" is not available for an annotated class, * what would seem to be "equivalent" handlers are actually not considered "equal" when - * the the lookup is "by type". + * the lookup is "by type". */ @Test // gh-33925 void isNotEqualToWithSameByTypeLookupMetadataFromFieldAndClassLevel() { diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanWithInterfacePresentTwiceTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanWithInterfacePresentTwiceTests.java new file mode 100644 index 0000000000..95d4ac8458 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanWithInterfacePresentTwiceTests.java @@ -0,0 +1,66 @@ +/* + * 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.test.context.bean.override.mockito; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; + +/** + * Integration tests for {@link MockitoBean @MockitoBean} which verify that type-level + * {@code @MockitoBean} declarations are not discovered more than once when searching + * a type hierarchy, when an interface is present twice in the hierarchy. + * + * @author Sam Brannen + * @since 6.2.7 + * @see MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests + * @see MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests + * @see gh-34844 + */ +class MockitoBeanWithInterfacePresentTwiceTests extends AbstractMockitoBeanWithInterfacePresentTwiceTests + implements MockConfigInterface { + + @Test + void test(ApplicationContext context) { + assertIsMock(service); + assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1); + + // The following are prerequisites for the tested scenario. + assertThat(getClass().getInterfaces()).containsExactly(MockConfigInterface.class); + assertThat(getClass().getSuperclass().getInterfaces()).containsExactly(MockConfigInterface.class); + } + +} + +@MockitoBean(types = ExampleService.class) +interface MockConfigInterface { +} + +@ExtendWith(SpringExtension.class) +abstract class AbstractMockitoBeanWithInterfacePresentTwiceTests implements MockConfigInterface { + + @Autowired + ExampleService service; + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/BarService.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/BarService.java new file mode 100644 index 0000000000..1b71868b4b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/BarService.java @@ -0,0 +1,24 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +class BarService { + + String bar() { + return "bar"; + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ErrorIfContextReloadedConfig.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ErrorIfContextReloadedConfig.java new file mode 100644 index 0000000000..5877553e1a --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ErrorIfContextReloadedConfig.java @@ -0,0 +1,37 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import jakarta.annotation.PostConstruct; + +import org.springframework.context.annotation.Configuration; + +@Configuration +class ErrorIfContextReloadedConfig { + + private static boolean loaded = false; + + + @PostConstruct + public void postConstruct() { + if (loaded) { + throw new RuntimeException("Context loaded multiple times"); + } + loaded = true; + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/FooService.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/FooService.java new file mode 100644 index 0000000000..ab2ee99fc9 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/FooService.java @@ -0,0 +1,24 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +class FooService { + + String foo() { + return "foo"; + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndContextHierarchyParentIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanAndContextHierarchyParentIntegrationTests.java similarity index 90% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndContextHierarchyParentIntegrationTests.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanAndContextHierarchyParentIntegrationTests.java index 00950dcd03..98d633a12b 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndContextHierarchyParentIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanAndContextHierarchyParentIntegrationTests.java @@ -14,12 +14,11 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.integration; +package org.springframework.test.context.bean.override.mockito.hierarchies; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.bean.override.example.ExampleService; @@ -45,8 +44,6 @@ public class MockitoBeanAndContextHierarchyParentIntegrationTests { @MockitoBean ExampleService service; - @Autowired - ApplicationContext context; @BeforeEach void configureServiceMock() { @@ -54,7 +51,7 @@ public class MockitoBeanAndContextHierarchyParentIntegrationTests { } @Test - void test() { + void test(ApplicationContext context) { assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1); assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).isEmpty(); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInChildContextHierarchyTests.java new file mode 100644 index 0000000000..e452b50830 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInChildContextHierarchyTests.java @@ -0,0 +1,111 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static org.springframework.test.mockito.MockitoAssertions.assertIsNotMock; + +/** + * Verifies that {@link MockitoBean @MockitoBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only mocked "by name" in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoBeanByNameInChildContextHierarchyTests { + + @MockitoBean(name = "service", contextName = "child") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test(ApplicationContext context) { + ExampleService serviceInParent = context.getParent().getBean(ExampleService.class); + + assertIsNotMock(serviceInParent); + + when(service.greeting()).thenReturn("Mock 2"); + + assertThat(service.greeting()).isEqualTo("Mock 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentAndChildContextHierarchyTests.java new file mode 100644 index 0000000000..539611a27f --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentAndChildContextHierarchyTests.java @@ -0,0 +1,110 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInParentAndChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInParentAndChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +/** + * Verifies that {@link MockitoBean @MockitoBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * identical beans are mocked "by name" in the parent and in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoBeanByNameInParentAndChildContextHierarchyTests { + + @MockitoBean(name = "service", contextName = "parent") + ExampleService serviceInParent; + + @MockitoBean(name = "service", contextName = "child") + ExampleService serviceInChild; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + when(serviceInParent.greeting()).thenReturn("Mock 1"); + when(serviceInChild.greeting()).thenReturn("Mock 2"); + + assertThat(serviceInParent.greeting()).isEqualTo("Mock 1"); + assertThat(serviceInChild.greeting()).isEqualTo("Mock 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(serviceInChild); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Mock 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentContextHierarchyTests.java new file mode 100644 index 0000000000..01832db8fd --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentContextHierarchyTests.java @@ -0,0 +1,100 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInParentContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInParentContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +/** + * Verifies that {@link MockitoBean @MockitoBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only mocked "by name" in the parent. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class) +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoBeanByNameInParentContextHierarchyTests { + + @MockitoBean(name = "service", contextName = "parent") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + when(service.greeting()).thenReturn("Mock 1"); + + assertThat(service.greeting()).isEqualTo("Mock 1"); + assertThat(serviceCaller1.getService()).isSameAs(service); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Mock 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 1"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInChildContextHierarchyTests.java new file mode 100644 index 0000000000..d6421b2081 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInChildContextHierarchyTests.java @@ -0,0 +1,111 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static org.springframework.test.mockito.MockitoAssertions.assertIsNotMock; + +/** + * Verifies that {@link MockitoBean @MockitoBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only mocked "by type" in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoBeanByTypeInChildContextHierarchyTests { + + @MockitoBean(contextName = "child") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test(ApplicationContext context) { + ExampleService serviceInParent = context.getParent().getBean(ExampleService.class); + + assertIsNotMock(serviceInParent); + + when(service.greeting()).thenReturn("Mock 2"); + + assertThat(service.greeting()).isEqualTo("Mock 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentAndChildContextHierarchyTests.java new file mode 100644 index 0000000000..f212d309a8 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentAndChildContextHierarchyTests.java @@ -0,0 +1,110 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInParentAndChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInParentAndChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +/** + * Verifies that {@link MockitoBean @MockitoBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * identical beans are mocked "by type" in the parent and in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoBeanByTypeInParentAndChildContextHierarchyTests { + + @MockitoBean(contextName = "parent") + ExampleService serviceInParent; + + @MockitoBean(contextName = "child") + ExampleService serviceInChild; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + when(serviceInParent.greeting()).thenReturn("Mock 1"); + when(serviceInChild.greeting()).thenReturn("Mock 2"); + + assertThat(serviceInParent.greeting()).isEqualTo("Mock 1"); + assertThat(serviceInChild.greeting()).isEqualTo("Mock 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(serviceInChild); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Mock 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentContextHierarchyTests.java new file mode 100644 index 0000000000..6a1f281cf2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentContextHierarchyTests.java @@ -0,0 +1,100 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInParentContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInParentContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +/** + * Verifies that {@link MockitoBean @MockitoBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only mocked "by type" in the parent. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class) +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoBeanByTypeInParentContextHierarchyTests { + + @MockitoBean(contextName = "parent") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + when(service.greeting()).thenReturn("Mock 1"); + + assertThat(service.greeting()).isEqualTo("Mock 1"); + assertThat(serviceCaller1.getService()).isSameAs(service); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Mock 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 1"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java similarity index 84% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java index b5f02fa893..aef8cd39cb 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.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. @@ -14,11 +14,10 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.integration; +package org.springframework.test.context.bean.override.mockito.hierarchies; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -50,18 +49,14 @@ public class MockitoSpyBeanAndContextHierarchyChildIntegrationTests extends @MockitoSpyBean ExampleServiceCaller serviceCaller; - @Autowired - ApplicationContext context; - @Test @Override - void test() { - assertThat(context).as("child ApplicationContext").isNotNull(); - assertThat(context.getParent()).as("parent ApplicationContext").isNotNull(); - assertThat(context.getParent().getParent()).as("grandparent ApplicationContext").isNull(); - + void test(ApplicationContext context) { ApplicationContext parentContext = context.getParent(); + assertThat(parentContext).as("parent ApplicationContext").isNotNull(); + assertThat(parentContext.getParent()).as("grandparent ApplicationContext").isNull(); + assertThat(parentContext.getBeanNamesForType(ExampleService.class)).hasSize(1); assertThat(parentContext.getBeanNamesForType(ExampleServiceCaller.class)).isEmpty(); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInChildContextHierarchyTests.java new file mode 100644 index 0000000000..63c3561d08 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInChildContextHierarchyTests.java @@ -0,0 +1,110 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsNotSpy; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only spied on "by name" in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoSpyBeanByNameInChildContextHierarchyTests { + + @MockitoSpyBean(name = "service", contextName = "child") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test(ApplicationContext context) { + ExampleService serviceInParent = context.getParent().getBean(ExampleService.class); + + assertIsNotSpy(serviceInParent); + assertIsSpy(service); + + assertThat(service.greeting()).isEqualTo("Service 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.java new file mode 100644 index 0000000000..255f3630be --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.java @@ -0,0 +1,110 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * identical beans are spied on "by name" in the parent and in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoSpyBeanByNameInParentAndChildContextHierarchyTests { + + @MockitoSpyBean(name = "service", contextName = "parent") + ExampleService serviceInParent; + + @MockitoSpyBean(name = "service", contextName = "child") + ExampleService serviceInChild; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + assertIsSpy(serviceInParent); + assertIsSpy(serviceInChild); + + assertThat(serviceInParent.greeting()).isEqualTo("Service 1"); + assertThat(serviceInChild.greeting()).isEqualTo("Service 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(serviceInChild); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyV2Tests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyV2Tests.java new file mode 100644 index 0000000000..951d37b962 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyV2Tests.java @@ -0,0 +1,82 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * This is effectively a one-to-one copy of + * {@link MockitoSpyBeanByNameInParentAndChildContextHierarchyTests}, except + * that this test class uses different names for the context hierarchy levels: + * level-1 and level-2 instead of parent and child. + * + *

If the context cache is broken, either this test class or + * {@code MockitoSpyBeanByNameInParentAndChildContextHierarchyTests} will fail + * when run within the same test suite. + * + * @author Sam Brannen + * @since 6.2.6 + * @see MockitoSpyBeanByNameInParentAndChildContextHierarchyTests + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.Config1.class, name = "level-1"), + @ContextConfiguration(classes = MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.Config2.class, name = "level-2") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoSpyBeanByNameInParentAndChildContextHierarchyV2Tests { + + @MockitoSpyBean(name = "service", contextName = "level-1") + ExampleService serviceInParent; + + @MockitoSpyBean(name = "service", contextName = "level-2") + ExampleService serviceInChild; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + assertIsSpy(serviceInParent); + assertIsSpy(serviceInChild); + + assertThat(serviceInParent.greeting()).isEqualTo("Service 1"); + assertThat(serviceInChild.greeting()).isEqualTo("Service 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(serviceInChild); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentContextHierarchyTests.java new file mode 100644 index 0000000000..13e1eba1b1 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentContextHierarchyTests.java @@ -0,0 +1,100 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInParentContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInParentContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only spied on "by name" in the parent. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class) +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoSpyBeanByNameInParentContextHierarchyTests { + + @MockitoSpyBean(name = "service", contextName = "parent") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + assertIsSpy(service); + + assertThat(service.greeting()).isEqualTo("Service 1"); + assertThat(serviceCaller1.getService()).isSameAs(service); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 1"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInChildContextHierarchyTests.java new file mode 100644 index 0000000000..1f71a5e674 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInChildContextHierarchyTests.java @@ -0,0 +1,110 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsNotSpy; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only spied on "by type" in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoSpyBeanByTypeInChildContextHierarchyTests { + + @MockitoSpyBean(contextName = "child") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test(ApplicationContext context) { + ExampleService serviceInParent = context.getParent().getBean(ExampleService.class); + + assertIsNotSpy(serviceInParent); + assertIsSpy(service); + + assertThat(service.greeting()).isEqualTo("Service 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests.java new file mode 100644 index 0000000000..90cf7d2856 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests.java @@ -0,0 +1,110 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * identical beans are spied on "by type" in the parent and in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests { + + @MockitoSpyBean(contextName = "parent") + ExampleService serviceInParent; + + @MockitoSpyBean(contextName = "child") + ExampleService serviceInChild; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + assertIsSpy(serviceInParent); + assertIsSpy(serviceInChild); + + assertThat(serviceInParent.greeting()).isEqualTo("Service 1"); + assertThat(serviceInChild.greeting()).isEqualTo("Service 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(serviceInChild); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentContextHierarchyTests.java new file mode 100644 index 0000000000..3d0d841c8f --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentContextHierarchyTests.java @@ -0,0 +1,100 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInParentContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInParentContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only spied on "by type" in the parent. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class) +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoSpyBeanByTypeInParentContextHierarchyTests { + + @MockitoSpyBean(contextName = "parent") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + assertIsSpy(service); + + assertThat(service.greeting()).isEqualTo("Service 1"); + assertThat(serviceCaller1.getService()).isSameAs(service); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 1"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests.java new file mode 100644 index 0000000000..e4fb4d16c3 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests.java @@ -0,0 +1,113 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * identical beans are spied on "by type" in the parent and in the child and + * configured via class-level {@code @MockitoSpyBean} declarations. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +@MockitoSpyBean(types = ExampleService.class, contextName = "parent") +@MockitoSpyBean(types = ExampleService.class, contextName = "child") +class MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests { + + @Autowired + ExampleService serviceInChild; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test(ApplicationContext context) { + ExampleService serviceInParent = context.getParent().getBean(ExampleService.class); + + assertIsSpy(serviceInParent); + assertIsSpy(serviceInChild); + + assertThat(serviceInParent.greeting()).isEqualTo("Service 1"); + assertThat(serviceInChild.greeting()).isEqualTo("Service 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(serviceInChild); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV1Tests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV1Tests.java new file mode 100644 index 0000000000..b5dc403a72 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV1Tests.java @@ -0,0 +1,66 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +/** + * If the {@link ApplicationContext} for {@link ErrorIfContextReloadedConfig} is + * loaded twice (i.e., not properly cached), either this test class or + * {@link ReusedParentConfigV2Tests} will fail when both test classes are run + * within the same test suite. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = ErrorIfContextReloadedConfig.class), + @ContextConfiguration(classes = FooService.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class ReusedParentConfigV1Tests { + + @Autowired + ErrorIfContextReloadedConfig sharedConfig; + + @MockitoBean(contextName = "child") + FooService fooService; + + + @Test + void test(ApplicationContext context) { + assertThat(context.getParent().getBeanNamesForType(FooService.class)).isEmpty(); + assertThat(context.getBeanNamesForType(FooService.class)).hasSize(1); + + given(fooService.foo()).willReturn("mock"); + assertThat(fooService.foo()).isEqualTo("mock"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV2Tests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV2Tests.java new file mode 100644 index 0000000000..009d85e163 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV2Tests.java @@ -0,0 +1,66 @@ +/* + * 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.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +/** + * If the {@link ApplicationContext} for {@link ErrorIfContextReloadedConfig} is + * loaded twice (i.e., not properly cached), either this test class or + * {@link ReusedParentConfigV1Tests} will fail when both test classes are run + * within the same test suite. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = ErrorIfContextReloadedConfig.class), + @ContextConfiguration(classes = BarService.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class ReusedParentConfigV2Tests { + + @Autowired + ErrorIfContextReloadedConfig sharedConfig; + + @MockitoBean(contextName = "child") + BarService barService; + + + @Test + void test(ApplicationContext context) { + assertThat(context.getParent().getBeanNamesForType(BarService.class)).isEmpty(); + assertThat(context.getBeanNamesForType(BarService.class)).hasSize(1); + + given(barService.bar()).willReturn("mock"); + assertThat(barService.bar()).isEqualTo("mock"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByNameTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByNameTests.java new file mode 100644 index 0000000000..63e9b6ee07 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByNameTests.java @@ -0,0 +1,108 @@ +/* + * 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.test.context.bean.override.mockito.integration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; + +/** + * Tests for {@link MockitoBean @MockitoBean} where the mocked bean is associated + * with a custom {@link Qualifier @Qualifier} annotation and the bean to override + * is selected by name. + * + * @author Sam Brannen + * @since 6.2.6 + * @see gh-34646 + * @see MockitoBeanWithCustomQualifierAnnotationByTypeTests + */ +@ExtendWith(SpringExtension.class) +class MockitoBeanWithCustomQualifierAnnotationByNameTests { + + @MockitoBean(name = "qualifiedService", enforceOverride = true) + @MyQualifier + ExampleService service; + + @Autowired + ExampleServiceCaller caller; + + + @Test + void test(ApplicationContext context) { + assertIsMock(service); + assertMockName(service, "qualifiedService"); + assertThat(service).isNotInstanceOf(QualifiedService.class); + + // Since the 'service' field's type is ExampleService, the QualifiedService + // bean in the @Configuration class effectively gets removed from the context, + // or rather it never gets created because we register an ExampleService as + // a manual singleton in its place. + assertThat(context.getBeanNamesForType(QualifiedService.class)).isEmpty(); + assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).hasSize(1); + + when(service.greeting()).thenReturn("mock!"); + assertThat(caller.sayGreeting()).isEqualTo("I say mock!"); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + QualifiedService qualifiedService() { + return new QualifiedService(); + } + + @Bean + ExampleServiceCaller myServiceCaller(@MyQualifier ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @interface MyQualifier { + } + + @MyQualifier + static class QualifiedService implements ExampleService { + + @Override + public String greeting() { + return "Qualified service"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByTypeTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByTypeTests.java new file mode 100644 index 0000000000..acf1fd66f4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByTypeTests.java @@ -0,0 +1,108 @@ +/* + * 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.test.context.bean.override.mockito.integration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; + +/** + * Tests for {@link MockitoBean @MockitoBean} where the mocked bean is associated + * with a custom {@link Qualifier @Qualifier} annotation and the bean to override + * is selected by type. + * + * @author Sam Brannen + * @since 6.2.6 + * @see gh-34646 + * @see MockitoBeanWithCustomQualifierAnnotationByNameTests + */ +@ExtendWith(SpringExtension.class) +class MockitoBeanWithCustomQualifierAnnotationByTypeTests { + + @MockitoBean(enforceOverride = true) + @MyQualifier + ExampleService service; + + @Autowired + ExampleServiceCaller caller; + + + @Test + void test(ApplicationContext context) { + assertIsMock(service); + assertMockName(service, "qualifiedService"); + assertThat(service).isNotInstanceOf(QualifiedService.class); + + // Since the 'service' field's type is ExampleService, the QualifiedService + // bean in the @Configuration class effectively gets removed from the context, + // or rather it never gets created because we register an ExampleService as + // a manual singleton in its place. + assertThat(context.getBeanNamesForType(QualifiedService.class)).isEmpty(); + assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).hasSize(1); + + when(service.greeting()).thenReturn("mock!"); + assertThat(caller.sayGreeting()).isEqualTo("I say mock!"); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + QualifiedService qualifiedService() { + return new QualifiedService(); + } + + @Bean + ExampleServiceCaller myServiceCaller(@MyQualifier ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @interface MyQualifier { + } + + @MyQualifier + static class QualifiedService implements ExampleService { + + @Override + public String greeting() { + return "Qualified service"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByNameTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByNameTests.java new file mode 100644 index 0000000000..f3d1fc1c37 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByNameTests.java @@ -0,0 +1,106 @@ +/* + * 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.test.context.bean.override.mockito.integration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; + +/** + * Tests for {@link MockitoSpyBean @MockitoSpyBean} where the mocked bean is associated + * with a custom {@link Qualifier @Qualifier} annotation and the bean to override + * is selected by name. + * + * @author Sam Brannen + * @since 6.2.6 + * @see gh-34646 + * @see MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests + */ +@ExtendWith(SpringExtension.class) +class MockitoSpyBeanWithCustomQualifierAnnotationByNameTests { + + @MockitoSpyBean(name = "qualifiedService") + @MyQualifier + ExampleService service; + + @Autowired + ExampleServiceCaller caller; + + + @Test + void test(ApplicationContext context) { + assertIsSpy(service); + assertMockName(service, "qualifiedService"); + assertThat(service).isInstanceOf(QualifiedService.class); + + assertThat(context.getBeanNamesForType(QualifiedService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).hasSize(1); + + when(service.greeting()).thenReturn("mock!"); + assertThat(caller.sayGreeting()).isEqualTo("I say mock!"); + verify(service).greeting(); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + QualifiedService qualifiedService() { + return new QualifiedService(); + } + + @Bean + ExampleServiceCaller myServiceCaller(@MyQualifier ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @interface MyQualifier { + } + + @MyQualifier + static class QualifiedService implements ExampleService { + + @Override + public String greeting() { + return "Qualified service"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests.java new file mode 100644 index 0000000000..197eedc5b0 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests.java @@ -0,0 +1,106 @@ +/* + * 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.test.context.bean.override.mockito.integration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; + +/** + * Tests for {@link MockitoSpyBean @MockitoSpyBean} where the mocked bean is associated + * with a custom {@link Qualifier @Qualifier} annotation and the bean to override + * is selected by name. + * + * @author Sam Brannen + * @since 6.2.6 + * @see gh-34646 + * @see MockitoSpyBeanWithCustomQualifierAnnotationByNameTests + */ +@ExtendWith(SpringExtension.class) +class MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests { + + @MockitoSpyBean + @MyQualifier + ExampleService service; + + @Autowired + ExampleServiceCaller caller; + + + @Test + void test(ApplicationContext context) { + assertIsSpy(service); + assertMockName(service, "qualifiedService"); + assertThat(service).isInstanceOf(QualifiedService.class); + + assertThat(context.getBeanNamesForType(QualifiedService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).hasSize(1); + + when(service.greeting()).thenReturn("mock!"); + assertThat(caller.sayGreeting()).isEqualTo("I say mock!"); + verify(service).greeting(); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + QualifiedService qualifiedService() { + return new QualifiedService(); + } + + @Bean + ExampleServiceCaller myServiceCaller(@MyQualifier ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @interface MyQualifier { + } + + @MyQualifier + static class QualifiedService implements ExampleService { + + @Override + public String greeting() { + return "Qualified service"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTestNGTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTestNGTests.java index f0663a50e2..033eb1b027 100644 --- a/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTestNGTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTestNGTests.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. @@ -31,6 +31,7 @@ import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; import org.springframework.test.context.testng.TrackingTestNGTestListener; @@ -162,12 +163,18 @@ class ClassLevelDirtiesContextTestNGTests { // ------------------------------------------------------------------- - @TestExecutionListeners(listeners = { DependencyInjectionTestExecutionListener.class, - DirtiesContextTestExecutionListener.class }, inheritListeners = false) @ContextConfiguration + // Ensure that we do not include the EventPublishingTestExecutionListener + // since it will access the ApplicationContext for each method in the + // TestExecutionListener API, thus distorting our cache hit/miss results. + @TestExecutionListeners({ + DirtiesContextBeforeModesTestExecutionListener.class, + DependencyInjectionTestExecutionListener.class, + DirtiesContextTestExecutionListener.class + }) abstract static class BaseTestCase extends AbstractTestNGSpringContextTests { - @Configuration + @Configuration(proxyBeanMethods = false) static class Config { /* no beans */ } diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTests.java index a5527be66e..1ed24d6a84 100644 --- a/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTests.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,7 +21,8 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.testkit.engine.EngineTestKit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -31,15 +32,14 @@ import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.springframework.test.context.cache.ContextCacheTestUtils.assertContextCacheStatistics; import static org.springframework.test.context.cache.ContextCacheTestUtils.resetContextCache; -import static org.springframework.test.context.junit4.JUnitTestingUtils.runTestsAndAssertCounters; /** * JUnit based integration test which verifies correct {@linkplain ContextCache @@ -131,15 +131,24 @@ class ClassLevelDirtiesContextTests { 0, cacheHits.incrementAndGet(), cacheMisses.get()); } - private void runTestClassAndAssertStats(Class testClass, int expectedTestCount) throws Exception { - runTestsAndAssertCounters(testClass, expectedTestCount, 0, expectedTestCount, 0, 0); - } - private void assertBehaviorForCleanTestCase() throws Exception { runTestClassAndAssertStats(CleanTestCase.class, 1); assertContextCacheStatistics("after clean test class", 1, cacheHits.get(), cacheMisses.incrementAndGet()); } + private void runTestClassAndAssertStats(Class testClass, int expectedTestCount) throws Exception { + EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(testClass)) + .execute() + .testEvents() + .assertStatistics(stats -> stats + .started(expectedTestCount) + .finished(expectedTestCount) + .succeeded(expectedTestCount) + .failed(0) + .aborted(0)); + } + @AfterAll static void verifyFinalCacheState() { assertContextCacheStatistics("AfterClass", 0, cacheHits.get(), cacheMisses.get()); @@ -148,7 +157,7 @@ class ClassLevelDirtiesContextTests { // ------------------------------------------------------------------- - @RunWith(SpringRunner.class) + @ExtendWith(SpringExtension.class) @ContextConfiguration // Ensure that we do not include the EventPublishingTestExecutionListener // since it will access the ApplicationContext for each method in the @@ -160,7 +169,7 @@ class ClassLevelDirtiesContextTests { }) abstract static class BaseTestCase { - @Configuration + @Configuration(proxyBeanMethods = false) static class Config { /* no beans */ } @@ -175,75 +184,75 @@ class ClassLevelDirtiesContextTests { } } - public static final class CleanTestCase extends BaseTestCase { + static final class CleanTestCase extends BaseTestCase { - @org.junit.Test - public void verifyContextWasAutowired() { + @Test + void verifyContextWasAutowired() { assertApplicationContextWasAutowired(); } } @DirtiesContext - public static class ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends BaseTestCase { + static class ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends BaseTestCase { - @org.junit.Test - public void verifyContextWasAutowired() { + @Test + void verifyContextWasAutowired() { assertApplicationContextWasAutowired(); } } - public static class InheritedClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends + static class InheritedClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase { } @DirtiesContext(classMode = ClassMode.AFTER_CLASS) - public static class ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends BaseTestCase { + static class ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends BaseTestCase { - @org.junit.Test - public void verifyContextWasAutowired() { + @Test + void verifyContextWasAutowired() { assertApplicationContextWasAutowired(); } } - public static class InheritedClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends + static class InheritedClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase { } @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) - public static class ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends BaseTestCase { + static class ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends BaseTestCase { - @org.junit.Test - public void verifyContextWasAutowired1() { + @Test + void verifyContextWasAutowired1() { assertApplicationContextWasAutowired(); } - @org.junit.Test - public void verifyContextWasAutowired2() { + @Test + void verifyContextWasAutowired2() { assertApplicationContextWasAutowired(); } - @org.junit.Test - public void verifyContextWasAutowired3() { + @Test + void verifyContextWasAutowired3() { assertApplicationContextWasAutowired(); } } - public static class InheritedClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends + static class InheritedClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase { } @DirtiesContext - public static class ClassLevelDirtiesContextWithDirtyMethodsTestCase extends BaseTestCase { + static class ClassLevelDirtiesContextWithDirtyMethodsTestCase extends BaseTestCase { - @org.junit.Test + @Test @DirtiesContext - public void dirtyContext() { + void dirtyContext() { assertApplicationContextWasAutowired(); } } - public static class InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase extends + static class InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase extends ClassLevelDirtiesContextWithDirtyMethodsTestCase { } diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/MethodLevelDirtiesContextTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/MethodLevelDirtiesContextTests.java index 72dbddc0ea..323d7eb6d3 100644 --- a/spring-test/src/test/java/org/springframework/test/context/cache/MethodLevelDirtiesContextTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/cache/MethodLevelDirtiesContextTests.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,7 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.annotation.DirtiesContext.MethodMode.BEFORE_METHOD; /** - * Integration test which verifies correct interaction between the + * Integration tests which verify correct interaction between the * {@link DirtiesContextBeforeModesTestExecutionListener}, * {@link DependencyInjectionTestExecutionListener}, and * {@link DirtiesContextTestExecutionListener} when diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionContextCacheTests.java index 1e67f03145..2c0861c8a2 100644 --- a/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionContextCacheTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionContextCacheTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,10 +37,9 @@ import static org.springframework.test.context.cache.ContextCacheTestUtils.asser import static org.springframework.test.context.cache.ContextCacheTestUtils.resetContextCache; /** - * Unit tests which verify correct {@link ContextCache - * application context caching} in conjunction with the - * {@link SpringExtension} and the {@link DirtiesContext - * @DirtiesContext} annotation at the method level. + * JUnit based integration test which verifies correct {@linkplain ContextCache + * application context caching} in conjunction with the {@link SpringExtension} and + * {@link DirtiesContext @DirtiesContext} at the method level. * * @author Sam Brannen * @author Juergen Hoeller @@ -48,7 +47,7 @@ import static org.springframework.test.context.cache.ContextCacheTestUtils.reset * @see ContextCacheTests * @see LruContextCacheTests */ -@SpringJUnitConfig(locations = "../junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml") +@SpringJUnitConfig(locations = "../config/CoreContextConfigurationAppCtxTests-context.xml") @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class }) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class SpringExtensionContextCacheTests { diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/AbsolutePathSpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/config/AbsolutePathContextConfigurationAppCtxTests.java similarity index 62% rename from spring-test/src/test/java/org/springframework/test/context/junit4/AbsolutePathSpringJUnit4ClassRunnerAppCtxTests.java rename to spring-test/src/test/java/org/springframework/test/context/config/AbsolutePathContextConfigurationAppCtxTests.java index 56caa8c903..4cc1559eaf 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/AbsolutePathSpringJUnit4ClassRunnerAppCtxTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/config/AbsolutePathContextConfigurationAppCtxTests.java @@ -14,22 +14,22 @@ * limitations under the License. */ -package org.springframework.test.context.junit4; +package org.springframework.test.context.config; import org.springframework.test.context.ContextConfiguration; /** - * Extension of {@link SpringJUnit4ClassRunnerAppCtxTests}, which verifies that + * Extension of {@link CoreContextConfigurationAppCtxTests}, which verifies that * we can specify an explicit, absolute path location for our * application context. * * @author Sam Brannen * @since 2.5 - * @see SpringJUnit4ClassRunnerAppCtxTests - * @see ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests - * @see RelativePathSpringJUnit4ClassRunnerAppCtxTests + * @see CoreContextConfigurationAppCtxTests + * @see ClassPathResourceContextConfigurationAppCtxTests + * @see RelativePathContextConfigurationAppCtxTests */ -@ContextConfiguration(locations = { SpringJUnit4ClassRunnerAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH }, inheritLocations = false) -public class AbsolutePathSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests { +@ContextConfiguration(locations = CoreContextConfigurationAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH, inheritLocations = false) +class AbsolutePathContextConfigurationAppCtxTests extends CoreContextConfigurationAppCtxTests { /* all tests are in the parent class. */ } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/config/AnnotationConfigContextConfigurationAppCtxTests.java similarity index 71% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java rename to spring-test/src/test/java/org/springframework/test/context/config/AnnotationConfigContextConfigurationAppCtxTests.java index cc368b2862..e55e6840ef 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/config/AnnotationConfigContextConfigurationAppCtxTests.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. @@ -14,19 +14,18 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.config; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunnerAppCtxTests; /** * Integration tests that verify support for configuration classes in * the Spring TestContext Framework. * - *

Furthermore, by extending {@link SpringJUnit4ClassRunnerAppCtxTests}, + *

Furthermore, by extending {@link CoreContextConfigurationAppCtxTests}, * this class also verifies support for several basic features of the * Spring TestContext Framework. See JavaDoc in - * {@code SpringJUnit4ClassRunnerAppCtxTests} for details. + * {@link CoreContextConfigurationAppCtxTests} for details. * *

Configuration will be loaded from {@link PojoAndStringConfig}. * @@ -34,6 +33,6 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunnerAppCtxTest * @since 3.1 */ @ContextConfiguration(classes = PojoAndStringConfig.class, inheritLocations = false) -public class AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests { +class AnnotationConfigContextConfigurationAppCtxTests extends CoreContextConfigurationAppCtxTests { /* all tests are in the parent class. */ } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr6128/AutowiredQualifierTests.java b/spring-test/src/test/java/org/springframework/test/context/config/AutowiredQualifierTests.java similarity index 56% rename from spring-test/src/test/java/org/springframework/test/context/junit4/spr6128/AutowiredQualifierTests.java rename to spring-test/src/test/java/org/springframework/test/context/config/AutowiredQualifierTests.java index 8436a50c95..d46cda1c46 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr6128/AutowiredQualifierTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/config/AutowiredQualifierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -14,43 +14,59 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.spr6128; +package org.springframework.test.context.config; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests to verify claims made in SPR-6128. + * Integration tests to verify claims made in + * gh-10796. * * @author Sam Brannen * @author Chris Beams * @since 3.0 */ +@ExtendWith(SpringExtension.class) @ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) -public class AutowiredQualifierTests { +class AutowiredQualifierTests { @Autowired - private String foo; + String foo; @Autowired @Qualifier("customFoo") - private String customFoo; + String customFoo; @Test - public void test() { + void test() { assertThat(foo).isEqualTo("normal"); assertThat(customFoo).isEqualTo("custom"); } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + String foo() { + return "normal"; + } + + @Bean + String customFoo() { + return "custom"; + } + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingDefaultConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/config/BeanOverridingDefaultConfigClassesInheritedTests.java similarity index 82% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingDefaultConfigClassesInheritedTests.java rename to spring-test/src/test/java/org/springframework/test/context/config/BeanOverridingDefaultConfigClassesInheritedTests.java index 9e0b2378dd..f9b1b3db0c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingDefaultConfigClassesInheritedTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/config/BeanOverridingDefaultConfigClassesInheritedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.config; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.beans.testfixture.beans.Employee; import org.springframework.context.annotation.Bean; @@ -36,13 +36,21 @@ import static org.assertj.core.api.Assertions.assertThat; * @since 3.1 */ @ContextConfiguration -public class BeanOverridingDefaultConfigClassesInheritedTests extends DefaultConfigClassesBaseTests { +class BeanOverridingDefaultConfigClassesInheritedTests extends DefaultConfigClassesBaseTests { - @Configuration + @Test + @Override + void verifyEmployeeSetFromBaseContextConfig() { + assertThat(this.employee).as("The employee should have been autowired.").isNotNull(); + assertThat(this.employee.getName()).as("The employee bean should have been overridden.").isEqualTo("Yoda"); + } + + + @Configuration(proxyBeanMethods = false) static class ContextConfiguration { @Bean - public Employee employee() { + Employee employee() { Employee employee = new Employee(); employee.setName("Yoda"); employee.setAge(900); @@ -51,12 +59,4 @@ public class BeanOverridingDefaultConfigClassesInheritedTests extends DefaultCon } } - - @Test - @Override - public void verifyEmployeeSetFromBaseContextConfig() { - assertThat(this.employee).as("The employee should have been autowired.").isNotNull(); - assertThat(this.employee.getName()).as("The employee bean should have been overridden.").isEqualTo("Yoda"); - } - } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingExplicitConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/config/BeanOverridingExplicitConfigClassesInheritedTests.java similarity index 82% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingExplicitConfigClassesInheritedTests.java rename to spring-test/src/test/java/org/springframework/test/context/config/BeanOverridingExplicitConfigClassesInheritedTests.java index 31fb3d0ecf..c2170704e9 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingExplicitConfigClassesInheritedTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/config/BeanOverridingExplicitConfigClassesInheritedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.config; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.test.context.ContextConfiguration; @@ -33,11 +33,11 @@ import static org.assertj.core.api.Assertions.assertThat; * @since 3.1 */ @ContextConfiguration(classes = BeanOverridingDefaultConfigClassesInheritedTests.ContextConfiguration.class) -public class BeanOverridingExplicitConfigClassesInheritedTests extends ExplicitConfigClassesBaseTests { +class BeanOverridingExplicitConfigClassesInheritedTests extends ExplicitConfigClassesBaseTests { @Test @Override - public void verifyEmployeeSetFromBaseContextConfig() { + void verifyEmployeeSetFromBaseContextConfig() { assertThat(this.employee).as("The employee should have been autowired.").isNotNull(); assertThat(this.employee.getName()).as("The employee bean should have been overridden.").isEqualTo("Yoda"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/config/ClassPathResourceContextConfigurationAppCtxTests.java similarity index 55% rename from spring-test/src/test/java/org/springframework/test/context/junit4/ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests.java rename to spring-test/src/test/java/org/springframework/test/context/config/ClassPathResourceContextConfigurationAppCtxTests.java index cbdc15cf9c..df4c3f7aae 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/config/ClassPathResourceContextConfigurationAppCtxTests.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. @@ -14,36 +14,35 @@ * limitations under the License. */ -package org.springframework.test.context.junit4; +package org.springframework.test.context.config; import org.springframework.test.context.ContextConfiguration; import org.springframework.util.ResourceUtils; /** - * Extension of {@link SpringJUnit4ClassRunnerAppCtxTests}, which verifies that + * Extension of {@link CoreContextConfigurationAppCtxTests}, which verifies that * we can specify an explicit, classpath location for our application * context. * * @author Sam Brannen * @since 2.5 - * @see SpringJUnit4ClassRunnerAppCtxTests + * @see CoreContextConfigurationAppCtxTests * @see #CLASSPATH_CONTEXT_RESOURCE_PATH - * @see AbsolutePathSpringJUnit4ClassRunnerAppCtxTests - * @see RelativePathSpringJUnit4ClassRunnerAppCtxTests + * @see AbsolutePathContextConfigurationAppCtxTests + * @see RelativePathContextConfigurationAppCtxTests */ -@ContextConfiguration(locations = { ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests.CLASSPATH_CONTEXT_RESOURCE_PATH }, inheritLocations = false) -public class ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests { +@ContextConfiguration(locations = { ClassPathResourceContextConfigurationAppCtxTests.CLASSPATH_CONTEXT_RESOURCE_PATH }, inheritLocations = false) +class ClassPathResourceContextConfigurationAppCtxTests extends CoreContextConfigurationAppCtxTests { /** * Classpath-based resource path for the application context configuration - * for {@link SpringJUnit4ClassRunnerAppCtxTests}: - * {@code "classpath:/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml"} + * for {@link CoreContextConfigurationAppCtxTests}: {@value} * - * @see SpringJUnit4ClassRunnerAppCtxTests#DEFAULT_CONTEXT_RESOURCE_PATH + * @see CoreContextConfigurationAppCtxTests#DEFAULT_CONTEXT_RESOURCE_PATH * @see ResourceUtils#CLASSPATH_URL_PREFIX */ public static final String CLASSPATH_CONTEXT_RESOURCE_PATH = ResourceUtils.CLASSPATH_URL_PREFIX + - SpringJUnit4ClassRunnerAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH; + CoreContextConfigurationAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH; /* all tests are in the parent class. */ diff --git a/spring-test/src/test/java/org/springframework/test/context/config/ContextConfigTestSuite.java b/spring-test/src/test/java/org/springframework/test/context/config/ContextConfigTestSuite.java new file mode 100644 index 0000000000..485e128b6b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/config/ContextConfigTestSuite.java @@ -0,0 +1,54 @@ +/* + * 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.test.context.config; + +import org.junit.jupiter.api.ClassOrderer; +import org.junit.platform.suite.api.ConfigurationParameter; +import org.junit.platform.suite.api.IncludeClassNamePatterns; +import org.junit.platform.suite.api.IncludeEngines; +import org.junit.platform.suite.api.SelectPackages; +import org.junit.platform.suite.api.Suite; + +/** + * JUnit Platform based test suite annotation-driven configuration class + * support in the Spring TestContext Framework. + * + *

This suite is only intended to be used manually within an IDE. + * + *

Logging Configuration

+ * + *

In order for our log4j2 configuration to be used in an IDE, you must + * set the following system property before running any tests — for + * example, in Run Configurations in Eclipse. + * + *

+ * -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager
+ * 
+ * + * @author Sam Brannen + * @since 3.1 + */ +@Suite +@IncludeEngines("junit-jupiter") +@SelectPackages("org.springframework.test.context.config") +@IncludeClassNamePatterns(".*Tests$") +@ConfigurationParameter( + key = ClassOrderer.DEFAULT_ORDER_PROPERTY_NAME, + value = "org.junit.jupiter.api.ClassOrderer$ClassName" +) +public class ContextConfigTestSuite { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/config/CoreContextConfigurationAppCtxTests.java similarity index 79% rename from spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests.java rename to spring-test/src/test/java/org/springframework/test/context/config/CoreContextConfigurationAppCtxTests.java index 4bbc906dd0..4d00fba6a6 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/config/CoreContextConfigurationAppCtxTests.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. @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.test.context.junit4; +package org.springframework.test.context.config; import jakarta.annotation.Resource; import jakarta.inject.Inject; import jakarta.inject.Named; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.InitializingBean; @@ -33,18 +33,18 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.GenericXmlContextLoader; import static org.assertj.core.api.Assertions.assertThat; /** - * SpringJUnit4ClassRunnerAppCtxTests serves as a proof of concept - * JUnit 4 based test class, which verifies the expected functionality of - * {@link SpringRunner} in conjunction with the following: + * {@code CoreContextConfigurationAppCtxTests} serves as a core test class, which + * verifies the expected functionality of {@link ContextConfiguration @ContextConfiguration} + * in conjunction with the following: * *